전체 목록
Next.jsMedium#40

Next.js의 렌더링 방식(SSR, SSG, ISR, CSR)을 비교 설명해주세요.

#Next.js#렌더링#SSR#SSG#성능
힌트

빌드 시점 vs 요청 시점, 데이터 신선도와 성능의 트레이드오프를 생각해보세요.

정답 및 해설

Next.js의 렌더링 방식(SSR, SSG, ISR, CSR)을 비교 설명해주세요.

Next.js는 하나의 프레임워크에서 SSR, SSG, ISR, CSR 네 가지 렌더링 전략을 모두 지원합니다. 각 방식은 HTML을 언제, 어디서 생성하느냐에 따라 구분되며, 데이터의 신선도와 성능 사이의 트레이드오프가 다릅니다.

렌더링 방식 개요

방식HTML 생성 시점데이터 최신성속도SEO
SSG빌드 시낮음 (빌드 시점)매우 빠름좋음
ISR빌드 + 주기적 갱신중간빠름좋음
SSR요청마다높음 (항상 최신)느림좋음
CSR클라이언트 (런타임)높음초기 느림나쁨

SSG (Static Site Generation)

개념

빌드 타임에 HTML을 미리 생성합니다. 생성된 HTML은 CDN에 캐싱되어 요청이 오면 즉시 반환합니다.

빌드 타임:
  데이터 조회 → HTML 생성 → 파일 저장

요청 시:
  사용자 → CDN → 저장된 HTML 즉시 반환

Pages Router에서 SSG

// pages/blog/[id].jsx

// 어떤 경로(id)를 미리 빌드할지 정의
export async function getStaticPaths() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
  return {
    paths: posts.map(post => ({ params: { id: String(post.id) } })),
    fallback: false, // 정의되지 않은 경로는 404
  };
}

// 빌드 시 각 경로에 대해 데이터 조회
export async function getStaticProps({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.id}`)
    .then(r => r.json());
  return {
    props: { post },
  };
}

export default function BlogPost({ post }) {
  return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}

App Router에서 SSG

// app/blog/[id]/page.jsx

// App Router에서는 기본적으로 fetch 결과를 캐싱 (SSG와 유사)
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
  return posts.map(post => ({ id: String(post.id) }));
}

export default async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.id}`, {
    cache: 'force-cache', // 빌드 시 캐시 (SSG)
  }).then(r => r.json());

  return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}

적합한 사용 사례: 블로그, 마케팅 페이지, 문서, 공지사항

ISR (Incremental Static Regeneration)

개념

SSG의 성능과 SSR의 데이터 신선도를 절충한 방식입니다. 빌드 시 HTML을 생성하지만, 지정된 시간(revalidate)이 지나면 백그라운드에서 페이지를 재생성합니다.

첫 번째 요청: 빌드된 HTML 반환 (즉시)
60초 후 요청: 오래된 HTML 반환 + 백그라운드 재생성 시작
재생성 완료 후 요청: 새로운 HTML 반환

Pages Router에서 ISR

// pages/products/[id].jsx

export async function getStaticProps({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`)
    .then(r => r.json());

  return {
    props: { product },
    revalidate: 60, // 60초마다 백그라운드에서 재생성
  };
}

App Router에서 ISR

// app/products/[id]/page.jsx

export default async function ProductPage({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 60 }, // 60초마다 재검증
  }).then(r => r.json());

  return <div><h1>{product.name}</h1><p>가격: {product.price}원</p></div>;
}

On-demand Revalidation (수동 재검증)

시간 기반이 아닌 이벤트 기반으로 페이지를 재생성할 수 있습니다.

// pages/api/revalidate.js
export default async function handler(req, res) {
  const secret = req.query.secret;
  if (secret !== process.env.REVALIDATION_SECRET) {
    return res.status(401).json({ message: 'Invalid token' });
  }
  try {
    await res.revalidate(`/products/${req.query.id}`);
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).send('Error revalidating');
  }
}

적합한 사용 사례: 상품 페이지, 뉴스 기사, 가격이 자주 바뀌지 않는 데이터

SSR (Server-Side Rendering)

개념

요청이 올 때마다 서버에서 HTML을 새로 생성합니다. 항상 최신 데이터를 보여주지만, 서버에서 데이터를 조회하고 HTML을 생성하는 시간이 필요하여 TTFB(Time to First Byte)가 느릴 수 있습니다.

Pages Router에서 SSR

// pages/dashboard.jsx

export async function getServerSideProps(context) {
  const { req, res, params, query } = context;

  // 요청마다 실행 — 항상 최신 데이터
  const session = await getSession(req);
  if (!session) {
    return { redirect: { destination: '/login', permanent: false } };
  }

  const data = await fetch(`https://api.example.com/dashboard`, {
    headers: { Authorization: `Bearer ${session.token}` }
  }).then(r => r.json());

  return { props: { data, user: session.user } };
}

export default function Dashboard({ data, user }) {
  return <div><h1>{user.name}의 대시보드</h1>...</div>;
}

App Router에서 SSR

// app/dashboard/page.jsx

// cache: 'no-store'로 캐싱을 비활성화하여 SSR 동작
export default async function DashboardPage() {
  const data = await fetch('https://api.example.com/dashboard', {
    cache: 'no-store', // 매 요청마다 새로 가져옴 (SSR)
  }).then(r => r.json());

  return <div>...</div>;
}

적합한 사용 사례: 사용자별 개인화 페이지, 실시간 주식 가격, 로그인 필요 페이지

CSR (Client-Side Rendering)

개념

서버는 빈 HTML과 JavaScript 번들만 전달하고, 브라우저에서 JavaScript가 실행되면서 화면을 그립니다. 초기 로딩이 느리고 SEO에 불리하지만, 이후 상호작용은 빠릅니다.

// 'use client'가 있는 클라이언트 컴포넌트에서 데이터 페칭
'use client';

import { useState, useEffect } from 'react';

export default function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/me')
      .then(r => r.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>로딩 중...</div>;
  return <div>{user.name}님 환영합니다.</div>;
}

SWR / React Query를 사용한 CSR (권장)

'use client';

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(r => r.json());

export default function UserProfile() {
  const { data, error, isLoading } = useSWR('/api/me', fetcher, {
    refreshInterval: 30000, // 30초마다 자동 갱신
  });

  if (isLoading) return <div>로딩 중...</div>;
  if (error) return <div>오류가 발생했습니다.</div>;
  return <div>{data.name}님 환영합니다.</div>;
}

적합한 사용 사례: 실시간으로 변하는 데이터(채팅, 알림), SEO 불필요한 관리자 페이지, 사용자 인터랙션이 많은 컴포넌트

App Router의 기본 동작 (Next.js 13+)

App Router에서는 컴포넌트가 기본적으로 서버 컴포넌트이며, fetch 옵션으로 렌더링 방식을 제어합니다.

// 빌드 시 1회 캐시 (SSG)
fetch(url, { cache: 'force-cache' });

// 매 요청마다 최신 데이터 (SSR)
fetch(url, { cache: 'no-store' });

// 60초마다 재검증 (ISR)
fetch(url, { next: { revalidate: 60 } });

렌더링 방식 선택 가이드

데이터가 모든 사용자에게 동일한가?
  └─ 예 → 데이터가 자주 변하는가?
            └─ 아니오 → SSG (블로그, 문서)
            └─ 예 (시간 기반) → ISR (상품, 뉴스)
            └─ 예 (실시간) → SSR (주식 가격)

사용자별 맞춤 데이터가 필요한가?
  └─ 예 → SEO가 중요한가?
            └─ 예 → SSR (개인화 페이지)
            └─ 아니오 → CSR (관리자 대시보드)