Nextjs SSR, SSG, ISR

정지훈·2025년 7월 14일

Next.js 13+ 렌더링 방식 가이드

Next.js 13 이상에서는 App Router를 통해 다양한 렌더링 방식을 지원합니다. 이 문서에서는 SSG(Static Site Generation), SSR(Server-Side Rendering), ISR(Incremental Static Regeneration)에 대해 자세히 설명하고 예시 코드를 제공합니다.

목차

  1. SSG (Static Site Generation)
  2. SSR (Server-Side Rendering)
  3. ISR (Incremental Static Regeneration)
  4. 렌더링 방식 비교
  5. 사용 시나리오

SSG (Static Site Generation)

정의

빌드 시점에 모든 페이지를 미리 생성하여 정적 HTML 파일로 제공하는 방식입니다.

특징

  • 빠른 로딩 속도: 미리 생성된 HTML을 즉시 제공
  • SEO 최적화: 검색 엔진이 완전한 HTML을 크롤링 가능
  • CDN 캐싱: 정적 파일로 CDN에 캐싱 가능
  • 서버 부하 최소화: 서버에서 동적 렌더링 불필요
  • 동적 데이터 제한: 빌드 시점의 데이터만 반영
  • 개인화 제한: 사용자별 맞춤 콘텐츠 제공 어려움

기본 예시

// app/blog/page.tsx
export default function BlogPage() {
  return (
    <div>
      <h1>블로그 목록</h1>
      <p>이 페이지는 빌드 시점에 생성됩니다.</p>
    </div>
  );
}

동적 데이터가 있는 SSG 예시

// app/posts/[id]/page.tsx
import { notFound } from 'next/navigation';

// 빌드 시점에 생성할 경로들을 정의
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json());
  
  return posts.map((post: any) => ({
    id: post.id.toString(),
  }));
}

// 빌드 시점에 데이터를 가져와서 정적 페이지 생성
export async function generateMetadata({ params }: { params: { id: string } }) {
  const post = await fetch(`https://api.example.com/posts/${params.id}`).then(res => res.json());
  
  if (!post) {
    notFound();
  }
  
  return {
    title: post.title,
    description: post.excerpt,
  };
}

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await fetch(`https://api.example.com/posts/${params.id}`).then(res => res.json());
  
  if (!post) {
    notFound();
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <time>{post.publishedAt}</time>
    </article>
  );
}

SSR (Server-Side Rendering)

정의

요청 시점에 서버에서 페이지를 동적으로 생성하여 완성된 HTML을 클라이언트에 전달하는 방식입니다.

특징

  • 실시간 데이터: 요청 시점의 최신 데이터 반영
  • 개인화: 사용자별 맞춤 콘텐츠 제공 가능
  • SEO 지원: 완전한 HTML 제공으로 SEO 최적화
  • 서버 부하: 매 요청마다 서버에서 렌더링 필요
  • 느린 응답: 동적 렌더링으로 인한 지연
  • CDN 캐싱 제한: 동적 콘텐츠로 인한 캐싱 어려움

기본 예시

// app/dashboard/page.tsx
import { headers } from 'next/headers';

export default async function DashboardPage() {
  // 요청 시점에 사용자 정보 가져오기
  const headersList = headers();
  const userAgent = headersList.get('user-agent');
  
  // 실시간 데이터 가져오기
  const data = await fetch('https://api.example.com/dashboard-data', {
    cache: 'no-store', // 캐싱 비활성화
  }).then(res => res.json());
  
  return (
    <div>
      <h1>대시보드</h1>
      <p>User Agent: {userAgent}</p>
      <div>
        <h2>실시간 데이터</h2>
        <pre>{JSON.stringify(data, null, 2)}</pre>
      </div>
    </div>
  );
}

사용자 인증이 필요한 SSR 예시

// app/profile/page.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export default async function ProfilePage() {
  const cookieStore = cookies();
  const token = cookieStore.get('auth-token');
  
  if (!token) {
    redirect('/login');
  }
  
  // 토큰을 사용하여 사용자 정보 가져오기
  const user = await fetch('https://api.example.com/user/profile', {
    headers: {
      'Authorization': `Bearer ${token.value}`,
    },
    cache: 'no-store',
  }).then(res => res.json());
  
  return (
    <div>
      <h1>프로필</h1>
      <div>
        <p>이름: {user.name}</p>
        <p>이메일: {user.email}</p>
        <p>가입일: {user.createdAt}</p>
      </div>
    </div>
  );
}

ISR (Incremental Static Regeneration)

정의

정적 페이지를 생성하되, 일정 시간이 지나면 백그라운드에서 페이지를 재생성하여 데이터를 업데이트하는 방식입니다.

특징

  • 빠른 응답: 캐시된 페이지를 즉시 제공
  • 데이터 최신성: 주기적으로 데이터 업데이트
  • SEO 최적화: 정적 HTML 제공
  • 서버 부하 감소: 캐시된 페이지 우선 제공
  • 복잡한 설정: 캐시 시간과 재검증 로직 관리 필요
  • 즉시 업데이트 불가: 설정된 시간까지 기다려야 함

기본 예시

// app/products/page.tsx
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }, // 1시간마다 재검증
  }).then(res => res.json());
  
  return (
    <div>
      <h1>상품 목록</h1>
      <div className="grid">
        {products.map((product: any) => (
          <div key={product.id}>
            <h3>{product.name}</h3>
            <p>{product.price}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

동적 경로가 있는 ISR 예시

// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';

// 빌드 시점에 인기 상품들만 미리 생성
export async function generateStaticParams() {
  const popularProducts = await fetch('https://api.example.com/products/popular').then(res => res.json());
  
  return popularProducts.map((product: any) => ({
    id: product.id.toString(),
  }));
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 1800 }, // 30분마다 재검증
  }).then(res => res.json());
  
  if (!product) {
    notFound();
  }
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>가격: {product.price}</p>
      <p>재고: {product.stock}</p>
      <p>마지막 업데이트: {new Date().toLocaleString()}</p>
    </div>
  );
}

On-demand Revalidation 예시

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { path, tag } = await request.json();
  
  if (path) {
    revalidatePath(path);
  }
  
  if (tag) {
    revalidateTag(tag);
  }
  
  return NextResponse.json({ revalidated: true, now: Date.now() });
}
// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { 
      revalidate: 3600,
      tags: [`product-${params.id}`] // 태그 기반 재검증
    },
  }).then(res => res.json());
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

렌더링 방식 비교

특징SSGSSRISR
빌드 시점✅ 정적 생성❌ 동적 생성✅ 정적 생성 + 재검증
요청 시점❌ 캐시된 페이지✅ 동적 렌더링✅ 캐시 우선, 필요시 재생성
데이터 최신성❌ 빌드 시점 고정✅ 실시간⚠️ 설정된 간격
성능⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
SEO⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
개인화❌ 불가능✅ 가능⚠️ 제한적
서버 부하⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

사용 시나리오

SSG 사용 시기

  • 블로그, 문서 사이트
  • 마케팅 페이지
  • 제품 카탈로그 (자주 변경되지 않는)
  • 포트폴리오 사이트

SSR 사용 시기

  • 사용자 대시보드
  • 개인화된 콘텐츠
  • 실시간 데이터가 필요한 페이지
  • 사용자 인증이 필요한 페이지

ISR 사용 시기

  • 뉴스 사이트
  • 전자상거래 상품 페이지
  • 자주 업데이트되지만 즉시성은 필요 없는 콘텐츠
  • API 데이터를 사용하는 정적 페이지

캐싱 전략

Next.js 13+ 캐싱 옵션

// 1. 기본 캐싱 (SSG)
const data = await fetch('https://api.example.com/data');

// 2. 캐싱 비활성화 (SSR)
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store',
});

// 3. 시간 기반 재검증 (ISR)
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 }, // 1시간
});

// 4. 태그 기반 재검증
const data = await fetch('https://api.example.com/data', {
  next: { 
    revalidate: 3600,
    tags: ['products', 'category-1']
  },
});

// 5. 강제 캐싱
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache',
});

성능 최적화 팁

  1. 적절한 렌더링 방식 선택: 콘텐츠 특성에 맞는 방식 선택
  2. 캐싱 전략 수립: 데이터 변경 빈도에 따른 캐싱 설정
  3. 이미지 최적화: Next.js Image 컴포넌트 활용
  4. 코드 분할: 동적 import를 통한 번들 최적화
  5. 메타데이터 최적화: generateMetadata 함수 활용

결론

Next.js 13+에서는 App Router를 통해 다양한 렌더링 방식을 유연하게 선택할 수 있습니다. 각 방식의 특징을 이해하고 프로젝트의 요구사항에 맞게 적절히 조합하여 사용하는 것이 중요합니다.

  • SSG: 빠른 성능과 SEO가 중요한 정적 콘텐츠
  • SSR: 실시간 데이터와 개인화가 필요한 동적 콘텐츠
  • ISR: 정적 성능과 데이터 최신성의 균형이 필요한 콘텐츠

0개의 댓글