Next.js 가이드 정리: Caching in Next.js
1. 개요 (Overview)
Next.js는 렌더링 결과와 데이터 요청을 적극적으로 캐싱하여 성능을 높이고 비용을 줄입니다. 이 문서는 Next.js의 캐싱 메커니즘과 관련 API, 그리고 서로가 어떻게 상호작용하는지를 설명합니다.
Next.js의 주요 캐싱 메커니즘은 다음 네 가지입니다.
| 메커니즘 | 무엇을 캐싱하는가 | 위치 | 목적 | 지속 시간 |
|---|---|---|---|---|
| Request Memoization | 함수/fetch 호출 결과 | 서버 | 동일 렌더링 패스 내에서 데이터 재사용 | 요청 1회 렌더링 주기 |
| Data Cache | 서버 데이터 (fetch 결과) | 서버 | 여러 사용자·배포 간에 데이터 재사용 | 지속적 (재검증 가능) |
| Full Route Cache | HTML + RSC Payload(서버 렌더링 결과) | 서버 | 렌더링 비용 절감, 빠른 응답 | 지속적 (재검증 가능) |
| Router Cache | RSC 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를 사용하면 라우트가 동적이 됩니다.
cookiesheadersconnectiondraftModesearchParams(Page의 props)unstable_noStorefetch옵션{ 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
- 메모리에서 바로 결과를 반환
- 동일한 URL + 옵션으로
- 렌더링이 끝나면 메모이제이션 데이터는 초기화됩니다.
3.2 특징 및 제약
- React 기능이며, Next.js에서 이를 활용
GET메서드의fetch에만 적용- React 컴포넌트 트리 안에서만 적용
generateMetadata,generateStaticParams, Layout, Page, Server Components 등- Route Handler에서는 적용되지 않음
- DB 클라이언트나 GraphQL 클라이언트 등
fetch가 아닌 경우에는 Reactcache()함수로 직접 메모이제이션할 수 있습니다. - 재검증 개념은 필요 없음 (렌더링 한 번에만 유효)
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에 저장 + 메모이제이션
- 최초 호출 시 Data Cache를 조회
cache옵션 미지정 또는{ cache: 'no-store' }:- 매번 데이터 소스에서 직접 가져오고, Data Cache에는 저장하지 않음
- 그래도 Request Memoization은 적용되어 동일 렌더링 패스 내 중복 호출은 제거
4.2 지속 시간 (Duration)
- Data Cache는 서버 요청과 배포 간에 지속됩니다.
- 다만, 재검증(시간 기반 / on-demand) 또는 명시적 opt-out에 의해 갱신/무효화됩니다.
4.3 재검증 (Revalidating)
재검증 방식은 두 가지입니다.
- 시간 기반 재검증 (Time-based Revalidation)
// 최대 1시간마다 한 번씩 재검증
fetch('https://...', { next: { revalidate: 3600 } })
- 최초 호출 시 데이터 소스에서 가져와 캐시에 저장
- 지정된 시간(예: 3600초) 내 반복 요청은 캐시된 데이터를 반환
- 시간이 지난 뒤 첫 요청:
- 여전히 기존(약간 오래된) 데이터를 먼저 반환
- 백그라운드에서 새 데이터를 가져와 캐시 갱신
- 실패하면 이전 데이터를 유지
- HTTP 캐시의
stale-while-revalidate와 비슷한 동작
- 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 서버에서의 렌더링
- 서버에서 React가 Server Component 트리를 렌더링해 RSC Payload를 생성
- Next.js가 RSC Payload와 클라이언트 컴포넌트 JS 정보를 활용해 HTML을 생성
- 이 과정은 스트리밍 기반이라, 일부가 준비되는 대로 응답을 보낼 수 있음
5.2 서버 캐싱 (Full Route Cache)
- 정적으로 렌더링되는 라우트의 경우, 위 렌더링 결과를 서버에 캐싱합니다.
- 이 캐시가 Full Route Cache입니다.
- 재검증 시에도 새로운 렌더링 결과가 다시 Full Route Cache에 저장됩니다.
5.3 클라이언트 측에서의 사용 (Hydration + Router Cache)
요청 시 클라이언트에서는:
- HTML로 컴포넌트 트리의 초기 뷰를 즉시 보여줌
- RSC Payload를 사용해 클라이언트·서버 컴포넌트 트리를 동기화
- 클라이언트 컴포넌트 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는 초기화되고 다시 채워짐
- Data Cache 재검증 (
5.6 Full Route Cache 사용하지 않기
-
라우트를 항상 동적으로 렌더링하려면:
- 동적 API 사용하기
cookies,headers,searchParams,draftMode,fetchwithno-store등
- Route Segment Config에서
export const dynamic = 'force-dynamic'- 또는
export const revalidate = 0
- 동적 API 사용하기
-
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를 지우고 서버에 새 요청을 보냄
- Server Action에서
-
<Link>컴포넌트의prefetch를false로 설정해 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등으로 예외를 설정하면 됩니다.
- 캐시 간 상호작용을 이해하면,
- “어디까지 정적으로 만들고, 어디부터는 실시간 데이터를 사용할지”를 세밀하게 설계할 수 있습니다.