← 글 목록

notes

Next.js ISR (Incremental Static Regeneration)

Next.js ISR의 정적 재생성·revalidate 동작과 캐시 갱신 메모.

💡 ISR은 Incremental Static Regeneration의 약자로, 빌드 시점에 페이지를 렌더링한 후 설정한 시간마다 페이지를 새로 렌더링하는 방식입니다. SSG에 포함되는 개념이지만, 데이터 변경 시 재빌드 없이 주기적으로 업데이트할 수 있다는 점이 핵심입니다.

📑 목차

  • SSG vs ISR vs SSR 비교
  • ISR 동작 원리
  • Pages Router에서 ISR 사용하기
  • App Router에서 ISR 사용하기
  • On-Demand Revalidation
  • 플랫폼 지원 및 제한사항
  • 참고 문서

📊 SSG vs ISR vs SSR 비교

핵심 차이

  • SSG → 데이터 변경 시 전체 사이트 재빌드 필요
  • ISR → revalidate 시간(초)마다 백그라운드 자동 갱신, 또는 revalidatePath / revalidateTag로 수동 갱신
  • SSR → 항상 최신 데이터, 하지만 TTFB 증가 및 서버 부하 증가

SSG vs ISR 타임라인


⚙️ ISR 동작 원리

흐름도

위 다이어그램은 ISR의 요청 처리 흐름을 단순화한 것입니다. (다이어그램 레이블은 호환성을 위해 영문으로 표기)

단계별 설명

1. 최초 요청 - 페이지가 캐시에 없으면 서버에서 렌더링 후 캐시에 저장 - fallback: 'blocking' 사용 시 첫 요청에서 생성

2. revalidate 이내 요청 - 캐시된 HTML을 즉시 반환 (빠른 응답)

3. revalidate 경과 후 첫 요청 - 기존 캐시(Stale)를 그대로 반환 - 백그라운드에서 새 페이지 생성 시작

4. 재생성 완료 - 성공 시 캐시 갱신, 이후 요청은 새 페이지 제공 - 실패 시 기존 페이지 유지, 다음 요청에서 재시도


📄 Pages Router에서 ISR 사용하기

기본 예시

getStaticProps에 revalidate를 추가합니다.

// pages/blog/[id].tsx
import type { GetStaticPaths, GetStaticProps } from 'next'

interface Post {
  id: string
  title: string
  content: string
}

export const getStaticPaths = async () => {
  const posts = await fetch('https://api.example.com/posts').then((r) => r.json())
  const paths = posts.map((post: Post) => ({ params: { id: post.id } }))

  return { paths, fallback: 'blocking' }
}

export const getStaticProps: GetStaticProps<{ post: Post }> = async ({ params }) => {
  const post = await fetch(`https://api.example.com/posts/${params?.id}`).then(
    (r) => r.json()
  )

  return {
    props: { post },
    revalidate: 60, // 60초마다 최대 1회 백그라운드 갱신
  }
}

export default function BlogPost({ post }: { post: Post }) {
  return (
    <main>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </main>
  )
}

fallback 옵션


📱 App Router에서 ISR 사용하기

App Router에서는 fetch의 next.revalidate 또는 세그먼트의 revalidate export를 사용합니다.

1. fetch 옵션으로 시간 기반 갱신

// app/blog/page.tsx
export default async function BlogPage() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }, // 60초마다 갱신
  })
  const posts = await res.json()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

2. 세그먼트 config로 페이지 전체 갱신

// app/products/page.tsx
export const revalidate = 3600 // 1시간

export default async function ProductsPage() {
  const products = await getProducts()
  return <ProductList products={products} />
}

3. 태그 + 시간 기반 조합

const res = await fetch('https://api.example.com/products', {
  next: {
    revalidate: 3600,
    tags: ['products'],
  },
})

🔄 On-Demand Revalidation

revalidate 시간을 기다리지 않고, 특정 시점에 캐시를 무효화할 수 있습니다.

Pages Router: API Route + res.revalidate()

// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.query.secret !== process.env.REVALIDATION_SECRET) {
    return res.status(401).json({ message: 'Invalid token' })
  }

  try {
    await res.revalidate('/blog/1')
    return res.json({ revalidated: true })
  } catch (err) {
    return res.status(500).send('Error revalidating')
  }
}
CMS 웹훅 등에서 /api/revalidate?secret=xxx 호출로 해당 경로만 갱신할 수 있습니다.

App Router: revalidatePath / revalidateTag

// app/actions.ts
'use server'

import { revalidatePath, revalidateTag } from 'next/cache'

// 특정 경로 갱신
export async function revalidateBlogPost(path: string) {
  revalidatePath(path)
}

// 특정 태그가 붙은 fetch 모두 갱신
export async function revalidateProducts() {
  revalidateTag('products')
}
// Route Handler에서 사용
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  const body = await request.json()
  // CMS 웹훅 등에서 호출
  revalidatePath(`/blog/${body.slug}`)
  return Response.json({ ok: true })
}

⚠️ 플랫폼 지원 및 제한사항

지원 환경

주의사항

  • Static Export (output: 'export')에서는 ISR 미지원
  • Node.js 런타임에서만 동작 (Edge 런타임 제한)
  • On-Demand ISR 시 Proxy/rewrite가 적용되지 않으므로, 실제 경로를 사용해야 함 (예: /posts/1)

디버깅

# .env
NEXT_PRIVATE_DEBUG_CACHE=1

next build 후 next start로 실행하면 캐시 hit/miss 로그를 확인할 수 있습니다.


📚 참고 문서


📅 버전 히스토리