Next.js Incremental Static Regeneration (ISR) 가이드 정리 (App Router)
1. ISR 개요
Incremental Static Regeneration (ISR) 은 “정적 페이지(SSG)의 장점”을 유지하면서, 전체 재빌드 없이 런타임에 정적 페이지를 갱신할 수 있게 해주는 기능입니다.
ISR로 가능한 것:
- 전체 사이트를 다시 빌드하지 않고도 정적 콘텐츠 업데이트
- 대부분 요청은 캐시된 정적 HTML을 서빙해서 서버 부하 감소
- 페이지에 맞는
cache-control헤더를 자동으로 설정 - 콘텐츠 페이지가 많아도
next build시간이 과도하게 늘지 않도록 대응
2. Step 1: 시간 기반(Time-based) ISR 설정하기
App Router에서 시간 기반 ISR은 라우트 파일에 아래처럼 revalidate 값을 export 해서 설정합니다.
// app/blog/[id]/page.tsx (예시)
export const revalidate = 60
동작 개념은 다음과 같습니다.
next build시점에 가능한 페이지들을 미리 생성- 해당 페이지 요청은 캐시되어 빠르게 응답
revalidate시간이 지난 뒤 다음 요청이 오면,- 우선 기존(stale) 캐시를 즉시 응답하고
- 동시에 백그라운드에서 새 페이지를 생성
- 새 페이지 생성이 성공하면, 이후 요청부터 최신 캐시를 서빙
3. Step 2: generateStaticParams + on-demand 생성
동적 라우트에서 빌드 시점에 “알려진 params”를 미리 생성하려면 generateStaticParams()를 사용합니다.
interface Post {
id: string
title: string
content: string
}
export const revalidate = 60
export async function generateStaticParams() {
const posts: Post[] = await fetch('https://api.vercel.app/blog').then((res) => res.json())
return posts.map((post) => ({ id: String(post.id) }))
}
추가로, “빌드 시점에 몰랐던 id”를 요청 시 생성할지(= on-demand generation) 여부는 dynamicParams 설정으로 바꿀 수 있습니다.
4. Step 3: On-demand ISR – revalidatePath
정확히 “어느 순간에” 갱신할지 제어하고 싶다면,
데이터 변경이 일어나는 지점(예: 글 생성/수정 Server Action)에서 revalidatePath() 를 호출합니다.
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
// ... DB에 글 생성 등
revalidatePath('/posts') // /posts 경로 캐시 무효화
}
주의:
revalidatePath는 캐시 엔트리를 무효화하지만, 재생성은 “다음 요청” 때 일어납니다.- App Router에서 “즉시 재생성(eager regeneration)”은 별도 메서드가 필요하며, 문서에서 Pages Router의
res.revalidate를 참고로 언급합니다.
5. Step 4: On-demand ISR – revalidateTag (더 세밀하게)
대부분은 “경로 단위” 재검증이 권장되지만, 더 세밀한 컨트롤이 필요하면 tag 기반으로 관리할 수 있습니다.
5-1) fetch에 tag 달기
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog', {
next: { tags: ['posts'] },
})
const posts = await data.json()
// ...
}
5-2) DB/ORM이면 unstable_cache + tags
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
const getCachedPosts = unstable_cache(
async () => {
return await db.select().from(posts)
},
['posts'],
{ revalidate: 3600, tags: ['posts'] }
)
export default async function Page() {
const posts = getCachedPosts()
// ...
}
5-3) tag 무효화하기
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
// ... DB 업데이트 등
revalidateTag('posts')
}
6. 예외/오류가 발생하면?
ISR 갱신 중 에러가 나면:
- 마지막으로 성공한 캐시 버전이 계속 제공됩니다.
- 다음 요청에서 Next.js가 재시도합니다.
7. 캐시 저장 위치 커스터마이즈
여러 컨테이너/인스턴스에서 캐시를 공유하거나, 캐시를 내구성 있는 스토리지에 저장하고 싶다면 Next.js의 캐시 위치를 구성할 수 있습니다(문서에서 “cache location” 설정을 안내).
또한 문서의 버전 히스토리에 따르면 v14.1.0에서 Custom cacheHandler 가 stable로 표시됩니다.
8. Troubleshooting (현상 확인/디버깅)
8-1) 로컬에서 “프로덕션처럼” 테스트
ISR은 dev 서버보다 프로덕션 모드(next build + next start) 에서 동작 확인이 더 정확합니다.
8-2) 캐시 히트/미스 로그 보기
.env에 아래를 추가하면 서버 콘솔에 ISR 캐시 로그가 출력됩니다.
NEXT_PRIVATE_DEBUG_CACHE=1
또는 next.config.js의 fetch logging 옵션을 활용해, 어떤 fetch가 캐시되는지 디버깅할 수 있습니다.
// next.config.js
module.exports = {
logging: {
fetches: {
fullUrl: true,
},
},
}
9. Caveats (주의사항)
- ISR은 기본 런타임인 Node.js runtime에서만 지원됩니다.
- Static export에서는 ISR이 지원되지 않습니다.
- 하나의 라우트에서 여러
fetch가 있고 revalidate 시간이 다르면,- ISR 기준으로는 가장 낮은 시간이 적용될 수 있습니다.
fetch에revalidate: 0또는no-store가 섞이면 해당 라우트는 동적 렌더링으로 전환될 수 있습니다.- On-demand ISR 요청에서는 Proxy가 실행되지 않을 수 있으므로, 리라이트된 경로가 아니라 실제 경로를 정확히 revalidate 해야 합니다.
10. 정리
- ISR은 정적 페이지를 유지하면서도, 필요할 때 런타임 갱신을 가능하게 합니다.
- 시간 기반(
export const revalidate = N)과, 이벤트 기반(revalidatePath,revalidateTag) 두 축으로 이해하면 설계가 쉽습니다. - 운영 환경 검증은
next build+next start+ debug cache 로그로 확인하는 것이 좋습니다.