Post

Next.js 의 Rendering 전략

3번의 프로젝트 동안 Next.js 를 사용하고 있었지만 Next.js 의 렌더링 전략의 개념이 살짝 헷갈리는 부분이 있었습니다. 그래서 이번 글에서는 확실히 그 개념을 정리해 보겠습니다.

이 글은 2024-06-23 에 업데이트 되었습니다.

이 글은 HTML, CSS , JavaScript, TypeScript, React, Next.js에 우터를 사용하는 13버전 이상의 버전을 설명하고 있습니다.

Next.js의 렌더링 전략

Next.js 에서 사용하는 공식적인 렌더링 전략은 기본적으로 작성된 컴포넌트가 Server Components 인지 Client Components 인지에 따라 달라집니다.

Server Components

Server Components는 기본적으로 Pre-rendering 방식으로 빌드되며 Server Components는 모두 이 방식을 사용하고 있습니다. 그리고 Pre-rendering 과정에서 data를 어떻게 cache 하느냐에 따라서 Static Rendering, Dynamic Rendering, Streaming 방식으로 나뉘게 됩니다.

Pre-rendering

이미지

Pre-rendering 이란 서버에서 미리 렌더링된 HTML을 제공하는 방식입니다.

우선 유저가 요청을 보내면 Next.js는 서버에서 DOM 요소를 만들어 미리 HTML 문서를 렌더링 합니다. (이 때 유저는 미리 랜더링된 정적 페이지를 보고 있음)

그리고 나서 모든 데이터를 받고나면 Hydration 과정을 거쳐 클라이언트에서 렌더링을 완료합니다. (페이지와 유저가 상호작용 할 수 있음)

Hydration 은 뭔가요? 미리 렌더링 된 뼈대만 있는 HTML에 JavaScript를 결합하여 이벤트가 동작하도록 하는 과정 입니다.

Static Rendering

Static Rendering은 서버에서 미리 렌더링된 HTML을 제공하는 방식은 같지만 빌드 시에 HTML을 만들고 클라이언트에서 요청 시 재사용할 수 있는 방식 입니다. StaticSiteGeneration : SSG 라고도 하지만 공식 문서에서는 Static Rendering 이라고 쓰여져 있습니다.

Next.js는 기본적으로 페이지를 생성할 때 이런 방식을 사용하고 있습니다. 아래의 코드에서 fetch를 사용하여 데이터를 받아오고 있는데 이때 cachedefalut 옵션인 force-cache로 설정하면 빌드 시 요청이 캐시되어 있기 때문에 다음에 다시 요청을 보내지 않고 재사용할 수 있습니다.

Static Rendering 방식은 빌드 시 HTML을 만들기 때문에 가장 빠른속도로 유저와 상호작용 할 수 있으며, 주로 정적 블로그 게시물이나 제품 페이지와 같이 경로에 사용자에게 맞춤화되지 않고 빌드 시점에 알 수 있는 데이터가 있는 경우에 유용합니다.

1
2
3
4
5
6
7
8
9
async function getData() {
  const res = await fetch("https://api.example.com/...");
  return res.json();
}

export default async function Page() {
  const data = await getData();
  return <></>;
}

Dynamic Rendering

Dynamic Rendering은 Static Rendering과는 다르게 클라이언트가 요청 시 마다 HTML을 생성하는 Pre-rendering 방식입니다. ServerSideRendering : SSR 이라고도 하며 공식 문서에는 Dynamic Rendering 이라고 쓰여져 있습니다.

Dynamic Rendering 방식은 fetch를 사용할 때 cache 옵션을 no-store로 설정하면 되는데 이렇게 설정하면 빌드시 요청하지 않고 클라이언트 요청마다 새로운 HTML을 만들어 제공합니다.

또는 Dynamic Functions 라는 함수를 사용하여 서버에 요청하게 되면 자동적으로 Dynamic Rendering 방식으로 동작합니다. Dynamic Functions 는 아래와 같습니다.

  • cookies() and headers(): 서버 컴포넌트에서 사용하면 요청 시 전체 경로가 동적 렌더링으로 선택됩니다.
  • searchParams: 페이지에서 searchParams 프로퍼티를 사용하면 요청 시 페이지가 동적 렌더링으로 선택됩니다.

Dynamic Rendering 방식은 사용자가 에게 맞춤화된 데이터가 있거나 쿠키 또는 URL의 SearchParams와 같이 요청 시점에만 알 수 있는 정보가 있는 경우에 유용합니다.

1
2
3
4
5
6
7
8
9
async function getData() {
  const res = await fetch("https://api.example.com/...", { cache: "no-store" });
  return res.json();
}

export default async function Page() {
  const data = await getData();
  return <></>;
}

Incremental-Static-Regeneration : ISR

Dynamic Rendering은 특정 시간이 지나면 data가 업데이트 되었는지 확인하고 만약 업데이트가 되었다면 새로운 data를 가진 페이지를 생성하여 보여주는 ISR 방식도 사용할 수 있습니다.

아래의 코드와 같이 { next: { revalidate: 10} 옵션을 준다면 Next.js는 10초마다 새로운 데이터를 가진 페이지를 빌드하여 보여줍니다.

ISR 방식은 블로그와 같이 컨텐츠가 동적이지만 자주 변경되지 않는 사이트인 경우 ISR 방식을 사용하면 유용합니다.

1
2
3
4
5
6
7
8
9
10
11
async function getData() {
  const res = await fetch("https://api.example.com/...", {
    next: { revalidate: 10 },
  });
  return res.json();
}

export default async function Page() {
  const data = await getData();
  return <></>;
}

Streaming

스트리밍

Streaming은 서버에서 전체 페이지가 렌더링될 때까지 기다리지 않고 웹 페이지의 HTML 콘텐츠를 청크로 브라우저에 점진적으로 전달할 수 있습니다. 이를 통해 사용자는 페이지의 일부를 더 빨리 볼 수 있습니다. 위의 이미지 처럼 정적인 부분은 미리 보여지게되고 데이터 로딩이 필요한 컴포넌트의 경우 대체 UI로 화면에 표시할 수 있습니다.

스트리밍은 loading.tsx 파일을 이용한 페이지 수준 스트리밍이 있고, React의 Suspense를 이용한 컴포넌트 스트리밍이 있습니다.

페이지 레벨 스트리밍
1
2
3
4
└── app/
    └── dashboard/
        ├── loading.tsx
        └── page.tsx

위와 같은 경로로 페이지를 구성했다고 했을 때 만약에 dashboard 페이지에서 데이터 로딩이 오래 걸리는 경우 대체 UI로 loadind.tsx 파일에 있는 로딩 UI를 보여줄 수 있습니다.

1
2
3
4
// loading.tsx
export default function Loading() {
  return <div>Loading...</div>;
}
컴포넌트 스트리밍

컴포넌트 레벨 스트리밍은 아래의 코드와 같이 Suspense로 ServerComponent 를 감싸고 fallback 으로 대체 UI를 전달하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
export default async function Page() {
  return (
    <main>
      <div>
        <Suspense fallback={<div>Loading...</div>}>
          <RevenueChart />
        </Suspense>
      </div>
    </main>
  );
}

Client Components

클라이언트 컴포넌트는 Client Side Rendering을 사용하며, 서버에서 미리 렌더링된 HTML을 제공하지 않고 클라이언트에서 HTML, CSS, JS등 모든 데이터를 받은 후 렌더링하는 방식입니다. 하지만 Next.js는 이미 모든 클라이언트 및 서버 컴포넌트를 pre-rendering 하기 때문에 유저는 기다릴 필요 없이 페이지의 콘텐츠를 즉시 볼 수 있습니다.

주로 상태, 표과, 이벤트 리스너를 사용할 수 있으므로 유저와 즉각적인 UI를 업데이트할 수 있을 때나 지리적 위치, 로컬스토리지 등 브라우저 API를 직접적으로 사용할 때 유용하게 사용할 수 있습니다.

클라이언트 컴포넌트를 사용하려면 파일의 가장 최상단에 use client 키워드를 작성해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

결론

Next.js 의 가장 큰 장점은 요구사항에 따라 적절한 렌더링 전략을 선택할 수 있다는 것 이었습니다. 정적 콘텐츠가 많은 경우 Static Rendering이, 사용자 맞춤형 데이터가 많은 경우 Dynamic Rendering이, 사용자 상호작용이 빈번한 경우 Client Components가 적합합니다.

이러한 전략들을 적절히 활용하면 Next.js를 통해 렌더링을 최적화하여 사용자 경험을 향상시킬 수 있습니다.

참고 사이트

This post is licensed under CC BY 4.0 by the author.