개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

Next.js Caching — 요청/데이터/라우트 캐싱 전략

2025년 6월 12일
Next.jsCachingFull Route CacheData Cacherevalidate

Next.js App Router는 성능 최적화를 위해 네 가지 독립적인 캐싱 레이어를 제공한다. 각 레이어는 서로 다른 범위와 생명주기를 가지며, 잘못 이해하면 오래된 데이터가 사용자에게 노출되는 문제가 생긴다. 레이어별 동작 원리와 무효화 방법을 명확히 파악해두면 예측 가능한 캐싱 전략을 세울 수 있다.

네 가지 캐싱 레이어

요청                서버                          클라이언트
 │                   │                               │
 ├─ Request          │                               │
 │  Memoization ─────┤ 동일 렌더 트리 내 중복 제거   │
 │                   │                               │
 ├─ Data Cache ──────┤ fetch() 결과, 빌드~만료까지   │
 │                   │                               │
 ├─ Full Route  ─────┤ 렌더링된 HTML/RSC Payload     │
 │  Cache            │                               │
 │                   │                               │
 └───────────────────┴──── Router Cache ────────────┤
                                클라이언트 인메모리    │
레이어저장 위치기본 지속 시간범위
Request Memoization서버 메모리단일 렌더 트리중복 fetch 제거
Data Cache서버 파일시스템영구 (명시 해제 전)fetch 응답
Full Route Cache서버 파일시스템빌드 시 생성정적 라우트 HTML
Router Cache브라우저 메모리세션 동안prefetch된 라우트

Request Memoization

Request Memoization은 동일 렌더 패스 내에서 같은 URL과 옵션으로 호출된 fetch를 자동으로 중복 제거한다. 여러 Server Component에서 같은 API를 호출해도 실제 네트워크 요청은 한 번만 발생한다.

// layout.tsx와 page.tsx에서 동일 fetch → 요청 1회만 발생
async function getUser(id: string) {
  const res = await fetch(`/api/users/${id}`)
  return res.json()
}

Request Memoization: React의 cache 함수를 기반으로 동작하며, 렌더 트리가 완료되면 캐시가 초기화된다.

Data Cache

fetch의 cache 옵션으로 Data Cache 동작을 제어한다. force-cache(기본값)는 빌드 이후에도 캐시를 유지하고, no-store는 매 요청마다 새로 가져온다.

// 영구 캐시 (기본)
fetch('/api/data', { cache: 'force-cache' })

// 캐시 없음
fetch('/api/data', { cache: 'no-store' })

// 시간 기반 재검증 (초 단위)
fetch('/api/data', { next: { revalidate: 60 } })

revalidate 값을 설정하면 stale-while-revalidate 방식으로 동작한다. 만료된 캐시를 즉시 반환하면서 백그라운드에서 새 데이터를 가져온다.

stale-while-revalidate: 오래된 데이터를 먼저 보여주고 동시에 갱신 요청을 보내는 HTTP 캐싱 패턴.

Full Route Cache

빌드 시 정적으로 렌더링된 라우트의 HTML과 RSC Payload를 서버에 캐싱한다. dynamic = 'force-dynamic' 또는 revalidate = 0으로 정적 캐싱을 비활성화할 수 있다.

// app/posts/page.tsx
export const revalidate = 3600 // 1시간마다 재검증

export const dynamic = 'force-dynamic' // 항상 동적 렌더링

캐시 무효화

On-demand Revalidation으로 특정 태그나 경로의 캐시를 즉시 무효화할 수 있다.

// app/actions.ts
import { revalidateTag, revalidatePath } from 'next/cache'

export async function updatePost(id: string) {
  await db.post.update({ where: { id }, data: { ... } })
  revalidateTag('posts')          // 태그 기반 무효화
  revalidatePath('/posts')        // 경로 기반 무효화
}

fetch에 태그를 붙여두면 revalidateTag로 관련 캐시를 일괄 제거할 수 있다.

fetch('/api/posts', { next: { tags: ['posts'] } })

Router Cache

클라이언트 사이드 네비게이션 시 prefetch된 라우트 페이로드를 브라우저 메모리에 보관한다. 정적 라우트는 5분, 동적 라우트는 30초 동안 유지된다. router.refresh를 호출하면 현재 라우트의 Router Cache를 즉시 비운다.

'use client'
import { useRouter } from 'next/navigation'

function RefreshButton() {
  const router = useRouter()
  return <button onClick={() => router.refresh()}>새로고침</button>
}

Data Cache 무효화(revalidateTag, revalidatePath)는 서버 캐시만 비우므로, 클라이언트에서도 최신 데이터를 즉시 보여야 한다면 router.refresh를 함께 호출해야 한다.

← 이전 글Next.js 성능 최적화 — 번들 크기와 렌더링 최적화