icon안동민 개발노트

증분 정적 재생성 (ISR)


 증분 정적 재생성(Incremental Static Regeneration, ISR)은 Next.js의 강력한 기능 중 하나로, 정적 생성(SSG)의 이점을 유지하면서도 데이터를 주기적으로 업데이트할 수 있게 해줍니다.

 이 절에서는 ISR의 구현 방법, 장점, 그리고 적합한 사용 사례에 대해 알아보겠습니다.

ISR의 장점

  1. 성능 : 정적 페이지의 빠른 로딩 속도를 유지합니다.
  2. 최신성 : 설정한 주기에 따라 데이터를 자동으로 업데이트합니다.
  3. 확장성 : 트래픽이 많아도 서버 부하를 줄일 수 있습니다.
  4. 비용 효율성 : 모든 요청마다 서버에서 렌더링하는 것보다 효율적입니다.

revalidate 옵션 사용법

 ISR을 구현하려면 fetch 함수나 페이지 컴포넌트에 revalidate 옵션을 추가하면 됩니다.

// app/products/[id]/page.js
export const revalidate = 3600 // 1시간마다 재검증
 
async function getProduct(id) {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { revalidate: 3600 } // fetch 레벨에서도 설정 가능
  })
  if (!res.ok) throw new Error('Failed to fetch product')
  return res.json()
}
 
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id)
  return <ProductDetails product={product} />
}

 이 예제에서는 제품 페이지가 1시간마다 재생성됩니다.

On-demand Revalidation 구현

 특정 이벤트 발생 시 페이지를 즉시 재생성하고 싶다면 on-demand revalidation을 사용할 수 있습니다.

// pages/api/revalidate.js
export default async function handler(req, res) {
  if (req.query.secret !== process.env.REVALIDATION_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }
 
  try {
    await res.revalidate('/products/1')
    return res.json({ revalidated: true })
  } catch (err) {
    return res.status(500).send('Error revalidating')
  }
}

 이 API 라우트를 호출하면 /products/1 페이지가 즉시 재생성됩니다.

ISR이 적합한 사용 사례

  1. 뉴스 또는 블로그 사이트 : 컨텐츠가 자주 업데이트되지만, 실시간성이 절대적으로 필요하지는 않은 경우
  2. E-커머스 제품 페이지 : 가격이나 재고 정보가 주기적으로 변경되는 경우
  3. 데이터 대시보드 : 정기적으로 업데이트되는 통계나 보고서를 표시하는 경우

6.2절 정적 데이터 생성(SSG)과의 연관성

 ISR은 SSG의 확장된 형태로 볼 수 있습니다. SSG가 빌드 시에만 페이지를 생성한다면, ISR은 초기에 정적으로 생성된 페이지를 주기적으로 또는 요청에 따라 재생성합니다. 이를 통해 SSG의 성능상 이점을 유지하면서도 데이터의 최신성을 보장할 수 있습니다.

실습 : 주기적으로 업데이트되는 제품 카탈로그 페이지 구현

 다음 요구사항을 만족하는 제품 카탈로그 페이지를 ISR로 구현해보세요.

  1. 제품 목록 페이지 (/products)
  2. 개별 제품 상세 페이지 (/products/[id])
  3. 제품 목록은 1시간마다 재생성
  4. 개별 제품 페이지는 30분마다 재생성
  5. 관리자가 제품을 업데이트할 때 해당 제품 페이지를 즉시 재생성

 구현 예시

// app/products/page.js
export const revalidate = 3600 // 1시간마다 재검증
 
async function getProducts() {
  const res = await fetch('https://api.example.com/products')
  if (!res.ok) throw new Error('Failed to fetch products')
  return res.json()
}
 
export default async function ProductList() {
  const products = await getProducts()
  return (
    <div>
      <h1>Our Products</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <Link href={`/products/${product.id}`}>{product.name}</Link>
          </li>
        ))}
      </ul>
    </div>
  )
}
 
// app/products/[id]/page.js
export const revalidate = 1800 // 30분마다 재검증
 
async function getProduct(id) {
  const res = await fetch(`https://api.example.com/products/${id}`)
  if (!res.ok) throw new Error('Failed to fetch product')
  return res.json()
}
 
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id)
  return (
    <div>
      <h1>{product.name}</h1>
      <p>Price: ${product.price}</p>
      <p>Description: {product.description}</p>
    </div>
  )
}
 
// app/api/revalidate/route.js
import { revalidatePath } from 'next/cache'
 
export async function POST(request) {
  const { productId, secret } = await request.json()
 
  if (secret !== process.env.REVALIDATION_TOKEN) {
    return Response.json({ message: 'Invalid token' }, { status: 401 })
  }
 
  try {
    revalidatePath(`/products/${productId}`)
    return Response.json({ revalidated: true, now: Date.now() })
  } catch (err) {
    return Response.json({ message: 'Error revalidating' }, { status: 500 })
  }
}

 이 실습을 통해 ISR을 사용하여 주기적으로 업데이트되는 제품 카탈로그를 구현하고, on-demand revalidation을 통해 즉시 페이지를 재생성하는 방법을 경험할 수 있습니다.

 이는 실제 e-커머스 플랫폼에서 유용하게 사용될 수 있는 패턴입니다.