performance2025-03-30·9 min·134/348

Vercel에서 ISR revalidation 설정

Next.js ISR(Incremental Static Regeneration)을 Vercel에서 설정하고 최적화하는 방법을 알아봅니다.

Vercel에서 ISR revalidation 설정

Introduction

ISR(Incremental Static Regeneration)은 Next.js의 강력한 기능으로, 정적 페이지를 동적으로 업데이트할 수 있습니다. Vercel 환경에서 ISR을 설정하고 최적화하는 방법을 알아보겠습니다.

Environment

  • Vercel 프로젝트 (Next.js 14+)
  • Node.js 18+
  • PostgreSQL 데이터베이스
  • Redis (캐싱용)

Problem

ISR 설정이 올바르지 않으면 페이지가 예상대로 업데이트되지 않습니다:

// 페이지가 업데이트되지 않는 문제
export async function getStaticProps() {
  const data = await fetchData();
  
  return {
    props: { data },
    revalidate: 60 // 60초마다 재검증
  };
}

// 하지만 실제로는 페이지가 업데이트되지 않음

Analysis

ISR의 동작 원리:

  1. 빌드 시 정적 페이지 생성: 첫 빌드 시 페이지가 생성됨
  2. 시간 기반 재검증: 설정된 시간 간격으로 재검증
  3. 요청 기반 재검증: on-demand 재검증 가능
  4. 스태리 컨텐츠 문제: 오래된 데이터가 표시될 수 있음

Solution

1. 기본 ISR 설정

// app/blog/[slug]/page.js
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json());
  
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`);
  const data = await post.json();
  
  return (
    

{data.title}

{data.content}

); } export const revalidate = 60; // 60초마다 재검증

2. On-Demand Revalidation

// app/api/revalidate/route.js
import { NextResponse } from 'next/server';
import { revalidatePath, revalidateTag } from 'next/cache';

export async function POST(request) {
  const { path, tag, secret } = await request.json();
  
  // 시크릿 키 확인
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
  }
  
  try {
    if (tag) {
      revalidateTag(tag);
      return NextResponse.json({ revalidated: true, type: 'tag', tag });
    }
    
    if (path) {
      revalidatePath(path);
      return NextResponse.json({ revalidated: true, type: 'path', path });
    }
    
    return NextResponse.json({ error: 'Path or tag required' }, { status: 400 });
  } catch (error) {
    return NextResponse.json({ error: 'Revalidation failed' }, { status: 500 });
  }
}

3. 태그 기반 재검증

// app/blog/page.js
import { Suspense } from 'react';

async function BlogList() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { tags: ['blog'] } // 태그 설정
  }).then(res => res.json());
  
  return (
    
  );
}

export default function BlogPage() {
  return (
    

Blog

Loading...
}>
); } export const revalidate = 3600; // 1시간마다 재검증

4. 외부 웹훅을 사용한 재검증

// lib/revalidation.js
export async function triggerRevalidation(path, tag) {
  const response = await fetch(`${process.env.VERCEL_URL}/api/revalidate`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      path,
      tag,
      secret: process.env.REVALIDATION_SECRET,
    }),
  });
  
  return response.json();
}

// CMS 웹훅 핸들러
// app/api/webhook/cms/route.js
import { NextResponse } from 'next/server';
import { triggerRevalidation } from '@/lib/revalidation';

export async function POST(request) {
  const { event, data } = await request.json();
  
  switch (event) {
    case 'post.created':
    case 'post.updated':
      await triggerRevalidation(`/blog/${data.slug}`, 'blog');
      break;
    case 'post.deleted':
      await triggerRevalidation('/blog', 'blog');
      break;
  }
  
  return NextResponse.json({ success: true });
}

5. 동적 ISR 설정

// app/products/[id]/page.js
export default async function ProductPage({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`);
  const data = await product.json();
  
  return (
    

{data.name}

{data.price}

); } // 상품별 다른 재검증 시간 설정 export async function generateStaticParams() { const products = await fetch('https://api.example.com/products').then(res => res.json()); return products.map((product) => ({ id: product.id.toString(), })); } export const revalidate = false; // 동적 렌더링 사용 // 또는 조건부 재검증 export async function getStaticProps({ params }) { const product = await fetch(`https://api.example.com/products/${params.id}`); const data = await product.json(); // 상품 유형에 따라 재검증 시간 결정 const revalidateTime = data.type === 'digital' ? 3600 : 86400; return { props: { product: data }, revalidate: revalidateTime, }; }

6. ISR 모니터링

// lib/isr-monitor.js
export async function checkISRStatus() {
  const response = await fetch(`${process.env.VERCEL_URL}/api/revalidation/status`);
  const status = await response.json();
  
  console.log('ISR Status:');
  console.log(`Last revalidation: ${status.lastRevalidation}`);
  console.log(`Next revalidation: ${status.nextRevalidation}`);
  console.log(`Revalidated paths: ${status.paths.length}`);
  
  return status;
}

// ISR 히스토리 추적
export function trackISRHistory(path, timestamp) {
  const history = JSON.parse(localStorage.getItem('isr-history') || '[]');
  history.push({ path, timestamp });
  localStorage.setItem('isr-history', JSON.stringify(history.slice(-100)));
}

7. 환경 변수 설정

# .env.local
REVALIDATION_SECRET=your-secret-key
VERCEL_URL=https://your-app.vercel.app

Lessons Learned

  1. 재검증 시간 선택: 콘텐츠 업데이트 빈도에 따라 적절한 재검증 시간을 설정해야 합니다.
  2. On-Demand Revalidation: 콘텐츠가 변경될 때 즉시 재검증하는 것이 효과적입니다.
  3. 태그 기반 관리: 태그를 사용하면 관련 페이지를 그룹으로 관리할 수 있습니다.
  4. 웹훅 통합: CMS나 외부 서비스와 웹훅을 통합하면 자동화가 가능합니다.
  5. 모니터링: ISR 동작을 모니터링하여 문제를 조기에 발견해야 합니다.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.

Previous
Node.js util.promisify 활용법
Next
Python Multiprocessing: CPU 코어 활용 완벽 가이드
Back to Blog