정적 사이트는 빠르지만 데이터가 오래된다는 단점이 있다. 서버 사이드 렌더링은 최신 데이터를 보장하지만 요청마다 렌더링 비용이 든다. ISR(Incremental Static Regeneration)은 이 두 가지 문제를 동시에 해결하는 Next.js의 핵심 기능이다.
ISR은 빌드 타임에 생성된 정적 페이지를 배포 후에도 백그라운드에서 주기적으로 재생성하는 전략이다. 사용자는 캐시된 HTML을 즉시 받고, 설정한 주기가 지나면 Next.js가 새 버전을 조용히 생성해 다음 요청부터 교체한다.
요청 흐름:
[사용자 요청]
│
▼
[CDN/캐시에 유효한 페이지 존재?]
│
Yes │ No
▼ ▼
[캐시 HTML 즉시 반환] [정적 HTML 생성 후 반환]
│
[revalidate 시간 초과?]
│
Yes │
▼
[백그라운드에서 페이지 재생성]
[다음 요청부터 새 HTML 제공]
Stale-While-Revalidate: 오래된 캐시를 즉시 반환하면서 백그라운드에서 갱신하는 HTTP 캐시 전략. ISR은 이 패턴을 페이지 단위로 구현한다.
App Router에서는 fetch 옵션이나 세그먼트 설정으로 revalidate 주기를 지정한다.
// app/news/page.tsx
export const revalidate = 60; // 60초마다 재생성
export default async function NewsPage() {
const res = await fetch('https://api.example.com/news');
const news = await res.json();
return <NewsList items={news} />;
}
fetch에 직접 옵션을 줄 수도 있다.
const res = await fetch('https://api.example.com/news', {
next: { revalidate: 60 },
});
revalidate: 페이지 또는 데이터를 재검증할 최소 간격(초). 이 시간이 지난 후 첫 번째 요청이 들어오면 재생성이 트리거된다.
주기 기반 갱신 외에, 특정 이벤트(CMS 발행, DB 업데이트 등)에 즉시 캐시를 무효화할 수 있다.
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(req: NextRequest) {
const { path, tag } = await req.json();
if (tag) revalidateTag(tag);
if (path) revalidatePath(path);
return Response.json({ revalidated: true });
}
CMS 웹훅이 이 엔드포인트를 호출하면 해당 경로나 태그를 가진 캐시가 즉시 무효화된다.
| 항목 | SSG | ISR |
|---|---|---|
| 빌드 시간 | 모든 페이지 빌드 | 요청 시 점진적 생성 |
| 데이터 신선도 | 배포 시점 고정 | 주기적 갱신 가능 |
| 응답 속도 | 매우 빠름 | 빠름 (캐시 히트 시) |
| 서버 비용 | 낮음 | 낮음 |
| 적합한 콘텐츠 | 문서, 마케팅 | 뉴스, 블로그, 상품 목록 |
뉴스 사이트는 revalidate: 300(5분)으로 설정해 최신 기사를 반영하면서도 CDN 캐시를 최대한 활용할 수 있다. 블로그는 글을 발행할 때 On-demand Revalidation으로 해당 포스트 경로만 즉시 갱신한다. 이커머스 상품 페이지는 재고·가격 변동이 생길 때마다 웹훅으로 캐시를 무효화해 사용자에게 정확한 정보를 보여준다.
ISR은 정적 페이지의 성능을 유지하면서 동적 데이터 요구를 충족하는 실용적인 중간 지점이다. revalidate 값을 콘텐츠 갱신 주기에 맞게 조정하고, 즉각적인 반영이 필요한 경우 On-demand Revalidation을 조합하면 된다.