[Next.js] App Router의 렌더링 전략 | CSR, SSR, SSG, ISR

Pages Router와 App Router의 렌더링 전략 구현 차이

Pages Router를 사용했을 때는 `getServerSideProps`, `getStaticProps` 등을 사용했기 때문에

API 이름만 봐도 렌더링 전략을 명확하게 구분할 수 있었다.

 

하지만 App Router에서는 더 이상 이러한 API를 사용하지 않고

서버/클라이언트 컴포넌트, 그리고 `fetch()` 함수와 `cache`, `revalidate` 옵션을 조합하여 렌더링 전략을 구현한다.

 

App Router를 사용하면서 생긴 의문

App Router로 프로젝트를 구현하면서 `cache: 'force-cache'` 옵션을 사용했던 적이 있다.

 

"빌드 시"에 HTML 파일을 생성할 의도는 없었고,

그냥 데이터를 서버에서 불러오고, 추가적으로 캐시까지 적용하고 싶었던 것이기 때문에

SSR 렌더링 전략을 이용해 구현한 것이라고 생각했다.

 

그런데 면접 준비를 하면서 문득 “App Router에서 SSG는 어떻게 구현하지?”라는 궁금증이 생겼다.

구현 방법을 찾다 보니 내가 SSR이라고 생각했던 방식이 실제로는 SSG 또는 ISR이었다는 것을 알게 됐다.

 

이 경험을 계기로 App Router에서 각 렌더링 전략이 정확히 어떻게 구현되는지를 정리해 보게 되었다.

저도 개인적으로 공부하면서 작성한 내용이기 때문에, 혹시 틀린 부분이 있다면 댓글로 알려주세요!

 

Server and Client Components

App Router의 렌더링 방식을 정확히 이해하려면,

먼저 서버 컴포넌트와 클라이언트 컴포넌트의 차이부터 짚고 넘어갈 필요가 있다.

 

Next.js App Router에서는

컴포넌트가 서버에서 실행되는지, 클라이언트에서 실행되는지에 따라 렌더링 방식이 달라진다.

 

Server Components

서버 컴포넌트는 서버에서 실행되어 HTML을 생성하는 컴포넌트이다.

서버에서 데이터를 직접 fetch하고, 이를 기반으로 UI를 렌더링 한 후 완성된 HTML을 클라이언트로 전송한다.

 

Next.js App Router에서는 기본적으로 모든 컴포넌트가 서버 컴포넌트로 동작한다.

 

서버 컴포넌트 사용 예시:

  • DB에서 데이터를 불러올 때
  • API 키, 토큰 및 기타 민감한 정보를 클라이언트에 노출하고 싶지 않을 때
  • 브라우저로 전송되는 JavaScript의 양을 줄이기 위해
  • First Contentful Paint (FCP)를 개선하고, 콘텐츠를 클라이언트에 점진적으로 스트리밍하고 싶을 때

 

Client Components

사용자 인터랙션이나 브라우저 API처럼 클라이언트에서만 가능한 기능을 구현할 때는 클라이언트 컴포넌트를 사용해야 한다.

브라우저에서 JavaScript가 이 컴포넌트를 hydrate하여 사용자와의 상호작용을 가능하게 만든다.

 

클라이언트 컴포넌트는 파일 최상단에 반드시 `"use client"` 지시어를 선언해야 한다.

`"use client"`가 선언된 파일에서 import한 모든 컴포넌트와 하위 컴포넌트는 클라이언트 JavaScript 번들에 포함된다.

따라서 클라이언트 컴포넌트는 필요한 부분에만 선택적으로 사용하는 것이 좋다.

 

클라이언트 컴포넌트 사용 예시:

  • 상태 및 이벤트 핸들러: `onClick`, `onChange`
  • 라이프사이클 로직: `useEffect`
  • 브라우저 전용 API: `localStorage`, `window`, `Navigator.geolocation` 등
  • 커스텀 훅

 

Client Side Rendering (CSR)

'use client'; // CSR 선언

import { useEffect, useState } from 'react';

export default function ProductsPage() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(setProducts);
  }, []);

  return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}

CSR은 서버에서 빈 HTML과 JavaScript 파일을 전달받은 후,

브라우저에서 JavaScript가 실행되어 페이지를 렌더링하는 방식이다.

 

Client Side Rendering의 동작 흐름은 다음과 같다.

  1. 사용자가 페이지에 접속하면, 서버는 최소한의 HTML(예: `<div id="root"></div>`)과 JavaScript 파일을 전달한다.
  2. 브라우저는 JS 파일을 다운로드 → 파싱 → 실행하고, 클라이언트 컴포넌트를 마운트한다.
  3. 이후 `useEffect()` 등을 통해 클라이언트에서 직접 데이터를 fetch하고 렌더링한다.

 

애플리케이션이 처음 로드될 때 사용자는 약간의 지연을 느낄 수 있는데,

이는 모든 JavaScript가 완전히 로드되어 실행되기 전까지 페이지가 완전히 렌더링되지 않기 때문이다.

 

페이지가 처음 로드된 후에는 브라우저 안에서 경로에 따라

JS 파일만 교체하여 컴포넌트를 전환하기 때문에 빠르고 부드러운 UI 변환이 가능하다.

 

Server Side Rendering (SSR) | Dynamic Rendering

Next.js App Router에서는 기존의 SSR과 SSG라는 용어 대신, 새로운 렌더링 개념을 도입했다.

  • SSG  Static Rendering (정적 렌더링)
  • SSR  Dynamic Rendering (동적 렌더링)

두 개념은 본질적으로 동일하지만, App Router에서는 개발자가 명시적으로 렌더링 방식을 선택하기보다는

코드의 특성에 따라 Next.js가 자동으로 렌더링 방식을 결정한다는 점에서 차이가 있다.

 

export default async function ProductsPage() {
  const res = await fetch('https://api.example.com/products', {
    cache: 'no-store', // SSR 설정
  });
  const products = await res.json();

  return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}

SSR은 사용자가 페이지를 요청할 때마다 서버에서 실시간으로 HTML을 생성하여 브라우저에 전달하는 렌더링 방식이다.

`cache: 'no-store'`로 설정된 `fetch` 요청이 있는 페이지는 SSR 방식으로 동작한다.

 

Server Side Rendering의 동작 흐름은 다음과 같다.

  1. 사용자가 페이지를 요청할 때마다 서버는 실시간으로 데이터를 fetch한 뒤, 해당 데이터를 기반으로 HTML을 생성한다.
  2. 서버에서 생성된 HTML 파일이 브라우저에 전달된다.
  3. 브라우저는 이 HTML에 연결된 JavaScript를 로드하고, 클라이언트 컴포넌트를 하이드레이션(hydration)하여 사용자와의 상호작용이 가능하도록 만든다.

 

동적 렌더링에는 다음과 같은 장점이 있다.

  1. 실시간 데이터: 동적 렌더링을 사용하면 애플리케이션에서 실시간 또는 자주 업데이트되는 데이터를 표시할 수 있기 때문에, 데이터가 자주 변경되는 애플리케이션에 적합하다.
  2. 개인화 콘텐츠: 대시보드나 사용자 프로필과 같은 개인화된 콘텐츠를 제공하고 사용자 상호 작용에 따라 데이터를 업데이트하기가 더 쉬워진다.
  3. 요청 시간 정보: 동적 렌더링을 사용하면 쿠키나 URL 검색 매개변수와 같이 요청 시점에만 알 수 있는 정보에 액세스 할 수 있다.

 

Static Site Generation (SSG) | Static Rendering

export default async function ProductsPage() {
  const res = await fetch('https://api.example.com/products', {
    cache: 'force-cache', // SSG 설정
  });
  const products = await res.json();

  return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}

SSG는 빌드 시점에 미리 HTML 파일을 생성해 두고,

사용자 요청 시 완성된 정적 HTML을 즉시 전달하는 렌더링 방식이다.

`cache: ' force-cache '`로 설정된 `fetch` 요청이 있는 페이지는 SSG 방식으로 동작한다.

 

Static Site Generation의 동작 흐름은 다음과 같다.

  1. 빌드 시점에 페이지에서 사용하는 `fetch` 요청들이 실행된다. (기본적으로 모든 fetch는 캐시 됨)
  2. 가져온 데이터를 기반으로 빌드 시점에  HTML 파일을 생성하고, 이를 서버에 저장한다.
  3. 사용자가 페이지에 접속하면, 서버는 이미 생성된 정적 HTML을 즉시 전달한다.
  4. 브라우저는 HTML과 함께 전달된 JavaScript를 로드하여 클라이언트 컴포넌트를 hydrate한다.
  5. 하이드레이션이 완료되면 사용자 인터랙션이 가능한 상태가 된다.

 

정적 렌더링에는 다음과 같은 장점이 있다.

  1. 웹사이트 속도 향상: 사전 렌더링된 콘텐츠는 Vercel과 같은 플랫폼에 배포할 때 캐싱되어 전 세계에 배포될 수 있다. 따라서 전 세계 사용자가 웹사이트의 콘텐츠에 더 빠르고 안정적으로 액세스 할 수 있다.
  2. 서버 부하 감소: 콘텐츠가 캐시되므로 서버에서 각 사용자 요청에 대해 콘텐츠를 동적으로 생성할 필요가 없다. 따라서 컴퓨팅 비용을 절감할 수 있다.
  3. SEO: 미리 렌더링된 콘텐츠는 페이지가 로드될 때 이미 콘텐츠를 사용할 수 있으므로 검색 엔진 크롤러가 색인을 생성하기가 더 쉽습니다. 이는 검색 엔진 순위 향상으로 이어질 수 있습니다.

 

정적 렌더링은 블로그 게시물이나 제품 페이지와 같이 사용자 간에 공유되는 데이터가 없는 UI에 유용하며,

정기적으로 업데이트되는 개인화된 데이터가 있는 대시보드 같은 타입에는 최신 데이터를 보여줄 수 없기 때문에 적합하지 않다.

 

Incremental Static Regeneration (ISR)

export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog', { 
    next: { revalidate: 3600 } // 한 시간마다 갱신
  });
  const posts = await data.json();
  
  return (
    <main>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </main>
  )
}

ISR은 정적 페이지를 빌드 이후에도 일정 주기나 조건에 따라 백그라운드에서 자동으로 갱신할 수 있는 렌더링 방식이다.
정적 렌더링의 빠른 속도를 유지하면서도 최신 데이터를 반영할 수 있다는 점이 가장 큰 장점이다.

 

Next.js에서는 `fetch` 요청에 `next: { revalidate: n }` 설정을 추가하는 것으로 ISR을 구현할 수 있다.

 

Incremental Static Regeneration의 동작 흐름은 다음과 같다.

  1. 페이지 빌드 시, 초기 정적 HTML 파일을 생성한다.
  2. 사용자가 페이지에 접근하면, 이미 생성된 HTML 파일을 그대로 전달한다.
  3. 설정된 `revalidate` 시간이 지난 후 누군가 페이지를 요청하면, 기존 HTML은 그대로 제공되지만 백그라운드에서 새 데이터를 기반으로 HTML을 다시 생성한다.
  4. 백그라운드에서 새 HTML 파일이 성공적으로 생성되면, 다음 요청부터는 최신 정적 HTML을 전달한다.

 

또한, 시간 기반 자동 갱신 외에도 `revalidatePath()` 또는 `revalidateTag()`를 이용해

원하는 시점에 직접 캐시를 갱신하는 On-demand ISR을 구현할 수 있다.

 

ISR을 사용하면 다음과 같은 작업을 수행할 수 있다.

  • 전체 사이트를 다시 빌드하지 않고 정적 콘텐츠 업데이트
  • 대부분의 요청에 대해 미리 렌더링된 정적 페이지를 제공하여 서버 부하 감소
  • 적절한 `cache-control` 헤더가 페이지에 자동으로 추가되도록 보장
  • `next build` 시간이 길지 않고 대량의 콘텐츠 페이지를 처리

 

Next.js App Router 렌더링 전략 정리

전략 캐시 설정 설명
CSR 'use client' + useEffect() 브라우저에서 렌더링
SSR cache: 'no-store' 요청 시마다 새 HTML
SSG cache: 'force-cache' 빌드 시 HTML 생성
ISR next: { revalidate: n } 일정 시간마다 정적 페이지 갱신

 

참고 문서