Next.js 13 이상에서는 App Router를 통해 다양한 렌더링 방식을 지원합니다. 이 문서에서는 SSG(Static Site Generation), SSR(Server-Side Rendering), ISR(Incremental Static Regeneration)에 대해 자세히 설명하고 예시 코드를 제공합니다.
빌드 시점에 모든 페이지를 미리 생성하여 정적 HTML 파일로 제공하는 방식입니다.
// app/blog/page.tsx
export default function BlogPage() {
return (
<div>
<h1>블로그 목록</h1>
<p>이 페이지는 빌드 시점에 생성됩니다.</p>
</div>
);
}
// 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>
);
}
요청 시점에 서버에서 페이지를 동적으로 생성하여 완성된 HTML을 클라이언트에 전달하는 방식입니다.
// 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>
);
}
// 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>
);
}
정적 페이지를 생성하되, 일정 시간이 지나면 백그라운드에서 페이지를 재생성하여 데이터를 업데이트하는 방식입니다.
// 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>
);
}
// 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>
);
}
// 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>
);
}
| 특징 | SSG | SSR | ISR |
|---|---|---|---|
| 빌드 시점 | ✅ 정적 생성 | ❌ 동적 생성 | ✅ 정적 생성 + 재검증 |
| 요청 시점 | ❌ 캐시된 페이지 | ✅ 동적 렌더링 | ✅ 캐시 우선, 필요시 재생성 |
| 데이터 최신성 | ❌ 빌드 시점 고정 | ✅ 실시간 | ⚠️ 설정된 간격 |
| 성능 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| SEO | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 개인화 | ❌ 불가능 | ✅ 가능 | ⚠️ 제한적 |
| 서버 부하 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
// 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',
});
Next.js 13+에서는 App Router를 통해 다양한 렌더링 방식을 유연하게 선택할 수 있습니다. 각 방식의 특징을 이해하고 프로젝트의 요구사항에 맞게 적절히 조합하여 사용하는 것이 중요합니다.