정적 데이터 생성 (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=별도 백엔드/실시간 서버로 분리해 두면 병행 실습 시 충돌을 줄일 수 있습니다.
빌드 시점 계산과 요청 시점 전달을 분리하면 빠른 응답과 오래된 데이터 위험을 함께 이해할 수 있다.
- buildRoute 발견
정적 params와 page 의존성을 수집
- buildData fetch
빌드 환경에서 필요한 데이터를 조회
- buildHTML 생성
route별 결과와 RSC payload를 저장
- requestCache 전달
서버 계산 없이 준비된 결과를 응답
- next build데이터 갱신
재빌드 전까지 같은 snapshot 유지
SSG란 무엇인가요?
정적 사이트 생성(SSG)은 웹 페이지의 모든 콘텐츠가 애플리케이션 빌드 타임(Build Time)에 미리 생성되는 방식입니다.
이렇게 생성된 HTML, CSS, JavaScript 파일은 CDN에 배포되어 사용자 요청이 들어왔을 때 서버를 거치지 않고 바로 전송됩니다.
SSG의 주요 특징 및 이점- 빠른 응답: 페이지 요청 시 미리 생성된 HTML 파일이 반환되므로, 요청 시점의 데이터 페칭이나 서버 렌더링 비용이 없습니다.
- 향상된 SEO: 검색 엔진 크롤러가 미리 생성된 HTML 콘텐츠를 쉽게 읽고 색인화할 수 있어 검색 엔진 최적화에 유리합니다.
- 비용 효율성: 서버 부하가 거의 없거나 낮기 때문에, 트래픽이 많아도 서버 비용을 절감할 수 있습니다.
- 안정성: 빌드 시 생성된 파일이므로 런타임 에러의 가능성이 적고, 정적 파일을 호스팅하는 CDN의 안정성에 따라 높은 가용성을 보장합니다.
- 변화가 적은 데이터: 블로그 게시물, 문서, 상품 목록(재고 변동이 적은), 포트폴리오 사이트 등 콘텐츠가 자주 업데이트되지 않는 경우.
- 모든 사용자에게 동일한 콘텐츠: 사용자별 맞춤형 콘텐츠가 필요하지 않고, 모든 사용자에게 동일한 페이지를 보여줄 때.
- 높은 SEO 요구사항: 검색 엔진 노출이 중요한 경우.
SSG는 "미리 만들 수 있는가"와 "얼마나 자주 바뀌는가"를 함께 판단해야 합니다.
아래 다이어그램은 빌드 시점, 배포 결과물, 사용자 요청의 관계를 한 장으로 정리합니다.
SSG는 빌드 시점에 데이터를 읽고 HTML을 생성한 뒤, 사용자 요청에는 이미 준비된 결과물을 전달합니다.
- 가장 잘 맞는 조건
콘텐츠가 같고 변경 빈도가 낮은 페이지
- 경로 수집
Build generateStaticParams 가 미리 만들 동적 경로 목록을 반환합니다.
- HTML 생성
Output 각 경로별 HTML과 필요한 정적 자산이 배포 결과물로 준비됩니다.
- 즉시 전달
Request 요청 시 서버 렌더링 없이 CDN이나 서버가 준비된 파일을 응답합니다.
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의 작동 방식
빌드 전에 만들 목록과 목록 밖 요청, 갱신 정책을 하나의 route 계약으로 묶는다.
- 고정 목록모두 사전 생성
알려진 slug를 params 배열로 반환
- 신규가 잦음요청 시 생성
dynamicParams 정책으로 목록 밖 URL 처리
- 대상 없음notFound
유효하지 않은 식별자를 명확히 종료
- 내용 변경revalidate
시간·tag·사건으로 오래된 결과 교체
- 호출 시점: 프로덕션 빌드에서는 대응하는 페이지나 레이아웃이 생성되기 전에 실행됩니다. 개발 서버에서는 해당 라우트로 이동할 때 호출됩니다.
- 재검증과의 관계: 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은 모두 정적 응답의 장점을 활용하지만, 데이터가 얼마나 자주 바뀌는지에 따라 선택 기준이 달라집니다.
빠른 응답만 보지 말고 목록 밖 요청과 데이터 변경 뒤 결과가 어떻게 바뀌는지 설계한다.
- BUILDKnown → Generated
알려진 slug의 HTML과 data를 생성
- HITGenerated → Served
같은 결과를 CDN에서 빠르게 공유
- MISSUnknown → Missing
fallback 생성 또는 notFound 정책 적용
- STALEServed → Refresh
revalidate 뒤 새 버전으로 교체
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를 다시 호출하지는 않습니다.
빌드가 경로 목록을 만들고 배포 산출물로 고정하면 요청은 서버 계산보다 CDN을 먼저 통과한다.
- 1데이터 목록
게시글 id·slug 중 미리 만들 대상을 수집
- 2params
동적 segment 이름과 맞는 객체 배열 반환
- 3HTML 생성
각 params 조합을 빌드에서 렌더
- 4배포
정적 파일과 data를 CDN에 배치
- 5요청 응답
런타임 계산 없이 가까운 캐시에서 전달
SSG는 빌드 시점에 페이지를 미리 생성해 응답 속도를 줄이는 렌더링 방식입니다.
generateStaticParams를 사용하여 빌드 시점에 동적 페이지를 미리 생성하고, 필요에 따라 ISR을 통해 데이터의 신선도를 유지함으로써 사용자에게 빠르고 효율적인 웹 경험을 제공할 수 있습니다.
정적 데이터 생성 (SSG)의 판단 흐름을 화면 결과, 서버 비용, 운영 신호 기준으로 다시 묶었습니다.
정적 생성은 요청 전에 HTML을 준비해 빠른 응답을 만든다. 경로 수, 데이터 변경 주기, 빌드 시간 증가를 함께 계산해야 한다.
- 경로 수를 센다
블로그 글이나 문서처럼 목록이 작거나 관리 가능한 경우 SSG가 잘 맞는다.
- 변경 주기 확인
격, 재고, 개인화처럼 자주 바뀌는 값은 다른 전략이 필요하다.
- 없는 경로를 닫는다
params에 없는 값은 notFound 또는 동적 생성 정책으로 처리한다.
- 콘텐츠 성격
페이지 데이터가 배포 사이에 바뀌어도 괜찮은가.
- 경로 수
정적 생성할 경로 수가 빌드 시간을 과하게 늘리지 않는가.
- 404
목록에 없는 params를 어떻게 처리할지 정했는가.
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
마지막으로 SSG와 generateStaticParams가 빌드 시점 페이지 생성 범위를 어떻게 결정하는지 확인합니다.
목록 조회부터 페이지 생성과 재생성까지 정적 경로의 전체 생명주기를 잇는다.
- Buildslug 목록
공개되고 자주 바뀌지 않는 경로를 선택
- Params경로 객체
동적 segment에 넣을 값을 반환
- Render정적 페이지
각 경로의 HTML을 배포 전에 생성
- Serve즉시 응답
요청마다 서버 렌더 없이 결과 공유
- ISR주기 재생성
변경 가능성이 있으면 수명 뒤 새 결과 준비