💢 발생한 문제

`"use client"`를 사용하는 컴포넌트에서, 데이터를 서버에 POST 하고 나서, 데이터들이 리스트형태로 되돌아가는 과정에서 최신 데이터가 반영되게 하려면 어떻게 해야할까??

 

Next.js 공식문서는 다음과 같은 상황에 2가지 해결책을 제시한다.

Cached data can be revalidated in two ways

[Time-based revalidation]
Automatically revalidate data after a certain amount of time has passed.
This is useful for data that changes infrequently and freshness is not as critical.

[On-demand revalidation]
Manually revalidate data based on an event (e.g. form submission). On-demand revalidation can use a tag-based or path-based approach to revalidate groups of data at once. This is useful when you want to ensure the latest data is shown as soon as possible (e.g. when content from your headless CMS is updated).

 

Time-based revalidation은 일정 시간마다 캐싱된 값들을 다시 만들도록 하는 것이다.

처음에 이렇게 만들었다가, 클라이언트분께서 Form을 작성한 이후에 다른 탭을 갔다 와야 저장이 된다고 해결해달라고 하셨다. 

개발자가 아닌 분들에게 하나하나 설명드릴 필요도 없고, 그분들도 알 필요 없다고 생각한다.

그래서, 이를 근본적으로 해결하는 방법으로 On-demand revalidation을 사용하기로 했다.

 

 

✏️ First Solution

1. server component fetch function에 revalidate tag를 달아준다.

const fetchData = async (): Promise<{
  data: NoticeType[];
  totalPages: number;
}> => {
  try {
    const res = await fetch(`${baseUrl}/api/getNotice`, {
      next: { tags: ["notice"] }, // Revalidate Tag
    });

    const { data, totalPages } = await res.json();

    return { data: data, totalPages: totalPages };
  } catch (error) {
    console.log(error);
  }
  return { data: [], totalPages: 0 };
};

 

2. Form을 제출하는 Client 컴포넌트에 `"next/cahce"`에서의 `revalidateTag` 함수를 실행시킨다.

  // Form Submit
  const submitHandler = async (isImp: boolean): Promise<undefined> => {
    try {
      // 데이터 만들기
      const data: NoticeType = {
        ...noticeInput,
        contents: contents,
        timeStamp: new Date(),
        important: isImp,
      };
      setNotices(data);      
      
      customRevalidateTag("notice"); // On-Demand revalidation
      
      const nextPath: Path = "/notification?page=1";
      router.push(nextPath);
    } catch (error) {
      console.log("Error Occured on Submitting!", error);
    }
  };

 

💢 Problem

하지만, 문제가 발생했다. revalidateTag는 서버에서 실행하는 코드이므로, "use client"를 사용한 컴포넌트 즉, Form이 있는 컴포넌트에서는 사용할 수 없다.

 

그래서, `revalidateTag`만 사용하는 함수를 분리하고, `import`하여 사용하였다.

 

최종 Solution

// customRevaliateTag.ts
"use server";
import { revalidateTag } from "next/cache";

export async function customRevalidateTag(tag: string) {
  console.log("revalidating");

  revalidateTag(tag);
}
// Form Component

  const submitHandler = async (isImp: boolean): Promise<undefined> => {
    try {
      const data: NoticeType = {
        ...noticeInput,
        contents: contents,
        timeStamp: new Date(),
        important: isImp,
      };
      setNotices(data);      
      
      customRevalidateTag("notice"); // 새로 만든 On-Demand Function
      
      const nextPath: Path = "/notification?page=1";
      router.push(nextPath);
    } catch (error) {
      console.log("Error Occured on Submitting!", error);
    }
  };

 

참고

추가로, 나는 build해서 실행했을때, `revalidate`을 통해 Caching을 다시하는 와중에 `router.push`가 되는 것을 방지하고자, `setTimeout`을 실행시켜주었다.

이때, async Function안에서 setTimeout을 연속적으로 실행시키기 위해 새로 함수를 정의하여서 사용하였다.

 

해당 함수를 이 글을 참고해주세요.

2024.04.30 - [웹 프로그래밍/JavaScript] - async, await + setTimeout 동시에 사용하기

const submitHandler = async (isImp: boolean): Promise<undefined> => {
    try {
      const data: NoticeType = {
        ...noticeInput,
        contents: contents,
        timeStamp: new Date(),
        important: isImp,
      };
      setNotices(data);
      
      setIsSaving(true); // 저장 중임을 표시하기 위해
      customRevalidateTag("notice");
      await delayTimeout(4000); // Promise Timer 함수
      
      const nextPath: Path = "/notification?page=1";
      router.push(nextPath);
    } catch (error) {
      console.log("Error Occured on Submitting!", error);
    }
  };

 

 

참고자료

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating

 

Data Fetching: Fetching, Caching, and Revalidating | Next.js

Learn how to fetch, cache, and revalidate data in your Next.js application.

nextjs.org

https://github.com/vercel/next.js/discussions/58600

 

'웹 프로그래밍 > Next.js' 카테고리의 다른 글

[Next.js] CSR, SSG, SSR 제대로 알자  (4) 2024.03.24

✏️서론

Next.js 공부를 하던중, 여러가지 Rendering 기법이 존재해서 정리해 보았다.

공부를 하기 전에는 "Next.js는 Server Side Rendering 방식을 사용해" 라는 말을 주변에서 굉장히 많이 들어서 Next.js == SSR 이라는 사고방식이 머리에 박혀있었다.

알고보니, Next.js는 SSR만을 (Pre)-Rendering 방식으로 사용하고 있지 않았다.

나와 같은 프린이들은 이글을 읽고 잘못된 지식을 바로잡아야 한다.


⭐CSR > SSG > SSR 순으로 이해해보자.⭐

1. CSR (Client Side Rendering)

→ 하나의 Page에 컴포넌트만 바꿔서 출력하는 SPA기법을 사용하는 React의 대중적인 방식

→ 클라이언트가 Skeleton Html에 Data를 Fetch해서 표시하는 방식 (useEffect + fetch)

→ SEO에 관여할 필요가 없는 페이지 이거나, Pre-Render하지 않아도 되는 페이지인 경우에는 기존처럼 CSR를 사용하면 된다.

친절하게도 NextJS에서 Pre-Fetching 코드를 CustomHook으로 만들어서 제공해주었다. → useSWR Hook

 

공식문서 읽어보고 사용하기

https://swr.vercel.app/docs/getting-started

 

Getting Started – SWR

SWR is a React Hooks library for data fetching. SWR first returns the data from cache (stale), then sends the fetch request (revalidate), and finally comes with the up-to-date data again.

swr.vercel.app

 

 

User가 페이지를 보고 싶은 Request를 날리면 다음과 같은 과정으로 처리된다.

  1. User가 Page Request를 Backend에 날린다.
  2. 브라우저는 뼈대 Html을 그린다.
  3. Server는 Javascript 코드를 제공한다.
  4. JavaScript 코드를 토대로 페이지를 그려나간다.

이때, 4번의 속도가 매우 빨라서, User는 마치 이미 만들어진 페이지를 가져오는 것처럼 보인다.

하지만, 실제로는 뼈대 Html만 받아오는 것이다

→ 이는, SEO (Search Engine Optimization)에 부정적인 영향을 미친다.


(아래 사진) Skeleton HTML만 렌더링된 모습

&rarr; Skeleton HTML만 렌더링된 모습


NextJS는 React와 다른 “Pre-Rendering”을 통해 페이지를 렌더링한다.

공식문서에는 다음과 같이 설명되어 있다.

By default, Next.js pre-renders every page.
This means that Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript.
Pre-rendering can result in better performance and SEO.

Each generated HTML is associated with minimal JavaScript code necessary for that page.
When a page is loaded by the browser, its JavaScript code runs and makes the page fully interactive (this process is called hydration in React).

 

Pre-Render하는 대략적인 과정은 다음과 같다.

  1. User의 Page Request
  2. Server는 뼈대 Html + Javascript 코드를 바탕으로 완성된 페이지를 제공한다.
    → 이때, 사용한 Javascript 코드를 같이 넘겨준다 (Hydration)
    이는, 첫 렌더링을 제외하면 Client Side Rendering 또한 가능하게 하기 위함이다. (event handling)


⭐여기서부터 중요하다⭐

Next.Js는 Pre-Rendering을 하는 시점을 On Build Time vs On Each Request로 구분한다

Build Time에 Pre-Render하는 방법을 Static Site Generation

Each Request에 Pre-Render하는 방법을 Server Side Rendering이라 한다.

그럼 CSR, SSG, SSR이 각각 어떻게 실행되는지 알아보자


Static Side Generation (Recommended)

→ 페이지들은 빌드할 때 Pre-Render된다.

이후, Hydrate을 통해 Regular React App처럼 Interactive한 Aplication으로 사용가능하다.

공식문서

If a page uses Static Generation, the ✏️page HTML is generated at build time.That means in production, the page HTML is generated when you run ⭐`next build`.
This HTML will then be reused on each request. It can be cached by a CDN.
In Next.js, you can statically generate pages with or without data.

How to User SSG?: `getStaticProps` Function

// 형식 정확히 일치 필요!
export async function getStaticProps(context) { … }

 

→ Server에서만 실행되는 코드를 작성한다.
(i.e. Client에서 사용하는 코드는 사용 불가능하다.)

Not Be included in code bundle that server gives to client

→ return 형식이 정해져 있음

return {
	props: {
	    my_data: data
    },
    revalidate: time_in_sec,
    notFound : Boolean,
    redirect : {
    	destination: URL_String,
    }
}


https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props

 

Data Fetching: getStaticProps | Next.js

Fetch data and generate static pages with `getStaticProps`. Learn more about this API for data fetching in Next.js.

nextjs.org


https://nextjs.org/docs/pages/api-reference/functions/get-static-props)

 

Functions: getStaticProps | Next.js

API reference for `getStaticProps`. Learn how to use `getStaticProps` to generate static pages with Next.js.

nextjs.org

 

import path from "path";
import fs from "fs/promises";

export async function getStaticProps() {
    // NodeJS Code To get Access to Data Json File
    // Data Json File Location: "/data/dummy-backend.json"
    
	const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
    const jsonData = await fs.readFile(filePath);
    const data = JSON.parse(jsonData);
    
    return {
        props: {
			products: data.products,
		},
	};
}

function HomePage(props) {
/*
의문점!
props를 상위 컴포넌트로부터 전달받는 경우에는 getStaticProps와 상위 컴포넌트 중에 어떤걸 사용하는 거지?
*/
	const { products } = props;
		return ({products.map((product) => (
			{product.title}
		))}
	);
}
export default HomePage;

그러면 실제로 build 했을때 어떻게 나타날까?

npm run build → Build NextJS Project

- Root Folder (Route : “/”) 가 SSG 되었다

 

데이터가 Pre-Rendering 되었다

→ 이때, props로 넘겨주는 Data는 hydrate에 이용되지 않는다

→ StaticProps는 페이지 검사를 하면 드러나기 때문에 중요한 정보는 표시하지 않아야 한다 (props로 사용하면 안된다는 뜻)

Props (underline : default)

- revalidate : x초 (default : false)

→ ISR을 만들기 위한 값  

- notFound : true | false

→ 404 Page를 리턴할할지 말지를 결정한다.  

- redirect : {destination : String, permanent: true | false}



동적 라우팅을 하는 경우 ([page_name.js]) SSG 사용방법이 다르다.

동적 라우팅 + Static Site Generation

getStaticProps의 context(인자)는 객체이다

key Values
- params : 동적 라우팅에 사용되는 값이다

export async function getStaticProps(context) {
	const { params } = context;
}



Q. `useRouter()의 query` vs `getStaticProps context.params` 같은 코드 아닌가요?

A. No, `useRouter`는 Client Side에서 실행하는 NextJS 코드이다.

즉, Pre-Rendendering에서는 `getStaticProps`의 `context.params`를 사용하고, Client Side에서 사용하고 싶으면 `useRouter`를 사용하자.

이때, NextJs는 동적 라우팅을 Pre-render하기 위해서는 몇개의 페이지를 만들어야 하는지 알려줘야 한다.

(알려주지 않으면 무한개의 동적 라우팅 주소를 만들지도?ㅋㅋ)

By `getStaticPaths`

export async function getStaticPaths ( ) {...}

 

→ 동적 라우팅의 경로들을 미리 알려주는 역할을 한다

해당 동적 라우트로 가는 Link가 있는 페이지는 Build때 Pre-render한다.

 

즉, Pre-render 된다는 것은, 이후에는 마치 SPA처럼 행동한다는 것이다

기본형

// [pid].js 파일에서 가능한 pid값들을 설정해주기
export async function getStaticPaths() {
	return {
		paths: [ // 객체 배열
		{params: {pid: "p1"}, // params가 key인 객체
		{params: {pid: "p2"},
		{params: {pid: "p3"},
	],
	fallback: true | false | "blocking"
	}
}

 

주의사항 및 사용법

동적 중첩 라우트

- If the page name is `pages/posts/[postId]/[commentId]`, then `params` should contain `postId` and `commentId`.

 

동적 라우트

- If the page name uses [catch-all routes](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments))like `pages/[...slug]`, then `params` should contain `slug` (which is an array). If this array is `['hello', 'world']`, then Next.js will statically generate the page at `/hello/world`.
- If the page uses an [optional catch-all route](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes#optional-catch-all-segments),,) use `null`, `[]`, `undefined` or `false` to render the root-most route. For example, if you supply `slug: false` for `pages/[[...slug]]`, Next.js will statically generate the page `/`.

example

// [pid].js
export async function getStaticPaths() {
	const data = await getData();
	const ids = data.products.map((product) => product.id);
	const paths = ids.map((id) => ({
		params: { pid: id },
	}));
	return {
		paths,
		fallback: true,
	};
}

 

What is fallback key?

fallback : true | false | “blocking”

fallback   설명 When to Use 단점
false → getStaticPaths에 명시된 모든 params를 Static Site Generate한다. (Build때 pre-render) → 명시되지 않은 Path에 접근하면 404 Error를 발생시킨다 → 해당 동적 라우트가 내용이 얼마 없을때 → Pre-Render해야 하는 Route가 많거나, 데이터가 많으면 오래 걸린다 (ex. 게시판 or 판매목록)
true → getStaticPaths에 명시된 params만 SSG한다 → 명시되지 않은 Params는 useEffect하는 것처럼 그때 getStaticPaths 코드를 실행한다. → 즉, params에 없는 코드라도 404에러를 발생시키지 않는다 → params에 없으면 “blocking” 처럼 행동한다 → 데이터가 많은데, Pre-Render해야하는 중요한 것만 몇개 있고 나머지는 그러지 않아도 되는 경우  
“blocking” → SSR처럼, 눌렀을때 Pre-Render되기를 기다렸다가 Load된다.    

Error Handling을 하면서 사용하기

 

fallback : false

→ route에 포함되지 않는 경우 404Error 발생.

`404.js` 에서 Error Handling

 

fallback : true

→ route에 포함되지 않는 경우 404Error가 발생하지 않음. 단, getStaticProps에서 데이터를 찾는 과정에서 코드Error가 발생함

→ `getStaticProps`에서 ErrorHandling해야함

export async function getStaticProps(context) {
	const { params } = context;

	const productId = params.pid;
	const data = await getData();
	const product = data.products.find((product) => product.id === productId);
	// Error Handling
	if (!product) {
		return {
			notFound: true,
		};
	}
	return {
		props: {
			loadedProduct: product,
		},
	};
}

 

ISR (Incremental Static Regeneration)

만약에 빌드 → 배포를 통해 프로젝트를 배포하고,😱 코드가 변경되면 NextJS는 빌드를 새로하고 배포를 다시하지 않는한, 변경된 코드가 적용되지 않는다.

cf) npm run dev 는 build를 다시하게끔 되어있어서 값이 바뀐다

따라서, NextJS에서는 이러한 수고를 덜기 위해 Build를 일정시간마다 해주는 방법을 제공한다. → revalidate key

export async function getStaticProps() {
	const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
	const jsonData = await fs.readFile(filePath);
	const data = JSON.parse(jsonData);
	console.log("Regenerated");
	return {
		props: {
			products: data.products,
		},
		revalidate: 5,
	};
}
// 5초마다 build를 재진행한다
// 없으면 false로 간주하여, 재빌드를 하지 않는다

 


이제, On Each Request에 Pre-Render하는 SSR에 대해 알아보자.

Server-Side Rendering

→ Pre-Render할때 즉, Build Time때 알 수 없는 데이터를 가져올 때.

즉, request를 날릴때 데이터를 가져올 수 있는 경우 ex) Authorization headers or geolocation

getServerSideProps

export async function getServerSideProps (context)

→ page 폴더 내에서 사용

→ request 날릴때에만 fetch data

 

cf) build타임때 알 수 있는 데이터이면, 그냥 `getStaticProps` 로 Pre-Fetching해도된다.

→ getServerSideProps는 build할때 pre-Render하지 않으므로, 동적 라우트를 사용하는 경우 getStaticPaths처럼 Route를 지정해줄 필요가 없다.

→ 사용법은 revalidation을 제외하고 `getStaticProps` 와 동일하다

 

// pages/user-profile.js
export async function getServerSideProps() {
	return {
		props: {
			username: "Jiho",
		},
	};
}

 

context : getStaticProps와는 다르게 매번 request, response가 다르므로, res res를 포함한다.

 

export async function getServerSideProps(context) {
	const { params, req, res } = context;
	return {
		props: {
			username: "Jiho",
		},
	};
}

 

https://nextjs.org/docs/pages/api-reference/functions/get-server-side-props)

 

Functions: getServerSideProps | Next.js

API reference for `getServerSideProps`. Learn how to fetch data on each request with Next.js.

nextjs.org

Client Side Rendering + Static Site Rendering 도 가능하다

→ Static Site로 Pre-Fetch하고(no - revalidating), CSR로 바뀐 값들을 가져오는 방법

참고 문헌

https://optinmonster.com/seo-ranking-factors/)

 

Cracking the Code: Unveiling 10 Powerful SEO Ranking Factors

Discover the essential SEO ranking factors to boost your Google rankings. This guide will unlock your website's potential and outrank your competition.

optinmonster.com

https://marketbrew.ai/server-side-rendering-and-seo-the-ultimate-guide)

 

Server-Side Rendering and SEO: The Ultimate Guide

Server-side rendering has long been a topic of discussion in the world of SEO. In this article, we explore the various ways in which server-side rendering can impact SEO, including its effects on loading speed, crawlability, and ranking. We also examine th

marketbrew.ai

 

+ Recent posts