정적 데이터 생성 (SSG)
빌드 시 HTML을 만드는 SSG를 구현하고 generateStaticParams와 ISR을 결합해 정적 상세 경로를 생성합니다.
Next.js는 높은 성능을 위해 다양한 렌더링 전략을 제공합니다.
그중 대표가 정적 사이트 생성(Static Site Generation, SSG)입니다.
SSG는 빌드 시점에 페이지를 HTML로 미리 만들어 두고, 요청 시 서버 렌더링 없이 즉시 전달하는 방식입니다.
이 접근은 로딩 속도를 크게 개선하고, CDN을 통해 전 세계 사용자에게 빠르게 콘텐츠를 제공할 수 있게 해줍니다.
이 절에서는 Next.js App Router에서 SSG를 구현하는 방법과 그 이점, 그리고 어떤 상황에서 SSG를 선택해야 하는지 자세히 알아보겠습니다.
실습 코드는 외부 API 장애 영향을 줄이기 위해 로컬 Mock 서버(json-server, http://localhost:4000) 기준으로 설명합니다.
포트 정책은 4000=Mock API, 4100=별도 백엔드/실시간 서버로 분리해 두면 병행 실습 시 충돌을 줄일 수 있습니다.
SSG란 무엇인가요?
정적 사이트 생성(SSG)은 웹 페이지의 모든 콘텐츠가 애플리케이션 빌드 타임(Build Time)에 미리 생성되는 방식입니다.
이렇게 생성된 HTML, CSS, JavaScript 파일은 CDN에 배포되어 사용자 요청이 들어왔을 때 서버를 거치지 않고 바로 전송됩니다.
SSG의 주요 특징 및 이점- 빠른 응답: 페이지 요청 시 미리 생성된 HTML 파일이 반환되므로, 요청 시점의 데이터 페칭이나 서버 렌더링 비용이 없습니다.
- 향상된 SEO: 검색 엔진 크롤러가 미리 생성된 HTML 콘텐츠를 쉽게 읽고 색인화할 수 있어 검색 엔진 최적화에 유리합니다.
- 비용 효율성: 서버 부하가 거의 없거나 낮기 때문에, 트래픽이 많아도 서버 비용을 절감할 수 있습니다.
- 안정성: 빌드 시 생성된 파일이므로 런타임 에러의 가능성이 적고, 정적 파일을 호스팅하는 CDN의 안정성에 따라 높은 가용성을 보장합니다.
- 변화가 적은 데이터: 블로그 게시물, 문서, 상품 목록(재고 변동이 적은), 포트폴리오 사이트 등 콘텐츠가 자주 업데이트되지 않는 경우.
- 모든 사용자에게 동일한 콘텐츠: 사용자별 맞춤형 콘텐츠가 필요하지 않고, 모든 사용자에게 동일한 페이지를 보여줄 때.
- 높은 SEO 요구사항: 검색 엔진 노출이 중요한 경우.
SSG는 "미리 만들 수 있는가"와 "얼마나 자주 바뀌는가"를 함께 판단해야 합니다.
아래 다이어그램은 빌드 시점, 배포 결과물, 사용자 요청의 관계를 한 장으로 정리합니다.
App Router에서 SSG 구현하기
Next.js App Router에서 SSG를 구현하는 핵심은 동적 라우트([slug], [id] 등)와 함께 generateStaticParams 함수를 사용하는 것입니다.
generateStaticParams 함수는 프로덕션 빌드에서 페이지가 생성되기 전에 실행되며, 해당 동적 라우트에서 어떤 파라미터 값들을 미리 생성할지 Next.js에 알려줍니다.
개발 서버에서는 해당 라우트로 이동할 때 호출되므로, 프로덕션 빌드에서만 실행되는 함수로 오해하면 안 됩니다.
기본적인 구현 단계동적 라우트 폴더 생성: SSG할 페이지에 해당하는 동적 라우트 폴더(예: blog/[slug])를 만듭니다.
page.tsx 파일 작성: 해당 폴더 안에 page.tsx 파일을 작성하고, params prop을 통해 동적인 값을 받아 데이터를 페칭하고 UI를 렌더링합니다.
generateStaticParams 함수 작성: page.tsx (또는 layout.tsx) 파일 내부에 generateStaticParams라는 async 함수를 정의합니다.
이 함수는 미리 생성할 경로의 파라미터 객체 배열을 반환해야 합니다.
이전 5장 1절에서 만들었던 src/app/posts/[id]/page.tsx 예제를 사용하여, 이 게시물 상세 페이지들을 빌드 시점에 미리 생성하도록 설정해 보겠습니다.
import Link from 'next/link';
interface PostDetailPageProps {
params: Promise<{
id: string; // URL에서 추출될 게시물 ID
}>;
}
interface Post {
id: number;
title: string;
body: string;
}
// 1. 특정 게시물 데이터를 가져오는 함수 (서버 컴포넌트 내부에서 사용)
async function getPost(id: string): Promise<Post> {
const res = await fetch(`http://localhost:4000/posts/${id}`);
if (!res.ok) {
// 실제 서비스에서는 적절한 에러 처리 또는 notFound() 호출
throw new Error(`Failed to fetch post with ID: ${id}`);
}
return res.json();
}
// 2. generateStaticParams 함수 정의: 미리 생성할 경로들을 결정합니다.
// 이 함수는 'app' 폴더 내의 동적 라우트 페이지/레이아웃에서만 사용할 수 있습니다.
export async function generateStaticParams() {
// 프로덕션 빌드 전 또는 개발 중 이 라우트로 이동할 때 호출됩니다.
console.log('generateStaticParams 🚀: fetching all posts IDs for SSG');
const res = await fetch('http://localhost:4000/posts?_limit=20');
const payload = await res.json();
const posts: Post[] = payload;
// 모든 게시물의 ID를 추출하여 { id: string } 형태의 객체 배열로 반환합니다.
// Next.js는 이 배열의 각 객체에 대해 해당 페이지를 미리 생성합니다.
return posts.map((post) => ({
id: post.id.toString(), // params의 값은 항상 문자열이어야 합니다.
}));
}
// 3. 페이지 컴포넌트: params를 받아 데이터를 렌더링합니다.
export default async function PostDetailPage({ params }: PostDetailPageProps) {
const { id } = await params;
const post = await getPost(id); // getStaticProps의 context.params와 유사하게 작동
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto', border: '1px solid #eee', borderRadius: '8px', boxShadow: '0 2px 5px rgba(0,0,0,0.05)' }}>
<h1 style={{ color: '#333', marginBottom: '15px' }}>{post.title}</h1>
<p style={{ lineHeight: '1.6', color: '#555' }}>{post.body}</p>
<div style={{ marginTop: '30px', paddingTop: '15px', borderTop: '1px dashed #eee' }}>
<Link href="/posts" style={{ color: '#0070f3', textDecoration: 'none' }}>
← 목록으로 돌아가기
</Link>
</div>
</div>
);
}빌드 실행: 터미널에서 다음 명령어를 실행하여 Next.js 애플리케이션을 빌드합니다.
npm run build
# 또는
yarn build빌드 과정에서 generateStaticParams 함수가 실행되어 console.log 메시지가 터미널에 출력되는 것을 볼 수 있습니다.
일반 next build의 프리렌더 결과와 캐시 메타데이터는 .next 빌드 산출물에 포함됩니다. output: 'export'로 정적 내보내기를 선택한 경우에만 독립 정적 파일이 기본 out 폴더에 생성됩니다.
프로덕션 모드에서 실행: 빌드된 정적 파일들을 서빙하기 위해 다음 명령어를 실행합니다.
npm run start
# 또는
yarn start브라우저 확인: http://localhost:3000/posts/1 또는 http://localhost:3000/posts/10과 같은 URL로 접속해 보세요.
로컬에서는 next start 서버가 빌드 때 준비한 프리렌더 결과를 응답하므로 요청마다 페이지 전체를 새로 렌더링하지 않습니다.
CDN을 제공하는 플랫폼에 배포하면 이 정적 응답을 엣지 캐시에서 전달할 수도 있지만, localhost 실행 자체에는 CDN이 개입하지 않습니다.
generateStaticParams의 작동 방식
- 호출 시점: 프로덕션 빌드에서는 대응하는 페이지나 레이아웃이 생성되기 전에 실행됩니다. 개발 서버에서는 해당 라우트로 이동할 때 호출됩니다.
- 재검증과의 관계: ISR로 페이지를 다시 검증할 때
generateStaticParams가 다시 호출되지는 않습니다. 배포 뒤 새 경로를 허용할지는dynamicParams와 반환 목록을 함께 보고 결정해야 합니다. - 데이터 페칭:
generateStaticParams내부에서도fetch와 같은 비동기 데이터 페칭 함수를 사용할 수 있습니다. 이 결과는 미리 생성할 경로 목록을 정하는 데 사용됩니다. - 반환 값: 함수는
{ paramName: value }형태의 객체 배열을 반환해야 합니다.paramName은 동적 라우트 폴더의 대괄호 안 이름([id]의id)과 정확히 일치해야 하며,value는string타입이어야 합니다. - Catch-all 세그먼트 (
[...slug]): Catch-all 세그먼트의 경우,slug: ['path', 'to', 'document']와 같이 문자열 배열을 반환해야 합니다.export async function generateStaticParams() { return [{ slug: ['a', 'b'] }, { slug: ['c'] }]; } - 성능 최적화:
generateStaticParams에서 반환하는 경로의 수가 많을수록 빌드 시간이 길어집니다. 따라서 실제 필요한 페이지만 SSG하거나, 중요한 페이지 위주로 SSG하고 나머지는 CSR 또는 SSR로 처리하는 전략을 고려할 수 있습니다.
SSG와 ISR
generateStaticParams를 통해 SSG된 페이지는 기본적으로 빌드 시점에 고정됩니다.
하지만 fetch의 next.revalidate 옵션을 사용하면 캐시가 지정한 시간보다 오래된 뒤 들어온 요청을 계기로 백그라운드 재검증을 수행해 새 데이터를 반영할 수 있습니다.
이것이 바로 ISR(Incremental Static Regeneration)입니다.
SSG와 ISR은 모두 정적 응답의 장점을 활용하지만, 데이터가 얼마나 자주 바뀌는지에 따라 선택 기준이 달라집니다.
ISR은 SSG의 빠른 응답 속도와 서버 렌더링의 데이터 신선도 장점을 결합한 전략입니다.
// ... (다른 코드 생략) ...
async function getProduct(id: string): Promise<Product> {
const res = await fetch(`http://localhost:4000/products/${id}`, {
// 이 페이지는 SSG로 미리 생성되지만, 캐시가 60초 이상 지난 뒤
// 새 요청이 들어오면 백그라운드 재검증을 시작할 수 있습니다.
next: { revalidate: 60 },
});
if (!res.ok) { /* ... */ }
return res.json();
}
export async function generateStaticParams() {
// 빌드 시점에 생성할 모든 상품 ID를 반환
// ... (모든 상품 ID를 가져오는 로직) ...
return [{ id: 'product-1' }, { id: 'product-2' }];
}
export default async function ProductDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await getProduct(id);
// ... (상품 정보 렌더링) ...
}위 예시에서 generateStaticParams는 프로덕션 빌드 때 미리 생성할 경로를 정합니다.
fetch의 next: { revalidate: 60 }는 캐시가 60초 이상 지난 뒤 새 요청이 들어오면 재검증할 수 있게 하지만, 이 과정에서 generateStaticParams를 다시 호출하지는 않습니다.
SSG는 빌드 시점에 페이지를 미리 생성해 응답 속도를 줄이는 렌더링 방식입니다.
generateStaticParams를 사용하여 빌드 시점에 동적 페이지를 미리 생성하고, 필요에 따라 ISR을 통해 데이터의 신선도를 유지함으로써 사용자에게 빠르고 효율적인 웹 경험을 제공할 수 있습니다.
정적 데이터 생성 (SSG)의 판단 흐름을 화면 결과, 서버 비용, 운영 신호 기준으로 다시 묶었습니다.
마지막으로 SSG와 generateStaticParams가 빌드 시점 페이지 생성 범위를 어떻게 결정하는지 확인합니다.