Next.js 가이드 정리: Caching in Next.js

1. 개요 (Overview)

Next.js는 렌더링 결과와 데이터 요청을 적극적으로 캐싱하여 성능을 높이고 비용을 줄입니다. 이 문서는 Next.js의 캐싱 메커니즘과 관련 API, 그리고 서로가 어떻게 상호작용하는지를 설명합니다.

Next.js의 주요 캐싱 메커니즘은 다음 네 가지입니다.

메커니즘무엇을 캐싱하는가위치목적지속 시간
Request Memoization함수/fetch 호출 결과서버동일 렌더링 패스 내에서 데이터 재사용요청 1회 렌더링 주기
Data Cache서버 데이터 (fetch 결과)서버여러 사용자·배포 간에 데이터 재사용지속적 (재검증 가능)
Full Route CacheHTML + RSC Payload(서버 렌더링 결과)서버렌더링 비용 절감, 빠른 응답지속적 (재검증 가능)
Router CacheRSC Payload(라우트 세그먼트 단위)클라이언트탐색 시 서버 요청 감소, 즉각적인 네비게이션브라우저 세션 또는 시간 기반

기본적으로 Next.js는 가능한 한 많이 캐싱합니다.

  • 정적 렌더링 가능한 라우트는 빌드 타임 또는 재검증 시점에 렌더링 후 Full Route Cache에 저장
  • fetch 데이터는 Data Cache에 저장되며, 필요 시 재검증
  • 클라이언트에서는 Router Cache로 이전에 방문한 라우트를 기억

단, proxy(전역 Proxy 파일) 안에서 실행되는 fetch는 캐싱되지 않습니다.


2. 렌더링 전략과 캐싱 (Rendering Strategies)

2.1 Static Rendering

  • 라우트를 빌드 타임 또는 백그라운드 재검증 시점에 렌더링
  • 결과(HTML + RSC Payload)가 Full Route Cache에 저장
  • 여러 사용자 요청에 대해 캐시된 결과를 재사용

2.2 Dynamic Rendering

  • 요청 시점에 라우트를 렌더링

  • 다음과 같은 API를 사용하면 라우트가 동적이 됩니다.

    • cookies
    • headers
    • connection
    • draftMode
    • searchParams (Page의 props)
    • unstable_noStore
    • fetch 옵션 { cache: 'no-store' }
  • 동적 라우트는 Full Route Cache에는 저장되지 않지만, 개별 fetch 요청은 여전히 Data Cache를 사용할 수 있습니다.

  • 하나의 라우트에서 정적·동적 렌더링을 섞고 싶다면 Cache Components 기능을 사용할 수 있습니다.


3. Request Memoization (요청 메모이제이션)

Next.js는 서버에서 실행되는 fetch 호출을 자동으로 중복 제거(memoization) 합니다.

async function getItem() {
  const res = await fetch('https://.../item/1')
  return res.json()
}

// 같은 요청이 여러 번 호출되더라도 실제로는 한 번만 실행
const a = await getItem() // MISS → 원본 호출
const b = await getItem() // HIT  → 메모이제이션된 결과

3.1 동작 방식

  • 한 번의 서버 렌더링 중:
    • 동일한 URL + 옵션으로 fetch가 최초 호출되면 MISS
      • 실제 데이터 소스에 요청
      • 결과를 메모리에 저장
    • 이후 같은 요청은 HIT
      • 메모리에서 바로 결과를 반환
  • 렌더링이 끝나면 메모이제이션 데이터는 초기화됩니다.

3.2 특징 및 제약

  • React 기능이며, Next.js에서 이를 활용
  • GET 메서드의 fetch에만 적용
  • React 컴포넌트 트리 안에서만 적용
    • generateMetadata, generateStaticParams, Layout, Page, Server Components 등
    • Route Handler에서는 적용되지 않음
  • DB 클라이언트나 GraphQL 클라이언트 등 fetch가 아닌 경우에는 React cache() 함수로 직접 메모이제이션할 수 있습니다.
  • 재검증 개념은 필요 없음 (렌더링 한 번에만 유효)

4. Data Cache (데이터 캐시)

Data Cache는 서버의 fetch 결과를 요청·배포 간에 재사용하는 캐시입니다.

  • 서버에서 fetch를 사용할 때, cache 옵션next.revalidate 옵션으로 캐싱 전략을 정의
  • 브라우저의 HTTP 캐시와 의미가 다름
    • 브라우저: cache 옵션은 HTTP 캐시와의 상호작용 정의
    • Next.js 서버: cache 옵션은 서버 Data Cache와의 상호작용 정의

4.1 기본 동작

  • cache: 'force-cache':
    • 최초 호출 시 Data Cache를 조회
      • 있으면 바로 반환 + 메모이제이션
      • 없으면 원본 데이터 소스에서 받아와 Data Cache에 저장 + 메모이제이션
  • cache 옵션 미지정 또는 { cache: 'no-store' }:
    • 매번 데이터 소스에서 직접 가져오고, Data Cache에는 저장하지 않음
    • 그래도 Request Memoization은 적용되어 동일 렌더링 패스 내 중복 호출은 제거

4.2 지속 시간 (Duration)

  • Data Cache는 서버 요청과 배포 간에 지속됩니다.
  • 다만, 재검증(시간 기반 / on-demand) 또는 명시적 opt-out에 의해 갱신/무효화됩니다.

4.3 재검증 (Revalidating)

재검증 방식은 두 가지입니다.

  1. 시간 기반 재검증 (Time-based Revalidation)
// 최대 1시간마다 한 번씩 재검증
fetch('https://...', { next: { revalidate: 3600 } })
  • 최초 호출 시 데이터 소스에서 가져와 캐시에 저장
  • 지정된 시간(예: 3600초) 내 반복 요청은 캐시된 데이터를 반환
  • 시간이 지난 뒤 첫 요청:
    • 여전히 기존(약간 오래된) 데이터를 먼저 반환
    • 백그라운드에서 새 데이터를 가져와 캐시 갱신
    • 실패하면 이전 데이터를 유지
  • HTTP 캐시의 stale-while-revalidate와 비슷한 동작
  1. On-demand 재검증 (경로/태그 기반)
  • 특정 이벤트(예: CMS에서 콘텐츠 갱신, 폼 제출) 발생 시 직접 캐시를 무효화
  • 두 가지 방식:
    • revalidatePath(path) : 특정 경로 아래의 데이터와 렌더링 결과를 한 번에 재검증
    • revalidateTag(tag) : 태그로 묶인 데이터 그룹을 모두 재검증

4.4 캐시 사용하지 않기 (Opting out)

  • 특정 요청에서 캐시를 사용하고 싶지 않다면:
await fetch('https://api.vercel.app/blog', { cache: 'no-store' })
  • 또는 Route Segment Config 옵션 fetchCache, dynamic 등을 사용할 수 있습니다.

5. Full Route Cache (전체 라우트 캐시)

Full Route Cache는 **라우트의 렌더링 결과(HTML + React Server Component Payload)**를 캐싱합니다.

5.1 서버에서의 렌더링

  1. 서버에서 React가 Server Component 트리를 렌더링해 RSC Payload를 생성
  2. Next.js가 RSC Payload와 클라이언트 컴포넌트 JS 정보를 활용해 HTML을 생성
  3. 이 과정은 스트리밍 기반이라, 일부가 준비되는 대로 응답을 보낼 수 있음

5.2 서버 캐싱 (Full Route Cache)

  • 정적으로 렌더링되는 라우트의 경우, 위 렌더링 결과를 서버에 캐싱합니다.
  • 이 캐시가 Full Route Cache입니다.
  • 재검증 시에도 새로운 렌더링 결과가 다시 Full Route Cache에 저장됩니다.

5.3 클라이언트 측에서의 사용 (Hydration + Router Cache)

요청 시 클라이언트에서는:

  1. HTML로 컴포넌트 트리의 초기 뷰를 즉시 보여줌
  2. RSC Payload를 사용해 클라이언트·서버 컴포넌트 트리를 동기화
  3. 클라이언트 컴포넌트 JS를 실행해 인터랙티브하게 만듦

이때, RSC Payload는 Router Cache에도 저장되어 이후 네비게이션에서 재사용됩니다.

5.4 정적 vs 동적 렌더링과 Full Route Cache

  • 정적 렌더링(Static Rendering):
    • 라우트는 빌드 타임 또는 재검증 시 렌더링
    • 결과가 Full Route Cache에 저장
  • 동적 렌더링(Dynamic Rendering):
    • 요청마다 렌더링
    • Full Route Cache에는 저장되지 않음
    • 대신 Data Cache는 그대로 사용할 수 있음

5.5 지속 시간과 무효화

  • Full Route Cache는 기본적으로 지속적입니다.
  • 무효화 방법:
    • Data Cache 재검증 (revalidatePath, revalidateTag 등)
      • 데이터가 바뀌면, 그 데이터를 사용하는 라우트의 렌더링 결과도 다시 생성
    • 새 배포
      • 새 배포 시 Full Route Cache는 초기화되고 다시 채워짐

5.6 Full Route Cache 사용하지 않기

  • 라우트를 항상 동적으로 렌더링하려면:

    • 동적 API 사용하기
      • cookies, headers, searchParams, draftMode, fetch with no-store
    • Route Segment Config에서
      • export const dynamic = 'force-dynamic'
      • 또는 export const revalidate = 0
  • fetch 요청 중 하나라도 no-store이면, 해당 라우트는 Full Route Cache에서 빠져나가고 매 요청마다 그 데이터만 새로 가져올 수 있습니다.


6. Client-side Router Cache (클라이언트 라우터 캐시)

Router Cache는 클라이언트 메모리에 RSC Payload를 저장하여 탐색 경험을 개선합니다.

  • 라우트 세그먼트(레이아웃, 로딩 상태, 페이지 단위)별로 분할 저장
  • 사용자가 라우트 사이를 이동할 때:
    • 이미 방문한 세그먼트는 다시 서버에 요청하지 않고 캐시에서 사용
    • 향후 방문 가능성이 높은 라우트를 prefetch

6.1 특징

  • 레이아웃은 네비게이션 간 재사용 (partial rendering)
  • 로딩 상태는 반복 네비게이션에서도 재사용 (instant navigation)
  • 페이지는 기본적으로 캐시되지 않지만, 브라우저 뒤로/앞으로 이동 시 재사용
  • 실험적 옵션 staleTimes로 페이지 세그먼트도 캐시할 수 있음
  • 브라우저 bfcache와는 다른 메커니즘이지만 비슷한 효과

6.2 지속 시간과 무효화

  • 브라우저의 임시 메모리에 저장

  • 세션 동안 유효하지만, 페이지 새로고침 시 초기화

  • 자동 무효화 시간:

    • 기본 prefetch: 정적 페이지는 약 5분, 동적 페이지는 미캐시
    • 전체 prefetch (prefetch={true} 또는 router.prefetch) 시 정적·동적 모두 약 5분
  • Router Cache 무효화 방법:

    • Server Action에서
      • revalidatePath, revalidateTag 호출
      • cookies.set, cookies.delete 사용 시, 관련 라우트의 Router Cache 무효화 (예: 로그인 상태 갱신)
    • 클라이언트에서 router.refresh() 호출
      • 현재 라우트의 Router Cache를 지우고 서버에 새 요청을 보냄
  • <Link> 컴포넌트의 prefetchfalse로 설정해 prefetch 동작을 끌 수 있지만,

    • 사용자가 실제로 해당 라우트를 방문하면 그때 Router Cache에 저장됩니다.

7. 캐시 간 상호작용 (Cache Interactions)

캐시 설정을 할 때, 각 레이어가 서로 어떤 영향을 주는지를 이해하는 것이 중요합니다.

7.1 Data Cache ↔ Full Route Cache

  • Data Cache를 재검증하거나 opt-out 하면, 그 데이터를 사용하는 라우트의 Full Route Cache도 무효화됩니다.
  • 반대로 Full Route Cache를 무효화해도 Data Cache는 그대로 남습니다.
    • 덕분에 일부 데이터는 캐시를 유지하면서, 다른 일부만 매 요청마다 새로 가져오는 하이브리드 구성이 가능합니다.

7.2 Data Cache ↔ Router Cache

  • Server Action에서 revalidatePath 또는 revalidateTag를 호출하면,
    • Data Cache와 Router Cache를 동시에 무효화할 수 있습니다.
  • Route Handler에서 revalidateTag를 호출해도 Router Cache는 즉시 무효화되지 않습니다.
    • Router Cache는 자동 무효화 시간 또는 하드 리프레시 시점까지 이전 RSC Payload를 사용할 수 있습니다.

8. 주요 API 요약

다음은 캐싱에 관여하는 주요 API 요약입니다. (원문에는 표 형태로 정리되어 있음)

  • <Link prefetch>: Router Cache에 데이터 추가 (클라이언트 캐시)
  • router.prefetch: 특정 경로를 미리 가져와 Router Cache에 저장
  • router.refresh: Router Cache를 비우고 서버에 새 요청 (Data/Full Route Cache는 유지)
  • fetch:
    • 기본값: 정적 렌더링에서는 Data/Full Route Cache 사용, 동적 렌더링에서는 매 요청마다 새 데이터
    • cache: 'force-cache': 명시적으로 Data Cache 사용
    • next.revalidate: Data Cache 재검증 주기 설정 (초 단위)
    • next.tags: 캐시 태그 지정, 이후 revalidateTag로 무효화
  • revalidateTag(tag):
    • 태그 단위로 Data Cache/Full Route Cache 무효화
    • Route Handler / Server Action에서 사용
  • revalidatePath(path):
    • 경로 단위로 Data Cache/Full Route Cache 무효화
  • Route Segment Config:
    • export const revalidate = N : 라우트 전체의 재검증 주기
    • export const dynamic = 'force-dynamic' : Full Route Cache 사용하지 않음, 매 요청마다 렌더링
    • export const fetchCache = 'default-no-store' : 라우트 내 fetch 기본값을 no-store로 설정
  • Dynamic APIs (cookies, headers, searchParams):
    • 사용 시 해당 라우트는 Full Route Cache에서 제외 (동적 렌더링)

9. generateStaticParams와 캐시

generateStaticParams는 동적 세그먼트 라우트(예: app/blog/[slug]/page.tsx)의 정적 경로 목록을 정의합니다.

  • 반환된 경로들은 빌드 타임에 Full Route Cache에 저장
  • 빌드 타임에 알 수 없었던 경로는 첫 방문 시 캐시됩니다.
  • 전부 미리 렌더링하고 싶다면 모든 경로를 반환
  • 일부만 미리 렌더링하고 나머지는 첫 방문 시 렌더링하려면, 부분 목록만 반환
  • 완전히 on-demand로 렌더링하고 싶다면, 빈 배열을 반환하거나 export const dynamic = 'force-static' 을 사용할 수 있습니다.
  • generateStaticParams반드시 배열을 반환해야 하며, 그렇지 않으면 라우트가 동적으로 렌더링됩니다.

또한 export const dynamicParams = false를 사용하면, generateStaticParams가 반환한 경로만 200이고 나머지는 404가 되도록 제어할 수 있습니다.


10. React cache 함수

React의 cache() 함수는 임의의 비동기 함수 결과를 메모이제이션하는 도구입니다.

import { cache } from 'react'
import db from '@/lib/db'

export const getItem = cache(async (id: string) => {
  return db.item.findUnique({ id })
})
  • GET/HEAD 기반 fetch는 자동으로 메모이제이션되므로 cache로 감쌀 필요가 없습니다.
  • 그 외 메서드나, 자체 캐시를 제공하지 않는 DB/GraphQL/CMS 클라이언트 등은 cache로 감싸서 Request Memoization과 동일한 효과를 얻을 수 있습니다.

11. 정리

  • Next.js는 서버·클라이언트 양쪽에 서로 다른 캐시 레이어를 두어 성능을 최적화합니다.
    • Request Memoization (요청 단위)
    • Data Cache (데이터 단위)
    • Full Route Cache (라우트 렌더링 결과 단위)
    • Router Cache (클라이언트 네비게이션 단위)
  • 기본값은 “가능한 많이 캐싱”이므로,
    • 특별한 이유가 없다면 기본 동작을 그대로 두고
    • 동적 데이터가 필요한 부분만 no-store, dynamic = 'force-dynamic', revalidatePath, revalidateTag 등으로 예외를 설정하면 됩니다.
  • 캐시 간 상호작용을 이해하면,
    • “어디까지 정적으로 만들고, 어디부터는 실시간 데이터를 사용할지”를 세밀하게 설계할 수 있습니다.