문서
App Router 사용하기
단계적 정적 재생성 (ISR)

증분 정적 재생성 (ISR)

예시

증분 정적 재생성 (ISR)을 사용하면 다음과 같은 작업을 할 수 있습니다:

  • 전체 사이트를 다시 빌드하지 않고 정적 콘텐츠 업데이트
  • 대부분의 요청에 대해 미리 렌더링된 정적 페이지를 제공하여 서버 부하 감소
  • 페이지에 적절한 cache-control 헤더가 자동으로 추가되도록 보장
  • next build 시간 없이 대량의 콘텐츠 페이지 처리

다음은 최소한의 예시입니다:

app/blog/[id]/page.tsx
interface Post {
  id: string;
  title: string;
  content: string;
}
 
// Next.js는 요청이 들어올 때 캐시를 무효화합니다,
// 최대 60초에 한 번씩만 수행됩니다.
export const revalidate = 60;
 
// 빌드 시 `generateStaticParams`의 매개변수만 미리 렌더링합니다.
// 생성되지 않은 경로에 대한 요청이 들어오면,
// Next.js는 온디맨드로 페이지를 서버 렌더링합니다.
export const dynamicParams = true; // 또는 false, 알 수 없는 경로에 대해 404 응답
 
export async function generateStaticParams() {
  let posts: Post[] = await fetch("https://api.vercel.app/blog").then((res) =>
    res.json()
  );
  return posts.map((post) => ({
    id: post.id,
  }));
}
 
export default async function Page({ params }: { params: { id: string } }) {
  let post = await fetch(`https://api.vercel.app/blog/${params.id}`).then(
    (res) => res.json()
  );
  return (
    <main>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </main>
  );
}
app/blog/[id]/page.jsx
// Next.js는 요청이 들어올 때 캐시를 무효화합니다,
// 최대 60초에 한 번씩만 수행됩니다.
export const revalidate = 60;
 
// 빌드 시 `generateStaticParams`의 매개변수만 미리 렌더링합니다.
// 생성되지 않은 경로에 대한 요청이 들어오면,
// Next.js는 온디맨드로 페이지를 서버 렌더링합니다.
export const dynamicParams = true; // 또는 false, 알 수 없는 경로에 대해 404 응답
 
export async function generateStaticParams() {
  let posts = await fetch("https://api.vercel.app/blog").then((res) =>
    res.json()
  );
  return posts.map((post) => ({
    id: post.id,
  }));
}
 
export default async function Page({ params }) {
  let post = await fetch(`https://api.vercel.app/blog/${params.id}`).then(
    (res) => res.json()
  );
  return (
    <main>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </main>
  );
}

이 예시의 작동 방식은 다음과 같습니다:

  1. next build 동안 알려진 모든 블로그 게시물이 생성됩니다 (이 예시에서는 25개)
  2. 이 페이지들에 대한 모든 요청 (예: /blog/1)은 캐시되고 즉시 제공됩니다
  3. 60초가 지난 후, 다음 요청은 여전히 캐시된 (오래된) 페이지를 보여줍니다
  4. 캐시가 무효화되고 새 버전의 페이지가 백그라운드에서 생성되기 시작합니다
  5. 성공적으로 생성되면 Next.js는 업데이트된 페이지를 표시하고 캐시합니다
  6. /blog/26이 요청되면 Next.js는 이 페이지를 온디맨드로 생성하고 캐시합니다

참조

라우트 세그먼트 구성

함수

예시

시간 기반 재검증

이 예시는 /blog에서 블로그 게시물 목록을 가져와 표시합니다. 1시간 후, 페이지에 대한 다음 방문 시 이 페이지의 캐시가 무효화됩니다. 그런 다음 백그라운드에서 최신 블로그 게시물로 새 버전의 페이지가 생성됩니다.

export const revalidate = 3600; // 1시간마다 무효화
 
export default async function Page() {
  let data = await fetch("https://api.vercel.app/blog");
  let posts = await data.json();
  return (
    <main>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </main>
  );
}

긴 재검증 시간을 설정하는 것이 좋습니다. 예를 들어, 1초 대신 1시간으로 설정하세요. 더 정밀한 제어가 필요한 경우 온디맨드 재검증을 고려하세요. 실시간 데이터가 필요한 경우 동적 렌더링으로 전환하는 것을 고려하세요.

revalidatePath를 사용한 온디맨드 재검증

더 정밀한 재검증 방법을 위해 revalidatePath 함수를 사용하여 페이지를 온디맨드로 무효화할 수 있습니다.

예를 들어, 이 서버 액션은 새 게시물을 추가한 후 호출됩니다. 서버 컴포넌트에서 데이터를 검색하는 방법에 관계없이 fetch를 사용하거나 데이터베이스에 연결하는 경우에도 이는 전체 라우트의 캐시를 지우고 서버 컴포넌트가 새로운 데이터를 가져올 수 있게 합니다.

"use server";
 
import { revalidatePath } from "next/cache";
 
export async function createPost() {
  // 캐시에서 /posts 라우트를 무효화
  revalidatePath("/posts");
}

데모 보기소스 코드 살펴보기.

revalidateTag를 사용한 온디맨드 재검증

대부분의 사용 사례에서는 전체 경로를 재검증하는 것이 좋습니다. 더 세밀한 제어가 필요한 경우 revalidateTag 함수를 사용할 수 있습니다. 예를 들어, 개별 fetch 호출에 태그를 지정할 수 있습니다:

export default async function Page() {
  let data = await fetch("https://api.vercel.app/blog", {
    next: { tags: ["posts"] },
  });
  let posts = await data.json();
  // ...
}

ORM을 사용하거나 데이터베이스에 연결하는 경우 unstable_cache를 사용할 수 있습니다:

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() {
  let posts = getCachedPosts();
  // ...
}

그런 다음 서버 액션 또는 라우트 핸들러에서 revalidateTag를 사용할 수 있습니다:

"use server";
 
import { revalidateTag } from "next/cache";
 
export async function createPost() {
  // 캐시에서 'posts' 태그가 지정된 모든 데이터 무효화
  revalidateTag("posts");
}

처리되지 않은 예외 처리

데이터를 재검증하려고 시도하는 동안 오류가 발생하면 마지막으로 성공적으로 생성된 데이터가 계속해서 캐시에서 제공됩니다. 다음 후속 요청에서 Next.js는 데이터 재검증을 재시도합니다. 오류 처리에 대해 자세히 알아보기.

캐시 위치 사용자 정의

페이지 캐싱 및 재검증 (증분 정적 재생성 사용)은 동일한 공유 캐시를 사용합니다. Vercel에 배포할 때 ISR 캐시는 자동으로 지속 가능한 스토리지에 저장됩니다.

자체 호스팅 시, ISR 캐시는 Next.js 서버의 파일 시스템 (디스크)에 저장됩니다. 이는 Pages와 App Router를 모두 사용하여 자체 호스팅할 때 자동으로 작동합니다.

캐시된 페이지와 데이터를 지속 가능한 스토리지에 저장하거나 Next.js 애플리케이션의 여러 컨테이너 또는 인스턴스 간에 캐시를 공유하려는 경우 Next.js 캐시 위치를 구성할 수 있습니다. 자세히 알아보기.

문제 해결

로컬 개발에서 캐시된 데이터 디버깅

fetch API를 사용하는 경우 추가 로깅을 추가하여 어떤 요청이 캐시되었거나 캐시되지 않았는지 이해할 수 있습니다. logging 옵션에 대해 자세히 알아보기.

next.config.js
module.exports = {
  logging: {
    fetches: {
      fullUrl: true,
    },
  },
};

올바른 프로덕션 동작 확인

프로덕션에서 페이지가 올바르게 캐시되고 재검증되는지 확인하려면 next build를 실행한 다음 next start를 실행하여 프로덕션 Next.js 서버를 실행하여 로컬에서 테스트할 수 있습니다.

이를 통해 프로덕션 환경에서 작동하는 것처럼 ISR 동작을 테스트할 수 있습니다. 추가 디버깅을 위해 .env 파일에 다음 환경 변수를 추가하세요:

.env
NEXT_PRIVATE_DEBUG_CACHE=1

이렇게 하면 Next.js 서버가 ISR 캐시 히트와 미스를 콘솔에 로그로 남깁니다. 출력을 검사하여 next build 중에 어떤 페이지가 생성되는지, 그리고 경로가 온디맨드로 액세스될 때 페이지가 어떻게 업데이트되는지 확인할 수 있습니다.

주의 사항

  • ISR은 Node.js 런타임 (기본값) 사용 시에만 지원됩니다.
  • ISR은 정적 내보내기를 생성할 때는 지원되지 않습니다.
  • 정적으로 렌더링된 라우트에 여러 fetch 요청이 있고 각각 다른 revalidate 빈도를 가진 경우, ISR에는 가장 낮은 시간이 사용됩니다. 그러나 이러한 재검증 빈도는 데이터 캐시에서 여전히 반영됩니다.
  • 라우트에 사용된 fetch 요청 중 하나라도 revalidate 시간이 0이거나 명시적인 no-store가 있는 경우, 해당 라우트는 동적으로 렌더링됩니다.
  • 온디맨드 ISR 요청에 대해서는 미들웨어가 실행되지 않습니다. 즉, 미들웨어의 경로 재작성이나 로직이 적용되지 않습니다. 정확한 경로를 재검증하고 있는지 확인하세요. 예를 들어, 재작성된 /post-1 대신 /post/1을 사용하세요.

버전 기록

버전변경 사항
v14.1.0커스텀 cacheHandler가 안정화됨.
v13.0.0App Router가 도입됨.
v12.2.0Pages Router: 온디맨드 ISR이 안정화됨
v12.0.0Pages Router: 봇 인식 ISR 폴백이 추가됨.
v9.5.0Pages Router: 안정화된 ISR 도입됨.