icon
6장 : 데이터 페칭

증분 정적 재생성 (ISR)


이전 절에서 우리는 정적 사이트 생성(SSG) 이 뛰어난 성능과 SEO 이점을 제공하지만, 빌드 시점에 콘텐츠가 고정된다는 한계가 있음을 확인했습니다. 또한, 서버 사이드 렌더링(SSR) 은 항상 최신 데이터를 제공하지만, 요청마다 서버에서 렌더링해야 하므로 SSG만큼 빠르지 않을 수 있습니다.

Next.js는 이 두 가지 렌더링 전략의 장점을 결합한 강력한 기능을 제공하는데, 바로 증분 정적 재생성(Incremental Static Regeneration, ISR) 입니다. ISR은 배포 후에 정적으로 생성된 페이지를 백그라운드에서 주기적으로 업데이트하여, SSG의 빠른 응답 속도를 유지하면서도 데이터의 신선도를 확보할 수 있게 해줍니다.


ISR이란 무엇인가요?

증분 정적 재생성(ISR) 은 Next.js 애플리케이션이 빌드된 후에도 정적 페이지를 "재생성"할 수 있도록 하는 기능입니다. 이는 fetch 함수의 next.revalidate 옵션을 사용하여 구현됩니다.

ISR의 작동 방식

초기 빌드: npm run build 시점에 generateStaticParams와 함께 정의된 페이지들은 SSG 방식으로 미리 생성됩니다.

첫 요청: 사용자가 ISR이 적용된 페이지에 처음 접속하면, 캐시된(미리 생성된) 페이지가 즉시 제공됩니다. 이때 Next.js는 revalidate 옵션으로 설정된 시간(예: 60초)을 확인합니다.

revalidate 시간 경과 후 요청: 설정된 revalidate 시간이 경과한 후, 다음 사용자 요청이 들어오면 Next.js는 다음과 같이 동작합니다.

  • 사용자에게는 즉시 오래된(stale) 캐시된 페이지를 제공합니다.
  • 동시에 백그라운드에서 새로운 데이터를 가져와 페이지를 재렌더링하고 캐시를 업데이트합니다.

다음 요청: 캐시가 업데이트된 후 들어오는 모든 후속 요청에는 새로 생성된 페이지가 제공됩니다.

ISR의 주요 이점

  • 성능과 신선도의 균형: SSG처럼 빠른 초기 로딩을 제공하면서도, 데이터 변경 시 수동으로 재빌드/재배포할 필요 없이 자동으로 최신 데이터를 반영할 수 있습니다.
  • 빌드 시간 단축: 모든 페이지를 빌드 시점에 생성할 필요 없이, 가장 중요한 페이지만 SSG하고 나머지는 ISR로 처리하여 빌드 시간을 단축할 수 있습니다.
  • 즉각적인 사용자 경험: 사용자는 항상 캐시된 콘텐츠를 즉시 받으므로 빈 화면을 보지 않습니다.
  • 확장성: 대규모 웹사이트에서 수많은 페이지를 관리할 때 효율적입니다.

App Router에서 ISR 구현하기

Next.js App Router에서 ISR을 구현하는 가장 기본적인 방법은 서버 컴포넌트 내의 fetch 함수에 next.revalidate 옵션을 추가하는 것입니다.

구현 단계

generateStaticParams를 사용하여 페이지를 SSG 방식으로 빌드할 경로를 지정합니다.

해당 page.tsx 파일 내에서 데이터를 페칭하는 fetch 호출에 next: { revalidate: N } 옵션을 추가합니다. 여기서 N은 초 단위로 캐시가 유효한 시간을 의미합니다.

실습: 주기적으로 업데이트되는 상품 정보 페이지

빌드 시점에 미리 생성되지만, 배포 후에도 주기적으로 가격이나 재고가 변하는 상품 상세 페이지를 ISR로 구현해 봅시다.

src/app/products/[productId]/page.tsx
// src/app/products/[productId]/page.tsx (새로 생성할 페이지)

import Link from 'next/link';

interface Product {
  id: string;
  name: string;
  price: number;
  lastUpdated: string; // 데이터 업데이트 시간을 확인하기 위함
}

// 1. 특정 상품 데이터를 가져오는 함수
async function getProduct(productId: string): Promise<Product> {
  console.log(`ISR 🚀: Fetching product ${productId} from API for revalidation`);

  // 실제 API 대신 더미 데이터와 지연 시간을 시뮬레이션합니다.
  // 실제 상황에서는 DB나 외부 API에서 데이터를 가져옵니다.
  await new Promise(resolve => setTimeout(resolve, 1500)); // 1.5초 지연

  const productsData = [
    { id: '1', name: '스마트워치 X', price: 299000 },
    { id: '2', name: '무선 이어폰 Pro', price: 199000 },
    { id: '3', name: '노트북 울트라', price: 1500000 },
  ];

  const product = productsData.find(p => p.id === productId);

  if (!product) {
    // 상품이 없을 경우 notFound() 함수를 사용하여 404 페이지를 렌더링할 수 있습니다.
    // import { notFound } from 'next/navigation';
    // notFound();
    throw new Error(`Product with ID ${productId} not found`);
  }

  return {
    ...product,
    price: product.price + Math.floor(Math.random() * 20000 - 10000), // 가격 변동 시뮬레이션
    lastUpdated: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
  };
}

// 2. generateStaticParams 함수: 빌드 시점에 어떤 상품 페이지들을 미리 생성할지 지정합니다.
export async function generateStaticParams() {
  console.log('generateStaticParams 🔥: Preparing product IDs for initial SSG');
  const productIds = [{ id: '1' }, { id: '2' }, { id: '3' }]; // 미리 생성할 상품 ID 목록
  return productIds;
}

// 3. 페이지 컴포넌트: params를 받아 데이터를 렌더링합니다.
export default async function ProductDetailPage({ params }: { params: { productId: string } }) {
  const product = await getProduct(params.productId); // 여기서 fetch에 revalidate 옵션이 없지만,
                                                      // Next.js는 generateStaticParams와 함께 사용될 때
                                                      // 자동으로 revalidate 옵션을 적용합니다.
                                                      // 만약 generateStaticParams가 없다면 SSR로 작동합니다.

  return (
    <div style={{ padding: '25px', maxWidth: '700px', margin: '20px auto', border: '2px solid #28a745', borderRadius: '12px', boxShadow: '0 6px 12px rgba(40,167,69,0.1)' }}>
      <h1 style={{ color: '#28a745', textAlign: 'center', marginBottom: '25px' }}>{product.name} (ISR)</h1>
      <div style={{ fontSize: '1.3em', lineHeight: '1.8' }}>
        <p><strong>상품 ID:</strong> {product.id}</p>
        <p><strong>가격:</strong> <span style={{ color: '#dc3545', fontWeight: 'bold' }}>{product.price.toLocaleString()}</span></p>
        <p><strong>마지막 업데이트:</strong> {product.lastUpdated}</p>
      </div>
      <p style={{ marginTop: '30px', textAlign: 'center', color: '#666', fontSize: '0.95em' }}>
        이 페이지는 빌드 시점에 미리 생성되었지만, <strong>10초</strong>마다 백그라운드에서 새로운 데이터로 업데이트됩니다. (페이지 새로고침 시 변경될 수 있습니다.)
      </p>
      <div style={{ marginTop: '30px', paddingTop: '15px', borderTop: '1px dashed #eee', textAlign: 'center' }}>
        <Link href="/">
          <a style={{ color: '#007bff', textDecoration: 'none', fontWeight: 'bold' }}>&larr; 홈으로 돌아가기</a>
        </Link>
      </div>
    </div>
  );
}

ISR 테스트 방법

ISR은 개발 모드(npm run dev)에서는 SSR처럼 동작하므로, 프로덕션 환경에서 테스트해야 그 효과를 명확히 볼 수 있습니다.

빌드 실행: 터미널에서 애플리케이션을 빌드합니다.

npm run build

이때 generateStaticParams가 실행되어 모든 상품 페이지가 미리 생성됩니다.

프로덕션 모드에서 실행: 빌드된 정적 파일들을 서빙하기 위해 다음 명령어를 실행합니다.

npm run start

브라우저 확인: http://localhost:3000/products/1과 같은 URL로 접속해 보세요.

  • 처음 접속: 페이지가 즉시 로드되고 현재 lastUpdated 시간이 표시됩니다. (이것은 빌드 시 생성된 초기 HTML입니다.)
  • 10초 이내 새로고침: 페이지를 새로고침해도 lastUpdated 시간과 가격은 변경되지 않습니다. 여전히 캐시된 페이지가 즉시 제공됩니다.
  • 10초 경과 후 새로고침: lastUpdated 시간이 10초 이상 지난 후에 다시 페이지를 새로고침하면, 이전 캐시된 페이지가 즉시 표시되지만, 동시에 백그라운드에서 새로운 데이터로 페이지를 재생성합니다.
  • 한 번 더 새로고침: 백그라운드 재생성이 완료된 후 한 번 더 새로고침하면, 이제 새로운 lastUpdated 시간과 변경된 가격이 표시되는 것을 볼 수 있습니다.

이 과정에서 Next.js 서버 콘솔에 ISR 🚀: Fetching product ... from API for revalidation과 같은 로그가 백그라운드에서 찍히는 것을 확인할 수 있습니다.


ISR의 고급 사용 사례 및 고려사항

  • revalidate를 0으로 설정: next: { revalidate: 0 }은 캐시를 사용하지 않고 매 요청마다 SSR처럼 동작하도록 강제합니다. (이는 cache: 'no-store'와 유사하게 작동합니다.)
  • 태그 기반 재검증 (revalidateTag): Next.js 13.1부터는 fetchtags 옵션을 추가하고 revalidateTag 함수를 사용하여 특정 데이터가 업데이트되었을 때 수동으로 캐시를 무효화하고 재생성할 수 있습니다. 이는 웹훅(webhook) 등을 사용하여 CMS에서 콘텐츠가 변경되었을 때 즉시 페이지를 업데이트하는 데 매우 유용합니다. (이 부분은 고급 주제이므로 추후 자세히 다룰 수 있습니다.)
  • 경로 기반 재검증 (revalidatePath): 특정 경로에 대한 캐시를 수동으로 무효화하고 재생성할 수도 있습니다.
  • 오프라인 페이지: ISR이 적용된 페이지는 처음 한 번 생성되면 오프라인 상태에서도 접근 가능합니다 (단, 초기 로딩된 후).
  • 에러 처리: ISR 과정에서 데이터 페칭에 실패하면, Next.js는 이전 캐시된 버전을 계속 제공합니다. 오류가 해결된 후 다음 재검증 주기에서 다시 시도합니다.

ISR은 Next.js가 제공하는 가장 강력한 데이터 페칭 전략 중 하나로, 빌드 시간과 데이터 신선도 사이의 트레이드오프를 현명하게 해결해 줍니다. 이를 통해 빠르고 유연하며 항상 최신 상태를 유지하는 웹 애플리케이션을 구축할 수 있습니다.