Data Fetching

13 이전까지 사용되었던 getServerSidePropsgetStaticProps는 이제 잊어라!!..는 아니고.. 13부터 도입된 방법이 앞으로 활발하게 사용되더라도, 레거시 코드에는 이 둘이 남아있을테니 아예 잊어버리는 건 안될 것 같다. (참고: [Next.js] Next.js의 프리 렌더링(pre-rendering) 옵션 3가지 / SSG, SSR, ISR)

이 둘은 이름도 뭔가 장황한 느낌이고, 개인적으로는 어떤 상황에 어떤 걸 써야할지 딱 떠오르지가 않았다. 13부터는 data fetching을 할 때 이 둘 대신 fetch API를 사용하면 된다! getServerSidePropsgetStaticProps와 비슷하게 구현하려면 fetch 메서드의 두 번째 인자로 다음과 같이 옵션 값을 주면 된다.

// 직접 무효화 하기 전까지는 이 request는 캐싱됨.
// `getStaticProps`와 비슷! (즉, 빌드 시점에 fetch)
// `force-cache`가 디폴트 값이므로 생략 가능
fetch(URL, { cache: 'force-cache' });

// 매번 요청 때마다 refetch 됨.
// `getServerSideProps`와 비슷!
fetch(URL, { cache: 'no-store' });

// 이 request는 10초동안 캐싱됨.
// This request should be cached with a lifetime of 10 seconds.
// `revalidate` 옵션을 지정한 `getStaticProps`와 비슷!
fetch(URL, { next: { revalidate: 10 } });


동일하게 fetch 메서드를 사용하면서 옵션 값으로 구분하는 방식이 확실히 이전보다 좀 더 직관적인 것 같다. Next.js 13 App Playground를 Vercel을 통해 직접 배포한 뒤 확인해보면 ssg, ssr, isg 각각에 해당하는 페이지가 있고, 각 페이지에서 어떤 식으로 data fetch가 이루어지는지 소스코드와 Last Rendered 시간을 통해 확인해볼 수 있다.

SSG

- Last Rendered 시간을 보면 앱이 빌드된 시점에 데이터를 fetch 해왔음을 알 수 있다.

// app/ssg/[id]/page.tsx

export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }];
}

async function fetchData(params: { id: string }) {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`,
  );
  const data = await res.json();
  return data;
}

export default async function Page({
  params,
}: {
  params?: any;
  children?: React.ReactNode;
}) {
  const data = await fetchData(params);

  return (
    <div className="space-y-4">
      <h1 className="text-2xl font-medium text-gray-100">{data.title}</h1>
      <p className="font-medium text-gray-400">{data.body}</p>
    </div>
  );
}


어차피 'force-cache'가 디폴트라서 옵션은 따로 주지 않았음을 확인할 수 있다.

SSR

- 매 요청 시마다 데이터를 refetch 해오며 그때마다 서버사이드에서 페이지가 생성된다. 이 글 작성한 시점에는 SSR 페이지에서 포스트 클릭하면 500 에러 뜨고 접속이 되지 않아서 Last Rendered 시간은 확인할 수 없었으나.. 아마 페이지가 새로고침될 때마다 refetch를 해서 Last Rendered 시간도 매번 갱신되지 않을까 싶다.

// app/ssr/[id]/page.tsx

async function fetchData(params: { id: string }) {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`,
    { cache: 'no-store' },
  );
  const data = await res.json();
  return data;
}

export default async function Page({
  params,
}: {
  params?: any;
  children?: React.ReactNode;
}) {
  const data = await fetchData(params);

  return (
    <div className="space-y-4">
      <h1 className="text-2xl font-medium text-gray-100">{data.title}</h1>
      <p className="font-medium text-gray-400">{data.body}</p>
    </div>
  );
}


cache 옵션의 값으로 'no-store'을 지정한 것을 확인할 수 있다.

ISR

- 예제에서는 15초를 기준으로 주기적으로 재검증이 일어난다. 만약 15초 이후 해당 페이지에 대한 요청이 다시 들어온다면 데이터를 refetch하고 페이지를 재생성한다.

// app/isr/[id]/page.tsx

export const dynamicParams = true;

export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }];
}

async function fetchData(params: { id: string }) {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`,
    { next: { revalidate: 15 } },
  );
  const data = await res.json();
  return data;
}

export default async function Page({
  params,
}: {
  params?: any;
  children?: React.ReactNode;
}) {
  const data = await fetchData(params);
  return (
    <div className="space-y-4">
      <h1 className="text-2xl font-medium text-gray-100">{data.title}</h1>
      <p className="font-medium text-gray-400">{data.body}</p>
    </div>
  );
}


next 옵션의 값으로 revalidate 시간을 15로 설정했다. MDN이랑 소스코드 보면 이 next 옵션은 기본 fetch 메서드에 있던 건 아닌 것 같고 Next.js에서 fetch를 확장해 둔 것 같다.

interface NextFetchRequestConfig {
  revalidate?: number | false
}

interface RequestInit {
  next?: NextFetchRequestConfig | undefined
}

Server Components

서버 컴포넌트를 이용하여 data fetching을 하는 방식이 유용하다고 하는데, 이 방식을 사용할 때의 장점은 다음과 같다. :
1. 클라이언트 단에서 돌아가지 않는 데이터베이스 및 API 등의 백엔드 서비스에 접근할 수 있다.
2. 보안 키 값들이 클라이언트 단에 드러나지 않도록 지킬 수 있다.
3. data fetching과 렌더링을 동일한 환경에서 수행할 수 있다.
4. 서버에 렌더링을 캐싱할 수 있다.
5. 번들링할 자바스크립트 양을 줄일 수 있다.

클라이언트 컴포넌트가 클라이언트 단에서 렌더링 되는 것과 달리 서버 컴포넌트는 말 그대로 서버 단에서 렌더링된다. 클라이언트는 브라우저를 의미하며, 브라우저는 서버로 요청을 보낸다. 한편 서버는 요청을 받아들일 수 있는 코드를 호스트하는 쪽이며, 요청에 대한 처리가 완료되면 응답을 돌려보낸다.

일단 일반적으로 리액트에선 모든 게 클라이언트 단에서 이루어진다. Next.js는 이러한 리액트 컴포넌트들을 서버에서 부분적으로 렌더링 되는 페이지로 만들어서 클라이언트 단에서 모든 것을 부담하지 않아도 되게끔 개선했다. 근데 이 방식의 단점은, 이렇게 서버 단에서 생성된 HTML에 결국 클라이언트 단에서 hydrate 작업을 진행해야 한다는 것이다. 즉 추가적인 자바스크립트 코드가 필요하다는 의미다.

서버사이드 렌더링은 정적으로도, 동적으로도 구현할 수 있다.
- 정적(static): 서버 단에서 서버 컴포넌트와 클라이언트 컴포넌트 모두 빌드 시에 미리 렌더링될 수 있다. 일단 요청에 따른 응답 결과를 캐싱해두고, 뒤이은 요청에는 그 캐싱해둔 결과를 재사용하는 방식이다. SSG, ISR 방식이 이에 해당한다.
- 동적(dynamic): 서버 컴포넌트와 클라이언트 컴포넌트가 매 요청 시 렌더링되며, 응답 결과는 캐싱되지 않는다. SSR 방식이 이에 해당한다.


언제나 그렇듯이 은탄환은 없으니, 상황에 따라 서버 컴포넌트와 클라이언트 컴포넌트 중 더 적합한 것을 사용해야 할 것이다. 어떤 상황에 어떤 것을 쓰는 게 좋을까? Next.js는 다음과 같이 권장한다. :

- data fetching이 필요한 경우 👉 서버 컴포넌트
- 백엔드 자원에 접근해야 하는 경우 👉 서버 컴포넌트
- 클라이언트에 드러내면 안 되는 민감한 정보가 있을 때 👉 서버 컴포넌트
- 자바스크립트 코드를 줄여야 할 때 👉 서버 컴포넌트
- click, change 리스너 등을 사용하여 대화형(상호작용) 컨텐츠를 구현하려는 경우 👉 클라이언트 컴포넌트
- '상태(state)'을 활용하는 경우 👉 클라이언트 컴포넌트
- 브라우저 상에서만 지원하는 API(예: local storage와 같은 웹 스토리지를 다루는 API)를 사용하는 경우 👉 클라이언트 컴포넌트

Next.js에 따르면 우선 위와 같은 기준을 두고, 가능한 경우에는 서버 컴포넌트로 만들고 따로 클라이언트 컴포넌트로 구현이 필요한 부분들만 추출하는 편이 좋다고 한다.


<참고>
Next 13 - Data fetching

Next 13 - Data fetching

A closer look at data fetching in Next 13

daily-dev-tips.com

Next 13 - Server and Client components

Next 13 - Server and Client components

What is the difference between server and client components in Next 13

daily-dev-tips.com

https://nextjs.org/blog/next-13

Next.js 13

Next.js 13 introduces layouts, React Server Components, and streaming in the app directory, as well as Turbopack, an improved image component, and the brand new font component.

nextjs.org

복사했습니다!