Next.js는 렌더링 작업과 데이터 요청을 캐싱(미리 저장해 두고 재사용하는 것)하여 애플리케이션의 성능을 크게 향상시키고 비용을 절감해 줍니다. 이 페이지에서는 Next.js의 캐싱 메커니즘, 이를 구성하기 위해 사용할 수 있는 API, 그리고 이들이 서로 어떻게 상호작용하는지에 대해 아주 깊이 있게 살펴볼 거예요.
📝 강사의 보충 설명: 여기서 '비용 절감'이라는 부분에 주목해 볼까요? 우리가 Vercel이나 AWS 같은 클라우드 환경에 앱을 배포하면, 서버가 연산을 하거나 DB를 조회할 때마다 다 돈(비용)이 나갑니다. 캐싱을 잘 활용하면 서버가 일하는 횟수 자체를 줄여주기 때문에 트래픽이 몰려도 서버 유지비용을 엄청나게 방어할 수 있답니다.
알아두면 좋은 점 (Good to know): 이 페이지는 Next.js가 내부적으로(under the hood) 어떻게 작동하는지 이해하는 데 큰 도움을 줍니다. 하지만 Next.js로 생산성 있게 개발을 하기 위해 반드시 당장 알아야만 하는 필수 지식은 아닙니다. Next.js의 캐싱 휴리스틱(자동으로 판단하는 기준)은 대부분 여러분이 사용하는 API에 의해 결정되며, 아예 설정을 안 하거나(zero) 최소한의 설정만으로도 최고의 성능을 내도록 기본값이 세팅되어 있어요. 원리보다 예제를 먼저 보고 싶으시다면, 여기서부터 시작해 보세요.
다음은 서로 다른 캐싱 메커니즘들과 각각의 목적을 한눈에 보여주는 요약 표입니다:
| 메커니즘 (Mechanism) | 무엇을 캐싱하나요? (What) | 어디에 저장되나요? (Where) | 목적 (Purpose) | 유지 기간 (Duration) |
|---|---|---|---|---|
| 요청 메모이제이션 (Request Memoization) | 함수의 반환값 | 서버 (Server) | React 컴포넌트 트리 내에서 데이터를 재사용하기 위해 | 요청(Request)의 생명주기 동안 |
| 데이터 캐시 (Data Cache) | 실제 데이터 | 서버 (Server) | 여러 사용자의 요청이나 배포 간에도 데이터를 저장해 두기 위해 | 영구적 (재검증/Revalidating 가능) |
| 전체 라우트 캐시 (Full Route Cache) | HTML과 RSC(React Server Component) 페이로드 | 서버 (Server) | 렌더링 비용을 줄이고 성능을 개선하기 위해 | 영구적 (재검증 가능) |
| 라우터 캐시 (Router Cache) | RSC 페이로드 | 클라이언트 (Client) | 페이지 이동(Navigation) 시 서버 요청을 줄이기 위해 | 사용자 세션 동안 또는 시간 기반 |
기본적으로 Next.js는 성능을 높이고 비용을 줄이기 위해 가능한 한 많은 것을 캐싱하려고 합니다. 즉, 여러분이 명시적으로 캐싱을 끄지(opt out) 않는 한, 라우트들은 정적으로 렌더링(statically rendered) 되고 데이터 요청은 캐싱된다는 뜻이에요.
아래 다이어그램은 라우트가 빌드 타임에 정적으로 렌더링될 때와 정적 라우트를 처음 방문했을 때 일어나는 기본 캐싱 동작(HIT, MISS, SET)을 보여줍니다.

캐싱 동작은 라우트가 정적으로 렌더링되는지 동적으로 렌더링되는지, 데이터가 캐시되었는지 안 되었는지, 그리고 요청이 최초 방문인지 아니면 그 이후의 페이지 이동(Navigation)인지에 따라 달라집니다. 여러분의 사용 사례에 맞게 개별 라우트와 데이터 요청의 캐싱 동작을 직접 설정할 수도 있어요.
proxy 내부에서는 Fetch 캐싱이 지원되지 않습니다. proxy 내부에서 실행되는 모든 fetch는 캐시되지 않으니 주의해 주세요.
Next.js에서 캐싱이 어떻게 작동하는지 이해하려면, 먼저 사용 가능한 렌더링 전략을 이해하는 것이 큰 도움이 됩니다. 렌더링 전략은 라우트의 HTML이 '언제' 생성되는지를 결정하며, 이는 무엇을 캐싱할 수 있는지에 직접적인 영향을 미치거든요.
정적 렌더링을 사용하면, 라우트는 빌드 타임(build time) 에 렌더링되거나 데이터 재검증 (Incremental Static Regeneration) 이후 백그라운드에서 렌더링됩니다. 그 결과물은 캐시되어 여러 요청에서 재사용될 수 있죠. 정적 라우트들은 전체 라우트 캐시 (Full Route Cache)에 완전히 캐시됩니다.
📝 강사의 보충 설명: 옛날 방식의 SSG(Static Site Generation)를 생각하시면 쉬워요. HTML을 미리 다 구워놓고, 접속하는 사람들에게 똑같은 결과물을 빠르게 서빙하는 겁니다. 블로그 글이나 회사 소개 페이지처럼 내용이 자주 안 바뀌는 곳에 아주 제격이죠.
동적 렌더링을 사용하면, 라우트는 요청 시간(request time) (사용자가 접속할 때마다) 렌더링됩니다. 이 방식은 라우트가 쿠키(cookies), 헤더(headers), 검색 매개변수(search params)처럼 해당 요청에만 고유한 정보를 사용해야 할 때 발생합니다.
라우트는 다음과 같은 API 중 하나라도 사용하면 자동으로 동적(Dynamic)으로 변합니다:
cookiesheadersconnectiondraftModesearchParams propunstable_noStorefetch 에 { cache: 'no-store' } 옵션을 준 경우동적 라우트들은 전체 라우트 캐시(Full Route Cache)에는 캐시되지 않지만, 데이터 요청에 대해서는 여전히 데이터 캐시 (Data Cache)를 사용할 수 있습니다.
💡 강사의 팁: "어? 내 페이지는 왜 자꾸 빌드할 때 동적(Dynamic)으로 빠지지?" 하고 당황하실 때가 있을 거예요. 십중팔구 페이지 내부 어디선가 URL의 쿼리 스트링(searchParams)을 읽고 있거나, 유저 인증 처리를 위해 cookies()를 호출했기 때문입니다. 이런 API를 쓰면 서버는 '아, 이 페이지는 접속하는 사람마다 다르게 보여줘야 하는구나!' 하고 판단해서 캐싱을 포기하고 실시간 렌더링을 하게 됩니다.
알아두면 좋은 점: 캐시 컴포넌트(Cache Components)를 사용하면 같은 라우트 내에서도 정적 렌더링과 동적 렌더링을 섞어서 사용할 수 있습니다.
Next.js는 기본 fetch API를 확장하여, 동일한 URL과 옵션을 가진 요청들을 자동으로 메모이제이션(memoize, 기억) 합니다. 즉, React 컴포넌트 트리 내의 여러 곳에서 동일한 데이터를 가져오는 fetch 함수를 호출하더라도, 실제 네트워크 요청은 단 한 번만 실행된다는 뜻입니다.

예를 들어, 라우트 전반에 걸쳐 동일한 데이터가 필요할 때 (예: Layout, Page, 그리고 여러 하위 컴포넌트들에서 동일한 정보가 필요할 때), 굳이 트리의 맨 위에서 데이터를 한 번만 가져와서 props로 컴포넌트들 사이에 계속 내려줄(Prop drilling) 필요가 없습니다. 대신, 데이터를 필요로 하는 각 컴포넌트에서 직접 fetch를 호출하면 됩니다. 네트워크를 통해 동일한 데이터를 여러 번 요청하게 될까 봐 성능 저하를 걱정할 필요가 전혀 없어요!
//filename="app/example.tsx" switcher
async function getItem() {
// 이 `fetch` 함수는 자동으로 메모이제이션되며,
// 그 결과는 캐시됩니다.
const res = await fetch('https://.../item/1')
return res.json()
}
// 이 함수는 두 번 호출되지만, 실제 실행은 처음 한 번뿐입니다.
const item = await getItem() // 캐시 MISS (실제 네트워크 요청 발생)
// 두 번째 호출은 라우트 내 어디에 있든 상관없습니다.
const item = await getItem() // 캐시 HIT (메모리에서 바로 반환)
//filename="app/example.js" switcher
async function getItem() {
// 이 `fetch` 함수는 자동으로 메모이제이션되며,
// 그 결과는 캐시됩니다.
const res = await fetch('https://.../item/1')
return res.json()
}
// 이 함수는 두 번 호출되지만, 실제 실행은 처음 한 번뿐입니다.
const item = await getItem() // 캐시 MISS (실제 네트워크 요청 발생)
// 두 번째 호출은 라우트 내 어디에 있든 상관없습니다.
const item = await getItem() // 캐시 HIT (메모리에서 바로 반환)
요청 메모이제이션은 어떻게 작동할까요? (How Request Memoization Works)

MISS가 됩니다.HIT가 발생하고, 함수를 다시 실행하지 않고 메모리에서 데이터를 즉시 반환합니다.알아두면 좋은 점:
- 요청 메모이제이션은 Next.js만의 기능이 아니라 React의 기능입니다. 여기 포함시킨 이유는 다른 캐싱 메커니즘들과 어떻게 상호작용하는지 보여주기 위해서예요.
- 메모이제이션은
fetch요청 중에서도GET메서드에만 적용됩니다.- 메모이제이션은 React 컴포넌트 트리에만 적용됩니다. 이 말은 즉:
generateMetadata,generateStaticParams, Layout, Page 및 기타 서버 컴포넌트 내의fetch요청에는 적용됩니다.- 하지만 Route Handler(API 라우트) 내부의
fetch요청에는 적용되지 않습니다. (Route Handler는 React 컴포넌트 트리의 일부가 아니니까요.)fetch를 쓰기 적합하지 않은 경우(예: 일부 데이터베이스 클라이언트, CMS 클라이언트, 또는 GraphQL 클라이언트)에는 Reactcache함수를 사용하여 함수를 수동으로 메모이제이션할 수 있습니다.
이 캐시는 서버 요청의 생명주기 동안, 즉 React 컴포넌트 트리가 렌더링을 마칠 때까지만 유지됩니다.
메모이제이션은 여러 서버 요청 간에 공유되지 않고 렌더링 중에만 적용되기 때문에, 굳이 재검증(revalidate)할 필요가 없습니다. (어차피 한 번의 화면 그리기가 끝나면 사라지는 일회용 메모장이니까요!)
메모이제이션은 fetch의 GET 메서드에만 적용되며, POST나 DELETE 같은 다른 메서드는 메모이제이션되지 않습니다. 이 기본 동작은 React의 최적화 기능이기 때문에, 저희는 이것을 끄는(opt out) 것을 권장하지 않습니다.
개별 요청을 직접 관리하고 싶다면, AbortController의 signal 속성을 사용할 수 있습니다.
//filename="app/example.js"
const { signal } = new AbortController()
fetch(url, { signal })
Next.js에는 들어오는 서버 요청(server requests) 및 배포(deployments) 전반에 걸쳐 데이터 fetch 결과를 영구적으로 유지(persist) 하는 내장형 데이터 캐시(Data Cache)가 있습니다. 이것이 가능한 이유는 Next.js가 기본 fetch API를 확장하여 서버의 각 요청이 자체적인 영구 캐싱 방식을 설정할 수 있도록 만들었기 때문입니다.
알아두면 좋은 점: 브라우저 환경에서
fetch의cache옵션은 요청이 브라우저의 HTTP 캐시와 어떻게 상호작용할지를 나타냅니다. 반면, Next.js 환경에서cache옵션은 서버 측 요청이 서버의 데이터 캐시와 어떻게 상호작용할지를 의미합니다.
fetch의 cache 및 next.revalidate 옵션을 사용하여 캐싱 동작을 구성할 수 있습니다.
개발 모드(development mode)에서는 HMR(Hot Module Replacement)을 위해 fetch 데이터가 재사용되며, 강력 새로고침(hard refreshes)을 할 경우 캐싱 옵션이 무시됩니다.
데이터 캐시는 어떻게 작동할까요? (How the Data Cache Works)

'force-cache' 옵션이 있는 fetch 요청이 처음 호출되면, Next.js는 데이터 캐시에 저장된 응답이 있는지 확인합니다.cache 옵션이 없거나 { cache: 'no-store' }를 사용한 경우), 결과는 항상 데이터 소스에서 가져오고 메모이제이션만 수행합니다. (데이터 캐시에는 저장하지 않아요.)데이터 캐시와 요청 메모이제이션의 차이점
두 캐싱 메커니즘 모두 저장된 데이터를 재사용하여 성능을 향상시키는 데 도움이 되지만, 데이터 캐시는 들어오는 요청이나 새 배포가 일어나도 계속 유지(persistent)되는 반면, 메모이제이션은 딱 한 번의 요청 수명(렌더링 과정) 동안만 지속된다는 점이 다릅니다.
데이터 캐시는 여러분이 명시적으로 재검증(revalidate)하거나 캐시를 끄지 않는 한, 서버 요청과 배포 간에도 계속 영구적으로 유지됩니다.
캐시된 데이터는 두 가지 방법으로 최신 상태로 갱신(재검증)할 수 있습니다:
정해진 시간 간격으로 데이터를 재검증하려면, fetch의 next.revalidate 옵션을 사용하여 리소스의 캐시 수명을 초(seconds) 단위로 설정하면 됩니다.
// 최대 1시간(3600초)마다 한 번씩만 재검증합니다.
fetch('https://...', { next: { revalidate: 3600 } })
또는 라우트 세그먼트 설정(Route Segment Config) 옵션을 사용하여 특정 세그먼트 내의 모든 fetch 요청에 적용하거나, fetch를 사용할 수 없는 환경을 위해 구성할 수도 있습니다.
시간 기반 재검증은 어떻게 작동할까요? (How Time-based Revalidation Works)

revalidate 옵션이 있는 fetch 요청이 처음 호출되면, 외부 데이터 소스에서 데이터를 가져와 데이터 캐시에 저장합니다.📝 강사의 보충 설명: 이 방식은 웹 표준 기술인 stale-while-revalidate 동작과 똑같습니다. 일단 사용자에게는 지연 없이 이전 데이터를 보여주고, 뒷단에서 조용히 최신 데이터로 업데이트를 쳐두는 아주 스마트한 방식이죠!
데이터는 경로(path)를 기준(revalidatePath)으로 하거나 캐시 태그(tag)를 기준(revalidateTag)으로 하여 필요할 때 즉시(on-demand) 재검증할 수 있습니다.
주문형 재검증은 어떻게 작동할까요? (How On-Demand Revalidation Works)

fetch 요청이 처음 호출되면, 외부 데이터 소스에서 데이터를 가져와 데이터 캐시에 저장합니다.MISS가 발생하고, 데이터를 외부 소스에서 새롭게 가져와 다시 데이터 캐시에 저장합니다.💡 강사의 팁: 게시판에서 사용자가 '글쓰기' 버튼을 눌러 새 글을 등록했다고 가정해 볼게요. 시간 기반으로 해두면 1시간 뒤에나 새 글이 목록에 보이겠죠? 이럴 때 글이 등록되는 서버 액션(Server Action) 내부에서 revalidatePath('/board')를 딱 호출해 주면, 캐시가 즉시 날아가고 새 글이 포함된 최신 목록이 화면에 짜잔! 하고 나타나게 됩니다.
fetch의 응답을 캐시하고 싶지 않다면 다음과 같이 설정하면 됩니다:
// 매 요청마다 새로운 데이터를 가져옵니다.
let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })
관련 용어 (Related terms):
문서를 보다 보면 자동 정적 최적화 (Automatic Static Optimization), 정적 사이트 생성 (Static Site Generation), 또는 정적 렌더링 (Static Rendering) 이라는 용어들이 서로 섞여서 쓰이는 것을 볼 수 있습니다. 이들은 모두 빌드 타임에 애플리케이션의 라우트를 렌더링하고 캐시하는 과정을 가리킵니다.
Next.js는 빌드 타임에 라우트를 자동으로 렌더링하고 캐시합니다. 이는 들어오는 모든 요청마다 서버에서 렌더링을 새로 하는 대신 캐시된 라우트를 바로 서빙할 수 있게 해주는 최적화 기법으로, 결과적으로 페이지 로딩 속도가 비약적으로 빨라집니다.
전체 라우트 캐시가 어떻게 작동하는지 이해하려면, React가 렌더링을 어떻게 처리하고 Next.js가 그 결과를 어떻게 캐시하는지 살펴보는 것이 좋습니다:
서버에서 Next.js는 React의 API를 사용하여 렌더링 과정을 오케스트레이션(지휘)합니다. 렌더링 작업은 청크(chunks) 단위로 나뉘는데, 개별 라우트 세그먼트와 Suspense 바운더리를 기준으로 쪼개집니다.
각 청크는 두 단계로 렌더링됩니다:
이는 화면을 그리기 위한 모든 작업이 끝날 때까지 응답을 기다릴 필요 없이, 작업이 완료되는 대로 조각조각 스트리밍(stream)해서 클라이언트로 보낼 수 있다는 뜻입니다.
React Server Component Payload (RSC 페이로드)란 무엇인가요?
RSC 페이로드는 렌더링된 React 서버 컴포넌트 트리를 압축된 형태의 이진(binary) 데이터로 표현한 것입니다. 클라이언트(브라우저)에 있는 React는 이 데이터를 사용해서 브라우저의 DOM을 업데이트합니다. RSC 페이로드에는 다음 내용이 포함됩니다:
- 서버 컴포넌트가 렌더링된 결과물
- 클라이언트 컴포넌트가 렌더링되어야 할 위치의 자리 표시자(Placeholders) 및 해당 JavaScript 파일에 대한 참조
- 서버 컴포넌트에서 클라이언트 컴포넌트로 전달된 모든 props
더 자세한 내용은 서버 및 클라이언트 컴포넌트(Server Components) 문서를 참조하세요.

Next.js의 기본 동작은 라우트의 렌더링 결과물 (RSC 페이로드와 HTML)을 서버에 캐시하는 것입니다. 이는 빌드 타임에 정적으로 렌더링되는 라우트나 재검증 중에 적용됩니다.
요청이 들어왔을 때, 클라이언트(브라우저)에서는 다음과 같은 일이 일어납니다:
📝 강사의 보충 설명: 하이드레이션(Hydration)이란 메마른 HTML에 생명력(수분=JavaScript 이벤트)을 불어넣어 살아 숨 쉬게 만드는 과정이라고 생각하시면 됩니다!
RSC 페이로드는 클라이언트 측에 있는 라우터 캐시(Router Cache)에 저장됩니다. 이는 개별 라우트 세그먼트별로 나뉘어 있는 별도의 인메모리(in-memory) 캐시입니다. 이 라우터 캐시는 이전에 방문한 라우트를 저장하고 앞으로 이동할 라우트를 미리 가져와(prefetching) 페이지 이동 경험을 대폭 향상시키는 데 사용됩니다.
이후 사용자가 다른 페이지로 이동하거나 prefetching을 할 때, Next.js는 RSC 페이로드가 라우터 캐시에 저장되어 있는지 확인합니다. 만약 있다면, 서버에 새로운 요청을 보내지 않고 건너뜁니다. (완전 빠르겠죠?)
라우트 세그먼트가 캐시에 없는 경우에만 Next.js는 서버에서 RSC 페이로드를 가져와 클라이언트의 라우터 캐시를 채웁니다.
빌드 타임에 라우트가 캐시될지 여부는 해당 라우트가 정적으로 렌더링되는지 동적으로 렌더링되는지에 달려 있습니다. 정적 라우트는 기본적으로 캐시되지만, 동적 라우트는 요청 시간에 그때그때 렌더링되므로 캐시되지 않습니다.
이 다이어그램은 캐시된 데이터와 캐시되지 않은 데이터를 사용하여 정적 라우트와 동적 라우트의 차이점을 보여줍니다:

정적 및 동적 렌더링에 대해 자세히 알아보세요.
기본적으로 전체 라우트 캐시(Full Route Cache)는 영구적(persistent)입니다. 즉, 렌더링 결과물이 사용자 요청 전반에 걸쳐 계속 캐시됩니다.
전체 라우트 캐시를 무효화(캐시를 깨는 것)할 수 있는 방법은 두 가지입니다:
전체 라우트 캐시에서 벗어나고 싶다면, 즉 들어오는 모든 요청에 대해 컴포넌트를 동적으로 렌더링하고 싶다면 다음과 같은 방법을 쓸 수 있습니다:
cookies()나 headers() 등을 사용하면 라우트는 전체 라우트 캐시 대상에서 제외되며 요청 시간에 동적으로 렌더링됩니다. (물론 이 경우에도 데이터 캐시는 여전히 사용할 수 있습니다.)dynamic = 'force-dynamic' 또는 revalidate = 0 라우트 세그먼트 옵션 사용: 이 옵션을 걸면 전체 라우트 캐시와 데이터 캐시를 모두 건너뜁니다. 즉, 서버에 요청이 올 때마다 매번 컴포넌트가 렌더링되고 데이터를 새로 가져옵니다. 단, 라우터 캐시는 클라이언트 측 캐시이므로 여전히 적용됩니다.fetch 요청이 하나라도 있다면, 해당 라우트는 전체 라우트 캐시에서 제외됩니다. 캐시되지 않은 해당 특정 데이터는 매 요청마다 새로 가져오게 됩니다. 반면 캐싱을 명시적으로 설정한 다른 fetch 요청들은 여전히 데이터 캐시에 저장됩니다. 이렇게 하면 캐시된 데이터와 캐시되지 않은 데이터가 혼합된 형태(hybrid)를 구성할 수 있습니다.Next.js는 레이아웃, 로딩 상태, 페이지 등으로 나뉜 라우트 세그먼트의 RSC 페이로드를 저장하는 메모리 내(in-memory) 클라이언트 측 라우터 캐시를 가지고 있습니다.
사용자가 경로 간을 이동할 때, Next.js는 방문한 라우트 세그먼트를 캐시하고 사용자가 이동할 가능성이 높은 경로를 미리 가져옵니다(prefetching). 그 결과 뒤로가기/앞으로가기 시 페이지가 즉각적으로 전환되고, 이동 간에 전체 페이지 새로고침이 발생하지 않으며, 공유 레이아웃의 브라우저 상태와 React 상태가 그대로 보존됩니다.
라우터 캐시 덕분에 다음과 같은 효과를 얻을 수 있습니다:
staleTimes 설정 옵션을 사용하면 페이지 세그먼트에 대한 캐싱을 명시적으로 활성화할 수도 있습니다.알아두면 좋은 점: 이 캐시는 Next.js와 서버 컴포넌트에 특별히 적용되는 것이며, 결과는 비슷해 보여도 브라우저 고유의 bfcache 기능과는 다릅니다.
이 캐시는 브라우저의 임시 메모리에 저장됩니다. 라우터 캐시가 얼마나 오래 유지되는지는 두 가지 요소에 의해 결정됩니다:
prefetch={null} 또는 설정 안 함): 동적 페이지는 캐시 안 됨, 정적 페이지는 5분 동안 유지.prefetch={true} 또는 router.prefetch 호출): 정적, 동적 페이지 모두 5분 동안 유지.페이지 새로고침은 캐시된 모든 세그먼트를 날려버리지만, 자동 무효화 기간은 미리 가져온 그 시점부터 계산되어 개별 세그먼트에만 영향을 미칩니다.
알아두면 좋은 점: 실험적인 기능인
staleTimes설정 옵션을 사용하면 방금 말씀드린 자동 무효화 시간을 입맛에 맞게 조절할 수 있습니다.
라우터 캐시를 무효화하는 방법은 두 가지가 있습니다:
revalidatePath) 또는 캐시 태그 기반(revalidateTag)으로 데이터를 주문형(on-demand)으로 재검증할 때.cookies.set 또는 cookies.delete를 사용하여 쿠키를 변경할 때. 이렇게 하면 로그인 인증과 같이 쿠키를 사용하는 라우트가 이전 데이터를 보여주는 것을 방지하기 위해 라우터 캐시가 무효화됩니다.router.refresh를 호출할 때. 이 함수를 부르면 즉시 라우터 캐시가 무효화되고 현재 경로에 대해 서버로 새로운 요청을 보냅니다.💡 강사의 팁: SPA(Single Page Application) 개발하실 때 화면 데이터가 안 바뀌어서 수동으로 리렌더링 트리거 해보신 적 있죠? Next.js App 라우터에서는 클라이언트 컴포넌트에서 const router = useRouter()를 선언하고 router.refresh()를 호출해주면 손쉽게 최신 서버 데이터를 가져오면서 화면을 갱신할 수 있답니다.
Next.js 15부터 페이지 세그먼트의 클라이언트 캐싱은 기본적으로 꺼져(opt-out) 있습니다.
알아두면 좋은 점:
<Link>컴포넌트의prefetchprop을false로 설정하면 미리 가져오기(prefetching) 동작도 끌 수 있습니다.
여러 캐싱 메커니즘을 설정할 때, 이들이 서로 어떻게 상호작용하는지 이해하는 것은 매우 중요합니다:
revalidatePath나 revalidateTag를 사용하면 됩니다.다음 표는 다양한 Next.js API가 각 캐싱 메커니즘에 어떤 영향을 미치는지 요약한 것입니다. 아주 유용한 치트시트가 될 거예요!
| API | 라우터 캐시 (Router Cache) | 전체 라우트 캐시 (Full Route Cache) | 데이터 캐시 (Data Cache) | React 캐시 (React Cache) |
|---|---|---|---|---|
<Link prefetch> | 캐시 | |||
router.prefetch | 캐시 | |||
router.refresh | 재검증 (Revalidate) | |||
fetch | 캐시 | 캐시 (GET 및 HEAD) | ||
fetch options.cache | 캐시 또는 끄기 | |||
fetch options.next.revalidate | 재검증 | 재검증 | ||
fetch options.next.tags | 캐시 | 캐시 | ||
revalidateTag | 재검증 (서버 액션에서) | 재검증 | 재검증 | |
revalidatePath | 재검증 (서버 액션에서) | 재검증 | 재검증 | |
const revalidate | 재검증 또는 끄기 | 재검증 또는 끄기 | ||
const dynamic | 캐시 또는 끄기 | 캐시 또는 끄기 | ||
cookies | 재검증 (서버 액션에서) | 끄기 (Opt out) | ||
headers, searchParams | 끄기 (Opt out) | |||
generateStaticParams | 캐시 | |||
React.cache | 캐시 | |||
unstable_cache | 캐시 |
<Link>기본적으로 <Link> 컴포넌트는 전체 라우트 캐시에서 라우트를 자동으로 미리 가져와(prefetch) 그 RSC 페이로드를 클라이언트의 라우터 캐시에 추가합니다.
이 prefetching 동작을 끄려면 prefetch prop을 false로 설정하면 됩니다. 하지만 이렇게 한다고 해서 영구적으로 캐싱을 건너뛰는 것은 아니며, 사용자가 실제로 해당 라우트를 방문할 때는 클라이언트 측에 해당 세그먼트가 다시 캐시됩니다.
<Link> 컴포넌트에 대해 더 자세히 알아보세요.
router.prefetchuseRouter 훅의 prefetch 옵션을 사용하면 코드로 직접 라우트를 미리 가져올 수 있습니다. 이 동작 역시 RSC 페이로드를 라우터 캐시에 추가합니다.
useRouter 훅 API 레퍼런스를 참조하세요.
router.refreshuseRouter 훅의 refresh 메서드는 현재 라우트를 수동으로 새로고침할 때 사용합니다. 이 메서드를 호출하면 라우터 캐시가 완전히 지워지고 서버에 현재 라우트에 대한 새로운 요청을 보냅니다. 중요한 건 refresh는 데이터 캐시나 전체 라우트 캐시에는 영향을 주지 않는다는 점입니다.
서버에서 받아온 렌더링 결과물은 클라이언트에서 기존의 React 상태(state)나 브라우저 상태를 유지하면서 부드럽게 재조정(reconcile)됩니다.
useRouter 훅 API 레퍼런스를 참조하세요.
fetchfetch에서 반환된 데이터는 자동으로 데이터 캐시에 들어가는 것이 아닙니다. (과거 버전과 헷갈리시면 안 돼요!)
cache 옵션이나 next.revalidate 옵션을 명시적으로 주지 않은 경우, 기본 동작은 렌더링 방식에 따라 다릅니다:
fetch가 실행되어 항상 신선한(fresh) 데이터를 반환합니다.fetch로 가져온 데이터는 데이터 캐시에 저장되고, 렌더링된 결과물은 전체 라우트 캐시에 저장됩니다. 경로가 재검증되기 전까지 Next.js는 이 캐시된 결과물을 서빙합니다.더 많은 옵션은 fetch API 레퍼런스를 확인하세요.
fetch options.cache개별 fetch 요청에 대해 명시적으로 캐싱을 켜고 싶다면 cache 옵션을 force-cache로 설정하면 됩니다:
// 캐싱 사용(opt into caching) 명시
fetch(`https://...`, { cache: 'force-cache' })
더 많은 옵션은 fetch API 레퍼런스를 확인하세요.
fetch options.next.revalidate개별 fetch 요청의 재검증 주기(초 단위)를 설정하려면 fetch의 next.revalidate 옵션을 사용하세요. 이렇게 하면 데이터 캐시가 재검증되고, 이는 곧 전체 라우트 캐시의 재검증으로 이어집니다. 새 데이터를 다시 가져오고 서버에서 컴포넌트가 새롭게 렌더링되게 되죠.
// 최대 1시간이 지나면 데이터를 재검증합니다.
fetch(`https://...`, { next: { revalidate: 3600 } })
더 많은 옵션은 fetch API 레퍼런스를 확인하세요.
fetch options.next.tags 및 revalidateTagNext.js는 더욱 세밀한 데이터 캐싱과 재검증을 위해 캐시 태그(tagging) 시스템을 제공합니다.
fetch나 unstable_cache를 사용할 때, 캐시 항목에 하나 이상의 태그를 지정할 수 있습니다.revalidateTag 함수를 호출할 수 있습니다.예를 들어, 데이터를 가져올 때 태그를 설정하려면:
// 여러 태그를 달아서 데이터를 캐시합니다.
fetch(`https://...`, { next: { tags: ['a', 'b', 'c'] } })
그런 다음, 특정 태그가 달린 데이터를 재검증(무효화) 하려면:
// 'a' 태그가 달린 캐시 항목들을 재검증합니다.
revalidateTag('a')
여러분의 목적에 따라 두 가지 위치에서 revalidateTag를 호출할 수 있습니다:
revalidatePathrevalidatePath 함수를 사용하면 단 한 번의 작업으로 특정 경로 아래에 있는 데이터 캐시를 수동으로 재검증하면서 동시에 라우트 세그먼트들을 다시 렌더링하게 만들 수 있습니다. revalidatePath를 호출하면 데이터 캐시가 무효화되고, 결과적으로 전체 라우트 캐시까지 무효화됩니다.
// 홈 경로('/')를 재검증합니다.
revalidatePath('/')
revalidatePath 역시 달성하고자 하는 목표에 따라 두 곳에서 사용할 수 있습니다:
더 자세한 정보는 revalidatePath API 레퍼런스를 참고하세요.
revalidatePathvs.router.refresh:
router.refresh를 호출하면 라우터 캐시를 지우고 서버에서 라우트 세그먼트를 다시 렌더링하지만, 데이터 캐시나 전체 라우트 캐시는 건드리지 않습니다 (무효화하지 않음).차이점을 요약하자면,
revalidatePath는 백엔드(서버) 쪽에 있는 데이터 캐시와 전체 라우트 캐시를 싹 날려버리는 강력한 방법이고,router.refresh()는 클라이언트 측 API로서 서버의 캐시는 그대로 둔 채 브라우저 쪽 화면 상태만 새로고침하는 것입니다.
cookies, headers 같은 동적 API나 페이지에서 사용하는 searchParams prop은 런타임에 들어오는 요청의 정보에 의존합니다. 이런 API들을 컴포넌트 내부에서 사용하면 전체 라우트 캐시에서 빠져나가게 되며(opt out), 다시 말해 라우트가 사용자 요청 시마다 동적으로 렌더링(dynamically rendered)됩니다.
cookies서버 액션 내부에서 cookies.set이나 cookies.delete를 사용하면 쿠키 정보를 사용하는 라우트가 예전 화면을 보여주지 않도록 하기 위해(예: 로그인/로그아웃 상태 반영) 라우터 캐시가 자동으로 무효화됩니다.
cookies API 레퍼런스를 참조하세요.
라우트 세그먼트 설정(Route Segment Config) 옵션은 특정 라우트 세그먼트의 기본 캐시 설정을 덮어쓰고 싶거나, (DB 클라이언트나 서드파티 라이브러리 등) fetch API를 직접 사용할 수 없는 환경에서 유용하게 쓰입니다.
아래 라우트 세그먼트 설정 옵션을 파일 상단에 적어두면 전체 라우트 캐시 동작을 끕니다:
const dynamic = 'force-dynamic'아래 옵션은 해당 세그먼트에서 발생하는 모든 fetch 요청이 데이터 캐시를 쓰지 못하도록(no-store) 만듭니다:
const fetchCache = 'default-no-store'더 고급 설정들을 보고 싶다면 fetchCache 부분을 확인하세요.
전체적인 옵션은 라우트 세그먼트 설정 (Route Segment Config) 문서에서 볼 수 있습니다.
generateStaticParams동적 세그먼트(dynamic segments, 예: app/blog/[slug]/page.js)의 경우, generateStaticParams 함수가 제공하는 경로(path)들은 빌드 타임에 전체 라우트 캐시에 안전하게 캐시됩니다. 그리고 빌드 타임에는 몰랐던 경로에 사용자가 처음 접근하면, 그 요청 시간에 Next.js가 해당 경로도 새로 렌더링해서 캐시에 추가해 줍니다.
빌드 타임에 모든 가능한 경로를 정적으로 렌더링해 두려면, generateStaticParams에서 전체 경로 리스트를 배열로 반환하면 됩니다:
//filename="app/blog/[slug]/page.js"
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
// 모든 포스트의 slug를 반환
return posts.map((post) => ({
slug: post.slug,
}))
}
일부 경로만 빌드 타임에 미리 정적으로 렌더링해 두고, 나머지 경로는 런타임에 처음 방문될 때 렌더링되도록 하려면, 전체 리스트의 일부(부분 리스트)만 반환하면 됩니다:
//filename="app/blog/[slug]/page.js"
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
// 인기 글이나 최신 글 같은 처음 10개만 빌드 타임에 미리 렌더링
return posts.slice(0, 10).map((post) => ({
slug: post.slug,
}))
}
모든 경로를 처음 방문될 때 렌더링되도록 미루고 싶다면, 그냥 빈 배열을 반환(빌드 시 아무것도 렌더링 안 함)하거나 export const dynamic = 'force-static'을 사용할 수 있습니다:
//filename="app/blog/[slug]/page.js"
export async function generateStaticParams() {
return [] // 빈 배열 반환
}
알아두면 좋은 점:
generateStaticParams함수에서는 반환값이 비어있더라도 반드시 배열을 반환해야 합니다. 배열을 반환하지 않으면 그 라우트는 동적으로 렌더링되어 버립니다.
//filename="app/changelog/[slug]/page.js"
export const dynamic = 'force-static'
요청 시간(runtime)에 이뤄지는 캐싱 동작을 끄고 싶다면 라우트 세그먼트에 export const dynamicParams = false 옵션을 추가하세요. 이 설정이 켜지면 오직 generateStaticParams에서 제공된 경로들만 서비스되며, 거기에 없는 다른 경로로 접근하면 404 에러가 나거나 포괄적 라우트(catch-all routes) 조건에 매칭되게 됩니다.
cache 함수React의 cache 함수를 사용하면 함수의 반환값을 메모이제이션할 수 있습니다. 그래서 같은 함수를 여러 번 호출하더라도 실제 실행은 처음 한 번만 되게 만들 수 있죠.
앞서 배웠듯이 GET 또는 HEAD 메서드를 사용하는 fetch 요청은 Next.js가 자동으로 메모이제이션해 주므로 굳이 React cache로 감쌀 필요가 없습니다. 하지만 다른 fetch 메서드를 사용하거나, 기본적으로 요청을 메모이제이션해주지 않는 데이터 가져오기 라이브러리(예: 특정 DB 클라이언트, CMS, GraphQL 클라이언트 등)를 사용하는 경우에는 cache를 직접 써서 데이터 요청을 수동으로 메모이제이션할 수 있습니다.
//filename="utils/get-item.ts" switcher
import { cache } from 'react'
import db from '@/lib/db'
// db 조회를 cache로 감싸두면, 같은 id로 여러 번 호출해도 DB 조회가 한 번만 일어납니다!
export const getItem = cache(async (id: string) => {
const item = await db.item.findUnique({ id })
return item
})
//filename="utils/get-item.js" switcher
import { cache } from 'react'
import db from '@/lib/db'
export const getItem = cache(async (id) => {
const item = await db.item.findUnique({ id })
return item
})
전체 문서의 구조적(semantic) 개요를 보려면, https://nextjs.org/docs/sitemap.md 를 확인해 보세요.
사용 가능한 모든 문서의 색인(index)을 보려면, https://nextjs.org/docs/llms.txt 를 확인해 보세요.