데이터 페칭 — SSR, SSG, ISR의 동작 원리
데이터 페칭 — SSR, SSG, ISR의 동작 원리
SSR, SSG, ISR이라는 약어가 자주 등장하는데, 정확히 뭐가 다르고 언제 뭘 써야 할까요? App Router에서는
getServerSideProps가 사라졌다는데, 그러면 데이터는 어디서 가져오는 걸까요?
Pages Router에서는 getServerSideProps, getStaticProps 같은 전용 함수가 있었습니다.
App Router에서는 이것들이 사라지고, 컴포넌트 안에서 직접 fetch를 호출하는 방식 으로 바뀌었습니다. 렌더링 전략은 fetch의 캐싱 옵션으로 결정됩니다.
세 가지 렌더링 전략
SSR (Server-Side Rendering) — 매 요청마다 서버에서 렌더링
export default async function Page() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store', // 캐싱하지 않음 → 매 요청마다 최신 데이터
});
const data = await res.json();
return <div>{data.title}</div>;
}
요청이 들어올 때마다 서버에서 데이터를 가져오고 HTML을 생성합니다. 항상 최신 데이터를 보여주지만, 요청마다 서버 작업이 발생하므로 응답이 느릴 수 있습니다.
SSG (Static Site Generation) — 빌드 시 한 번만 생성
export default async function Page() {
const res = await fetch('https://api.example.com/data');
// cache: 'force-cache'가 기본 (Next.js 14)
const data = await res.json();
return <div>{data.title}</div>;
}
** 빌드 시점에** 데이터를 가져와 HTML을 생성하고, 이후 요청에는 미리 만든 HTML을 그대로 전달합니다. CDN에 캐싱되므로 가장 빠르지만, 빌드 이후 데이터가 변경되어도 반영되지 않습니다.
ISR (Incremental Static Regeneration) — 정적 생성 + 주기적 갱신
export default async function Page() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }, // 60초마다 백그라운드에서 재생성
});
const data = await res.json();
return <div>{data.title}</div>;
}
SSG처럼 정적 HTML을 제공하되, ** 지정한 시간이 지나면 백그라운드에서 새로운 HTML을 생성 **합니다.
- 사용자 A가 접근 → 캐싱된 HTML 즉시 전달 (빠름)
- 60초 경과 후 사용자 B가 접근 → 기존 HTML 즉시 전달 + 백그라운드에서 새 HTML 생성
- 사용자 C가 접근 → 새로 생성된 HTML 전달
핵심은 ** 사용자는 항상 빠른 응답 **을 받고, 데이터 갱신은 백그라운드에서 일어난다는 것입니다.
비교표
| 기준 | SSG | ISR | SSR |
|---|---|---|---|
| 생성 시점 | 빌드 시 | 빌드 시 + 주기적 갱신 | 매 요청 |
| 응답 속도 | 가장 빠름 (CDN) | 빠름 (CDN) | 느림 (서버 연산) |
| 데이터 신선도 | 빌드 시점 고정 | revalidate 주기만큼 | 항상 최신 |
| fetch 옵션 | force-cache (기본) | next: { revalidate: N } | cache: 'no-store' |
| 적합한 경우 | 블로그, 문서 | 상품 목록, 뉴스 피드 | 실시간 대시보드, 장바구니 |
generateStaticParams — 동적 경로의 정적 생성
블로그처럼 URL이 동적이지만 내용은 빌드 시 확정되는 경우, ** 미리 어떤 경로들을 생성할지 알려줘야** 합니다.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug, // /blog/hello-world, /blog/nextjs-intro 등
}));
}
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
빌드 시 generateStaticParams가 반환한 모든 경로에 대해 HTML을 미리 생성합니다.
반환하지 않은 경로는 런타임에 처리됩니다 — dynamicParams 설정으로 이 동작을 제어할 수 있습니다.
// 목록에 없는 경로 접근 시 404 반환
export const dynamicParams = false;
라우트 세그먼트 설정으로 전체 제어
fetch 옵션 외에, 파일 레벨에서 렌더링 전략을 강제할 수도 있습니다.
// 이 페이지를 항상 동적(SSR)으로 처리
export const dynamic = 'force-dynamic';
// 이 페이지를 항상 정적(SSG)으로 처리
export const dynamic = 'force-static';
// ISR — 페이지 단위 revalidate
export const revalidate = 60;
이 설정은 해당 라우트 세그먼트의 ** 모든 fetch 요청 **에 영향을 줍니다.
데이터 워터폴 피하기
Server Component에서 흔히 발생하는 실수입니다.
// 워터폴 발생 — 순차 실행
export default async function Dashboard() {
const user = await getUser(); // 1초
const orders = await getOrders(); // 1초
const stats = await getStats(); // 1초
// 총 3초
return <div>...</div>;
}
세 요청이 서로 의존하지 않는다면 ** 병렬로 실행 **해야 합니다.
// 병렬 실행 — Promise.all
export default async function Dashboard() {
const [user, orders, stats] = await Promise.all([
getUser(),
getOrders(),
getStats(),
]);
// 총 1초 (가장 느린 요청 기준)
return <div>...</div>;
}
주의할 점
Next.js 15에서 fetch 기본 캐싱이 바뀌었다
Next.js 14에서는 fetch()의 기본 동작이 force-cache(캐싱)였습니다.
Next.js 15에서는 기본이 no-store(캐싱 안 함)로 변경되었습니다.
이 변화를 모르면, 14에서 15로 업그레이드할 때 ** 갑자기 모든 페이지가 SSR로 동작 **하여 성능이 떨어질 수 있습니다. 명시적으로 캐싱 옵션을 지정하는 습관이 필요합니다.
revalidate: 0은 SSR과 같다
// 이건 ISR이 아니라 SSR
const res = await fetch(url, { next: { revalidate: 0 } });
revalidate: 0은 "0초마다 갱신" = 매 요청마다 새로 만드는 것이므로 SSR과 동일합니다.
SSR을 원한다면 cache: 'no-store'를 명시적으로 쓰는 것이 의도가 더 명확합니다.
정리
| 전략 | 언제 사용 | fetch 옵션 |
|---|---|---|
| SSG | 빌드 시 확정, 변경 거의 없음 | force-cache |
| ISR | 정적이지만 주기적 갱신 필요 | next: { revalidate: N } |
| SSR | 항상 최신 데이터 필요 | cache: 'no-store' |
기억할 한 줄: "fetch의 캐싱 옵션이 곧 렌더링 전략이다."