Caching in Next.js

김동현·2026년 3월 4일

next.js 공식문서 번역

목록 보기
21/79

Next.js는 렌더링 작업과 데이터 요청을 캐싱(미리 저장해 두고 재사용하는 것)하여 애플리케이션의 성능을 크게 향상시키고 비용을 절감해 줍니다. 이 페이지에서는 Next.js의 캐싱 메커니즘, 이를 구성하기 위해 사용할 수 있는 API, 그리고 이들이 서로 어떻게 상호작용하는지에 대해 아주 깊이 있게 살펴볼 거예요.

📝 강사의 보충 설명: 여기서 '비용 절감'이라는 부분에 주목해 볼까요? 우리가 Vercel이나 AWS 같은 클라우드 환경에 앱을 배포하면, 서버가 연산을 하거나 DB를 조회할 때마다 다 돈(비용)이 나갑니다. 캐싱을 잘 활용하면 서버가 일하는 횟수 자체를 줄여주기 때문에 트래픽이 몰려도 서버 유지비용을 엄청나게 방어할 수 있답니다.

알아두면 좋은 점 (Good to know): 이 페이지는 Next.js가 내부적으로(under the hood) 어떻게 작동하는지 이해하는 데 큰 도움을 줍니다. 하지만 Next.js로 생산성 있게 개발을 하기 위해 반드시 당장 알아야만 하는 필수 지식은 아닙니다. Next.js의 캐싱 휴리스틱(자동으로 판단하는 기준)은 대부분 여러분이 사용하는 API에 의해 결정되며, 아예 설정을 안 하거나(zero) 최소한의 설정만으로도 최고의 성능을 내도록 기본값이 세팅되어 있어요. 원리보다 예제를 먼저 보고 싶으시다면, 여기서부터 시작해 보세요.

전체적인 개요 (Overview)

다음은 서로 다른 캐싱 메커니즘들과 각각의 목적을 한눈에 보여주는 요약 표입니다:

메커니즘 (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)을 보여줍니다.

Diagram showing the default caching behavior in Next.js for the four mechanisms, with HIT, MISS and SET at build time and when a route is first visited.

캐싱 동작은 라우트가 정적으로 렌더링되는지 동적으로 렌더링되는지, 데이터가 캐시되었는지 안 되었는지, 그리고 요청이 최초 방문인지 아니면 그 이후의 페이지 이동(Navigation)인지에 따라 달라집니다. 여러분의 사용 사례에 맞게 개별 라우트와 데이터 요청의 캐싱 동작을 직접 설정할 수도 있어요.

proxy 내부에서는 Fetch 캐싱이 지원되지 않습니다. proxy 내부에서 실행되는 모든 fetch는 캐시되지 않으니 주의해 주세요.

렌더링 전략 (Rendering Strategies)

Next.js에서 캐싱이 어떻게 작동하는지 이해하려면, 먼저 사용 가능한 렌더링 전략을 이해하는 것이 큰 도움이 됩니다. 렌더링 전략은 라우트의 HTML이 '언제' 생성되는지를 결정하며, 이는 무엇을 캐싱할 수 있는지에 직접적인 영향을 미치거든요.

정적 렌더링 (Static Rendering)

정적 렌더링을 사용하면, 라우트는 빌드 타임(build time) 에 렌더링되거나 데이터 재검증 (Incremental Static Regeneration) 이후 백그라운드에서 렌더링됩니다. 그 결과물은 캐시되어 여러 요청에서 재사용될 수 있죠. 정적 라우트들은 전체 라우트 캐시 (Full Route Cache)에 완전히 캐시됩니다.

📝 강사의 보충 설명: 옛날 방식의 SSG(Static Site Generation)를 생각하시면 쉬워요. HTML을 미리 다 구워놓고, 접속하는 사람들에게 똑같은 결과물을 빠르게 서빙하는 겁니다. 블로그 글이나 회사 소개 페이지처럼 내용이 자주 안 바뀌는 곳에 아주 제격이죠.

동적 렌더링 (Dynamic Rendering)

동적 렌더링을 사용하면, 라우트는 요청 시간(request time) (사용자가 접속할 때마다) 렌더링됩니다. 이 방식은 라우트가 쿠키(cookies), 헤더(headers), 검색 매개변수(search params)처럼 해당 요청에만 고유한 정보를 사용해야 할 때 발생합니다.

라우트는 다음과 같은 API 중 하나라도 사용하면 자동으로 동적(Dynamic)으로 변합니다:

동적 라우트들은 전체 라우트 캐시(Full Route Cache)에는 캐시되지 않지만, 데이터 요청에 대해서는 여전히 데이터 캐시 (Data Cache)를 사용할 수 있습니다.

💡 강사의 팁: "어? 내 페이지는 왜 자꾸 빌드할 때 동적(Dynamic)으로 빠지지?" 하고 당황하실 때가 있을 거예요. 십중팔구 페이지 내부 어디선가 URL의 쿼리 스트링(searchParams)을 읽고 있거나, 유저 인증 처리를 위해 cookies()를 호출했기 때문입니다. 이런 API를 쓰면 서버는 '아, 이 페이지는 접속하는 사람마다 다르게 보여줘야 하는구나!' 하고 판단해서 캐싱을 포기하고 실시간 렌더링을 하게 됩니다.

알아두면 좋은 점: 캐시 컴포넌트(Cache Components)를 사용하면 같은 라우트 내에서도 정적 렌더링과 동적 렌더링을 섞어서 사용할 수 있습니다.

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

Next.js는 기본 fetch API를 확장하여, 동일한 URL과 옵션을 가진 요청들을 자동으로 메모이제이션(memoize, 기억) 합니다. 즉, React 컴포넌트 트리 내의 여러 곳에서 동일한 데이터를 가져오는 fetch 함수를 호출하더라도, 실제 네트워크 요청은 단 한 번만 실행된다는 뜻입니다.

Deduplicated Fetch Requests

예를 들어, 라우트 전반에 걸쳐 동일한 데이터가 필요할 때 (예: 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)

Diagram showing how fetch memoization works during React rendering.

  • 라우트를 렌더링하는 동안, 특정 요청이 처음 호출되면 메모리에 그 결과가 없으므로 캐시 MISS가 됩니다.
  • 따라서 함수가 실행되고, 외부 소스에서 데이터를 가져온(fetch) 다음, 그 결과가 서버의 메모리에 저장됩니다.
  • 동일한 렌더링 패스(render pass) 내에서 해당 요청 함수가 연달아 호출되면 캐시 HIT가 발생하고, 함수를 다시 실행하지 않고 메모리에서 데이터를 즉시 반환합니다.
  • 라우트 렌더링이 완료되고 렌더링 패스가 끝나면, 메모리가 "초기화(reset)"되고 모든 요청 메모이제이션 항목이 지워집니다.

알아두면 좋은 점:

  • 요청 메모이제이션은 Next.js만의 기능이 아니라 React의 기능입니다. 여기 포함시킨 이유는 다른 캐싱 메커니즘들과 어떻게 상호작용하는지 보여주기 위해서예요.
  • 메모이제이션은 fetch 요청 중에서도 GET 메서드에만 적용됩니다.
  • 메모이제이션은 React 컴포넌트 트리에만 적용됩니다. 이 말은 즉:
    • generateMetadata, generateStaticParams, Layout, Page 및 기타 서버 컴포넌트 내의 fetch 요청에는 적용됩니다.
    • 하지만 Route Handler(API 라우트) 내부의 fetch 요청에는 적용되지 않습니다. (Route Handler는 React 컴포넌트 트리의 일부가 아니니까요.)
  • fetch를 쓰기 적합하지 않은 경우(예: 일부 데이터베이스 클라이언트, CMS 클라이언트, 또는 GraphQL 클라이언트)에는 React cache 함수를 사용하여 함수를 수동으로 메모이제이션할 수 있습니다.

유지 기간 (Duration)

이 캐시는 서버 요청의 생명주기 동안, 즉 React 컴포넌트 트리가 렌더링을 마칠 때까지만 유지됩니다.

재검증 (Revalidating)

메모이제이션은 여러 서버 요청 간에 공유되지 않고 렌더링 중에만 적용되기 때문에, 굳이 재검증(revalidate)할 필요가 없습니다. (어차피 한 번의 화면 그리기가 끝나면 사라지는 일회용 메모장이니까요!)

캐싱 끄기 (Opting out)

메모이제이션은 fetchGET 메서드에만 적용되며, POSTDELETE 같은 다른 메서드는 메모이제이션되지 않습니다. 이 기본 동작은 React의 최적화 기능이기 때문에, 저희는 이것을 끄는(opt out) 것을 권장하지 않습니다.

개별 요청을 직접 관리하고 싶다면, AbortControllersignal 속성을 사용할 수 있습니다.

//filename="app/example.js"
const { signal } = new AbortController()
fetch(url, { signal })

데이터 캐시 (Data Cache)

Next.js에는 들어오는 서버 요청(server requests)배포(deployments) 전반에 걸쳐 데이터 fetch 결과를 영구적으로 유지(persist) 하는 내장형 데이터 캐시(Data Cache)가 있습니다. 이것이 가능한 이유는 Next.js가 기본 fetch API를 확장하여 서버의 각 요청이 자체적인 영구 캐싱 방식을 설정할 수 있도록 만들었기 때문입니다.

알아두면 좋은 점: 브라우저 환경에서 fetchcache 옵션은 요청이 브라우저의 HTTP 캐시와 어떻게 상호작용할지를 나타냅니다. 반면, Next.js 환경에서 cache 옵션은 서버 측 요청이 서버의 데이터 캐시와 어떻게 상호작용할지를 의미합니다.

fetchcachenext.revalidate 옵션을 사용하여 캐싱 동작을 구성할 수 있습니다.

개발 모드(development mode)에서는 HMR(Hot Module Replacement)을 위해 fetch 데이터가 재사용되며, 강력 새로고침(hard refreshes)을 할 경우 캐싱 옵션이 무시됩니다.

데이터 캐시는 어떻게 작동할까요? (How the Data Cache Works)

Diagram showing how cached and uncached fetch requests interact with the Data Cache. Cached requests are stored in the Data Cache, and memoized, uncached requests are fetched from the data source, not stored in the Data Cache, and memoized.

  • 렌더링 중에 'force-cache' 옵션이 있는 fetch 요청이 처음 호출되면, Next.js는 데이터 캐시에 저장된 응답이 있는지 확인합니다.
  • 캐시된 응답이 발견되면 즉시 반환하고 메모이제이션합니다.
  • 캐시된 응답이 없다면, 데이터 소스(서버나 DB)로 요청을 보내어 결과를 받아온 뒤, 데이터 캐시에 저장하고 메모이제이션합니다.
  • 캐시되지 않는 데이터의 경우 (예: cache 옵션이 없거나 { cache: 'no-store' }를 사용한 경우), 결과는 항상 데이터 소스에서 가져오고 메모이제이션만 수행합니다. (데이터 캐시에는 저장하지 않아요.)
  • 데이터가 캐시되든 안 되든, React 렌더링 과정에서 동일한 데이터를 중복 요청하는 것을 막기 위해 요청은 항상 메모이제이션됩니다.

데이터 캐시와 요청 메모이제이션의 차이점

두 캐싱 메커니즘 모두 저장된 데이터를 재사용하여 성능을 향상시키는 데 도움이 되지만, 데이터 캐시는 들어오는 요청이나 새 배포가 일어나도 계속 유지(persistent)되는 반면, 메모이제이션은 딱 한 번의 요청 수명(렌더링 과정) 동안만 지속된다는 점이 다릅니다.

유지 기간 (Duration)

데이터 캐시는 여러분이 명시적으로 재검증(revalidate)하거나 캐시를 끄지 않는 한, 서버 요청과 배포 간에도 계속 영구적으로 유지됩니다.

재검증 (Revalidating)

캐시된 데이터는 두 가지 방법으로 최신 상태로 갱신(재검증)할 수 있습니다:

  • 시간 기반 재검증 (Time-based Revalidation): 일정 시간이 지난 후 새로운 요청이 들어오면 데이터를 재검증합니다. 이 방식은 데이터가 자주 변경되지 않고 실시간성이 아주 중요하지는 않은 경우에 유용합니다.
  • 주문형(요청형) 재검증 (On-demand Revalidation): 이벤트(예: 폼 제출, 버튼 클릭 등)를 기반으로 데이터를 재검증합니다. 주문형 재검증은 태그 기반(tag-based)이나 경로 기반(path-based) 방식을 사용하여 여러 데이터 그룹을 한 번에 갱신할 수 있습니다. 헤드리스 CMS에서 콘텐츠가 업데이트되었을 때처럼, 가능한 한 빨리 사용자에게 최신 데이터를 보여주고 싶을 때 아주 유용하죠.

시간 기반 재검증 (Time-based Revalidation)

정해진 시간 간격으로 데이터를 재검증하려면, fetchnext.revalidate 옵션을 사용하여 리소스의 캐시 수명을 초(seconds) 단위로 설정하면 됩니다.

// 최대 1시간(3600초)마다 한 번씩만 재검증합니다.
fetch('https://...', { next: { revalidate: 3600 } })

또는 라우트 세그먼트 설정(Route Segment Config) 옵션을 사용하여 특정 세그먼트 내의 모든 fetch 요청에 적용하거나, fetch를 사용할 수 없는 환경을 위해 구성할 수도 있습니다.

시간 기반 재검증은 어떻게 작동할까요? (How Time-based Revalidation Works)

Diagram showing how time-based revalidation works, after the revalidation period, stale data is returned for the first request, then data is revalidated.

  • revalidate 옵션이 있는 fetch 요청이 처음 호출되면, 외부 데이터 소스에서 데이터를 가져와 데이터 캐시에 저장합니다.
  • 지정된 시간(예: 60초) 내에 들어오는 모든 요청은 저장되어 있던 캐시 데이터를 그대로 반환합니다.
  • 시간이 지난 후 들어오는 첫 번째 다음 요청은 여전히 캐시되어 있는 (이제는 오래된/stale) 데이터를 반환합니다.
    • 이때 Next.js는 백그라운드에서 데이터를 몰래 다시 가져오는 재검증 작업을 트리거(실행)합니다.
    • 데이터를 성공적으로 가져오면, Next.js는 신선한(fresh) 최신 데이터로 데이터 캐시를 업데이트합니다.
    • 만약 백그라운드 재검증에 실패하면, 이전의 오래된 데이터를 그대로 유지합니다.

📝 강사의 보충 설명: 이 방식은 웹 표준 기술인 stale-while-revalidate 동작과 똑같습니다. 일단 사용자에게는 지연 없이 이전 데이터를 보여주고, 뒷단에서 조용히 최신 데이터로 업데이트를 쳐두는 아주 스마트한 방식이죠!

주문형 재검증 (On-demand Revalidation)

데이터는 경로(path)를 기준(revalidatePath)으로 하거나 캐시 태그(tag)를 기준(revalidateTag)으로 하여 필요할 때 즉시(on-demand) 재검증할 수 있습니다.

주문형 재검증은 어떻게 작동할까요? (How On-Demand Revalidation Works)

Diagram showing how on-demand revalidation works, the Data Cache is updated with fresh data after a revalidation request.

  • fetch 요청이 처음 호출되면, 외부 데이터 소스에서 데이터를 가져와 데이터 캐시에 저장합니다.
  • 주문형 재검증(On-demand revalidation)이 트리거되면, 해당 캐시 항목이 캐시 저장소에서 완전히 삭제(purge)됩니다.
    • 새로운 최신 데이터를 가져올 때까지 예전 데이터를 캐시에 유지하는 '시간 기반 재검증'과는 완전히 다른 방식이에요! 바로 지워버립니다.
  • 이후 다음번 요청이 들어오면 다시 캐시 MISS가 발생하고, 데이터를 외부 소스에서 새롭게 가져와 다시 데이터 캐시에 저장합니다.

💡 강사의 팁: 게시판에서 사용자가 '글쓰기' 버튼을 눌러 새 글을 등록했다고 가정해 볼게요. 시간 기반으로 해두면 1시간 뒤에나 새 글이 목록에 보이겠죠? 이럴 때 글이 등록되는 서버 액션(Server Action) 내부에서 revalidatePath('/board')를 딱 호출해 주면, 캐시가 즉시 날아가고 새 글이 포함된 최신 목록이 화면에 짜잔! 하고 나타나게 됩니다.

캐싱 끄기 (Opting out)

fetch의 응답을 캐시하고 싶지 않다면 다음과 같이 설정하면 됩니다:

// 매 요청마다 새로운 데이터를 가져옵니다.
let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })

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

관련 용어 (Related terms):

문서를 보다 보면 자동 정적 최적화 (Automatic Static Optimization), 정적 사이트 생성 (Static Site Generation), 또는 정적 렌더링 (Static Rendering) 이라는 용어들이 서로 섞여서 쓰이는 것을 볼 수 있습니다. 이들은 모두 빌드 타임에 애플리케이션의 라우트를 렌더링하고 캐시하는 과정을 가리킵니다.

Next.js는 빌드 타임에 라우트를 자동으로 렌더링하고 캐시합니다. 이는 들어오는 모든 요청마다 서버에서 렌더링을 새로 하는 대신 캐시된 라우트를 바로 서빙할 수 있게 해주는 최적화 기법으로, 결과적으로 페이지 로딩 속도가 비약적으로 빨라집니다.

전체 라우트 캐시가 어떻게 작동하는지 이해하려면, React가 렌더링을 어떻게 처리하고 Next.js가 그 결과를 어떻게 캐시하는지 살펴보는 것이 좋습니다:

1. 서버에서의 React 렌더링 (React Rendering on the Server)

서버에서 Next.js는 React의 API를 사용하여 렌더링 과정을 오케스트레이션(지휘)합니다. 렌더링 작업은 청크(chunks) 단위로 나뉘는데, 개별 라우트 세그먼트와 Suspense 바운더리를 기준으로 쪼개집니다.

각 청크는 두 단계로 렌더링됩니다:

  1. React는 서버 컴포넌트(Server Components)를 스트리밍에 최적화된 특수한 데이터 형식인 React Server Component Payload (RSC 페이로드) 로 렌더링합니다.
  2. Next.js는 이 RSC 페이로드와 클라이언트 컴포넌트(Client Component)의 JavaScript 명령어를 사용하여 서버에서 HTML을 렌더링합니다.

이는 화면을 그리기 위한 모든 작업이 끝날 때까지 응답을 기다릴 필요 없이, 작업이 완료되는 대로 조각조각 스트리밍(stream)해서 클라이언트로 보낼 수 있다는 뜻입니다.

React Server Component Payload (RSC 페이로드)란 무엇인가요?

RSC 페이로드는 렌더링된 React 서버 컴포넌트 트리를 압축된 형태의 이진(binary) 데이터로 표현한 것입니다. 클라이언트(브라우저)에 있는 React는 이 데이터를 사용해서 브라우저의 DOM을 업데이트합니다. RSC 페이로드에는 다음 내용이 포함됩니다:

  • 서버 컴포넌트가 렌더링된 결과물
  • 클라이언트 컴포넌트가 렌더링되어야 할 위치의 자리 표시자(Placeholders) 및 해당 JavaScript 파일에 대한 참조
  • 서버 컴포넌트에서 클라이언트 컴포넌트로 전달된 모든 props

더 자세한 내용은 서버 및 클라이언트 컴포넌트(Server Components) 문서를 참조하세요.

2. 서버에서의 Next.js 캐싱 (전체 라우트 캐시)

Default behavior of the Full Route Cache, showing how the React Server Component Payload and HTML are cached on the server for statically rendered routes.

Next.js의 기본 동작은 라우트의 렌더링 결과물 (RSC 페이로드와 HTML)을 서버에 캐시하는 것입니다. 이는 빌드 타임에 정적으로 렌더링되는 라우트나 재검증 중에 적용됩니다.

3. 클라이언트에서의 React 하이드레이션과 재조정 (React Hydration and Reconciliation on the Client)

요청이 들어왔을 때, 클라이언트(브라우저)에서는 다음과 같은 일이 일어납니다:

  1. 먼저 HTML이 사용되어 사용자와 상호작용은 불가능하지만 매우 빠른 초기 화면 미리보기를 즉시 표시합니다. (빠르게 화면이 뜹니다.)
  2. RSC 페이로드가 사용되어 클라이언트 트리와 이미 렌더링된 서버 컴포넌트 트리를 서로 맞추는 재조정(reconcile) 과정을 거치고 DOM을 업데이트합니다.
  3. JavaScript 명령어들이 사용되어 클라이언트 컴포넌트에 하이드레이션(hydrate)을 수행하고, 화면의 버튼 등을 클릭할 수 있는 상호작용(interactive) 가능한 상태로 만듭니다.

📝 강사의 보충 설명: 하이드레이션(Hydration)이란 메마른 HTML에 생명력(수분=JavaScript 이벤트)을 불어넣어 살아 숨 쉬게 만드는 과정이라고 생각하시면 됩니다!

4. 클라이언트에서의 Next.js 캐싱 (라우터 캐시)

RSC 페이로드는 클라이언트 측에 있는 라우터 캐시(Router Cache)에 저장됩니다. 이는 개별 라우트 세그먼트별로 나뉘어 있는 별도의 인메모리(in-memory) 캐시입니다. 이 라우터 캐시는 이전에 방문한 라우트를 저장하고 앞으로 이동할 라우트를 미리 가져와(prefetching) 페이지 이동 경험을 대폭 향상시키는 데 사용됩니다.

5. 후속 네비게이션 (Subsequent Navigations)

이후 사용자가 다른 페이지로 이동하거나 prefetching을 할 때, Next.js는 RSC 페이로드가 라우터 캐시에 저장되어 있는지 확인합니다. 만약 있다면, 서버에 새로운 요청을 보내지 않고 건너뜁니다. (완전 빠르겠죠?)

라우트 세그먼트가 캐시에 없는 경우에만 Next.js는 서버에서 RSC 페이로드를 가져와 클라이언트의 라우터 캐시를 채웁니다.

정적 및 동적 렌더링 (Static and Dynamic Rendering)

빌드 타임에 라우트가 캐시될지 여부는 해당 라우트가 정적으로 렌더링되는지 동적으로 렌더링되는지에 달려 있습니다. 정적 라우트는 기본적으로 캐시되지만, 동적 라우트는 요청 시간에 그때그때 렌더링되므로 캐시되지 않습니다.

이 다이어그램은 캐시된 데이터와 캐시되지 않은 데이터를 사용하여 정적 라우트와 동적 라우트의 차이점을 보여줍니다:

How static and dynamic rendering affects the Full Route Cache. Static routes are cached at build time or after data revalidation, whereas dynamic routes are never cached

정적 및 동적 렌더링에 대해 자세히 알아보세요.

유지 기간 (Duration)

기본적으로 전체 라우트 캐시(Full Route Cache)는 영구적(persistent)입니다. 즉, 렌더링 결과물이 사용자 요청 전반에 걸쳐 계속 캐시됩니다.

무효화 (Invalidation)

전체 라우트 캐시를 무효화(캐시를 깨는 것)할 수 있는 방법은 두 가지입니다:

  • 데이터 재검증 (Revalidating Data): 데이터 캐시를 재검증하면, 결과적으로 서버에서 컴포넌트를 다시 렌더링하고 그 새로운 렌더링 결과물을 캐시하게 되므로 라우터 캐시도 무효화됩니다.
  • 재배포 (Redeploying): 배포 간에도 유지되는 데이터 캐시와 달리, 전체 라우트 캐시는 새로운 배포가 이루어지면 완전히 지워집니다(cleared).

캐싱 끄기 (Opting out)

전체 라우트 캐시에서 벗어나고 싶다면, 즉 들어오는 모든 요청에 대해 컴포넌트를 동적으로 렌더링하고 싶다면 다음과 같은 방법을 쓸 수 있습니다:

  • 동적 API (Dynamic API) 사용: cookies()headers() 등을 사용하면 라우트는 전체 라우트 캐시 대상에서 제외되며 요청 시간에 동적으로 렌더링됩니다. (물론 이 경우에도 데이터 캐시는 여전히 사용할 수 있습니다.)
  • dynamic = 'force-dynamic' 또는 revalidate = 0 라우트 세그먼트 옵션 사용: 이 옵션을 걸면 전체 라우트 캐시와 데이터 캐시를 모두 건너뜁니다. 즉, 서버에 요청이 올 때마다 매번 컴포넌트가 렌더링되고 데이터를 새로 가져옵니다. 단, 라우터 캐시는 클라이언트 측 캐시이므로 여전히 적용됩니다.
  • 데이터 캐시(Data Cache) 끄기: 라우트 내에 캐시되지 않는 fetch 요청이 하나라도 있다면, 해당 라우트는 전체 라우트 캐시에서 제외됩니다. 캐시되지 않은 해당 특정 데이터는 매 요청마다 새로 가져오게 됩니다. 반면 캐싱을 명시적으로 설정한 다른 fetch 요청들은 여전히 데이터 캐시에 저장됩니다. 이렇게 하면 캐시된 데이터와 캐시되지 않은 데이터가 혼합된 형태(hybrid)를 구성할 수 있습니다.

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

Next.js는 레이아웃, 로딩 상태, 페이지 등으로 나뉜 라우트 세그먼트의 RSC 페이로드를 저장하는 메모리 내(in-memory) 클라이언트 측 라우터 캐시를 가지고 있습니다.

사용자가 경로 간을 이동할 때, Next.js는 방문한 라우트 세그먼트를 캐시하고 사용자가 이동할 가능성이 높은 경로를 미리 가져옵니다(prefetching). 그 결과 뒤로가기/앞으로가기 시 페이지가 즉각적으로 전환되고, 이동 간에 전체 페이지 새로고침이 발생하지 않으며, 공유 레이아웃의 브라우저 상태와 React 상태가 그대로 보존됩니다.

라우터 캐시 덕분에 다음과 같은 효과를 얻을 수 있습니다:

  • 레이아웃(Layouts) 은 네비게이션 시 캐시되고 재사용됩니다 (부분 렌더링 (partial rendering)).
  • 로딩 상태(Loading states) 는 네비게이션 시 캐시되고 재사용되어 즉각적인 로딩 화면(instant loading states)을 보여줍니다.
  • 페이지(Pages) 는 기본적으로 캐시되지 않지만 브라우저의 뒤로가기 및 앞으로가기 네비게이션 중에는 재사용됩니다. 실험적인 staleTimes 설정 옵션을 사용하면 페이지 세그먼트에 대한 캐싱을 명시적으로 활성화할 수도 있습니다.

알아두면 좋은 점: 이 캐시는 Next.js와 서버 컴포넌트에 특별히 적용되는 것이며, 결과는 비슷해 보여도 브라우저 고유의 bfcache 기능과는 다릅니다.

유지 기간 (Duration)

이 캐시는 브라우저의 임시 메모리에 저장됩니다. 라우터 캐시가 얼마나 오래 유지되는지는 두 가지 요소에 의해 결정됩니다:

  • 세션 (Session): 캐시는 이동(Navigation) 간에는 유지되지만, 페이지를 강제로 새로고침(F5)하면 모두 지워집니다.
  • 자동 무효화 기간 (Automatic Invalidation Period): 레이아웃과 로딩 상태의 캐시는 특정 시간이 지나면 자동으로 무효화됩니다. 이 기간은 리소스를 어떻게 미리 가져왔는지(prefetched), 그리고 그 리소스가 정적으로 생성(statically generated) 되었는지 여부에 따라 다릅니다:
    • 기본 Prefetching (prefetch={null} 또는 설정 안 함): 동적 페이지는 캐시 안 됨, 정적 페이지는 5분 동안 유지.
    • 전체 Prefetching (prefetch={true} 또는 router.prefetch 호출): 정적, 동적 페이지 모두 5분 동안 유지.

페이지 새로고침은 캐시된 모든 세그먼트를 날려버리지만, 자동 무효화 기간은 미리 가져온 그 시점부터 계산되어 개별 세그먼트에만 영향을 미칩니다.

알아두면 좋은 점: 실험적인 기능인 staleTimes 설정 옵션을 사용하면 방금 말씀드린 자동 무효화 시간을 입맛에 맞게 조절할 수 있습니다.

무효화 (Invalidation)

라우터 캐시를 무효화하는 방법은 두 가지가 있습니다:

  • 서버 액션(Server Action) 내부에서:
    • 경로 기반(revalidatePath) 또는 캐시 태그 기반(revalidateTag)으로 데이터를 주문형(on-demand)으로 재검증할 때.
    • cookies.set 또는 cookies.delete를 사용하여 쿠키를 변경할 때. 이렇게 하면 로그인 인증과 같이 쿠키를 사용하는 라우트가 이전 데이터를 보여주는 것을 방지하기 위해 라우터 캐시가 무효화됩니다.
  • router.refresh를 호출할 때. 이 함수를 부르면 즉시 라우터 캐시가 무효화되고 현재 경로에 대해 서버로 새로운 요청을 보냅니다.

💡 강사의 팁: SPA(Single Page Application) 개발하실 때 화면 데이터가 안 바뀌어서 수동으로 리렌더링 트리거 해보신 적 있죠? Next.js App 라우터에서는 클라이언트 컴포넌트에서 const router = useRouter()를 선언하고 router.refresh()를 호출해주면 손쉽게 최신 서버 데이터를 가져오면서 화면을 갱신할 수 있답니다.

캐싱 끄기 (Opting out)

Next.js 15부터 페이지 세그먼트의 클라이언트 캐싱은 기본적으로 꺼져(opt-out) 있습니다.

알아두면 좋은 점: <Link> 컴포넌트의 prefetch prop을 false로 설정하면 미리 가져오기(prefetching) 동작도 끌 수 있습니다.

캐시 상호작용 (Cache Interactions)

여러 캐싱 메커니즘을 설정할 때, 이들이 서로 어떻게 상호작용하는지 이해하는 것은 매우 중요합니다:

데이터 캐시와 전체 라우트 캐시 (Data Cache and Full Route Cache)

  • 데이터 캐시를 재검증하거나 끄면, 전체 라우트 캐시도 반드시 무효화됩니다. 왜냐하면 렌더링 결과물은 데이터에 종속되어 있기 때문이죠.
  • 전체 라우트 캐시를 무효화하거나 끈다고 해서 데이터 캐시에 영향을 미치지는 않습니다. 따라서 캐시된 데이터와 캐시되지 않은 데이터가 섞인 라우트를 동적으로 렌더링할 수 있습니다. 예를 들어, 페이지의 대부분은 캐시된 데이터를 사용하지만 몇몇 컴포넌트만 실시간으로 받아와야 하는 데이터에 의존할 때 아주 유용합니다. 이렇게 하면 모든 데이터를 다시 가져와야 하는 성능 부담 없이 동적 렌더링을 구현할 수 있죠.

데이터 캐시와 클라이언트 측 라우터 캐시 (Data Cache and Client-side Router cache)

  • 데이터 캐시와 라우터 캐시를 즉시 동시에 무효화하려면, 서버 액션(Server Action) 내부에서 revalidatePathrevalidateTag를 사용하면 됩니다.
  • 반면, 라우트 핸들러(Route Handler, API 라우트)에서 데이터 캐시를 재검증한다고 해서 라우터 캐시가 즉시 무효화되지는 않습니다. 라우트 핸들러는 특정 라우트 화면에 종속되어 있지 않기 때문이에요. 따라서 이 경우 사용자가 강제로 새로고침을 하거나 자동 무효화 기간이 지나기 전까지 라우터 캐시는 이전 페이로드를 계속 보여주게 됩니다.

API 모음 (APIs)

다음 표는 다양한 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> 컴포넌트는 전체 라우트 캐시에서 라우트를 자동으로 미리 가져와(prefetch) 그 RSC 페이로드를 클라이언트의 라우터 캐시에 추가합니다.

이 prefetching 동작을 끄려면 prefetch prop을 false로 설정하면 됩니다. 하지만 이렇게 한다고 해서 영구적으로 캐싱을 건너뛰는 것은 아니며, 사용자가 실제로 해당 라우트를 방문할 때는 클라이언트 측에 해당 세그먼트가 다시 캐시됩니다.

<Link> 컴포넌트에 대해 더 자세히 알아보세요.

router.prefetch

useRouter 훅의 prefetch 옵션을 사용하면 코드로 직접 라우트를 미리 가져올 수 있습니다. 이 동작 역시 RSC 페이로드를 라우터 캐시에 추가합니다.

useRouter API 레퍼런스를 참조하세요.

router.refresh

useRouter 훅의 refresh 메서드는 현재 라우트를 수동으로 새로고침할 때 사용합니다. 이 메서드를 호출하면 라우터 캐시가 완전히 지워지고 서버에 현재 라우트에 대한 새로운 요청을 보냅니다. 중요한 건 refresh는 데이터 캐시나 전체 라우트 캐시에는 영향을 주지 않는다는 점입니다.

서버에서 받아온 렌더링 결과물은 클라이언트에서 기존의 React 상태(state)나 브라우저 상태를 유지하면서 부드럽게 재조정(reconcile)됩니다.

useRouter API 레퍼런스를 참조하세요.

fetch

fetch에서 반환된 데이터는 자동으로 데이터 캐시에 들어가는 것이 아닙니다. (과거 버전과 헷갈리시면 안 돼요!)

cache 옵션이나 next.revalidate 옵션을 명시적으로 주지 않은 경우, 기본 동작은 렌더링 방식에 따라 다릅니다:

더 많은 옵션은 fetch API 레퍼런스를 확인하세요.

fetch options.cache

개별 fetch 요청에 대해 명시적으로 캐싱을 켜고 싶다면 cache 옵션을 force-cache로 설정하면 됩니다:

// 캐싱 사용(opt into caching) 명시
fetch(`https://...`, { cache: 'force-cache' })

더 많은 옵션은 fetch API 레퍼런스를 확인하세요.

fetch options.next.revalidate

개별 fetch 요청의 재검증 주기(초 단위)를 설정하려면 fetchnext.revalidate 옵션을 사용하세요. 이렇게 하면 데이터 캐시가 재검증되고, 이는 곧 전체 라우트 캐시의 재검증으로 이어집니다. 새 데이터를 다시 가져오고 서버에서 컴포넌트가 새롭게 렌더링되게 되죠.

// 최대 1시간이 지나면 데이터를 재검증합니다.
fetch(`https://...`, { next: { revalidate: 3600 } })

더 많은 옵션은 fetch API 레퍼런스를 확인하세요.

fetch options.next.tagsrevalidateTag

Next.js는 더욱 세밀한 데이터 캐싱과 재검증을 위해 캐시 태그(tagging) 시스템을 제공합니다.

  1. fetchunstable_cache를 사용할 때, 캐시 항목에 하나 이상의 태그를 지정할 수 있습니다.
  2. 그런 다음, 나중에 특정 태그가 달린 캐시 항목들을 싹 날려버리기 위해(purge) revalidateTag 함수를 호출할 수 있습니다.

예를 들어, 데이터를 가져올 때 태그를 설정하려면:

// 여러 태그를 달아서 데이터를 캐시합니다.
fetch(`https://...`, { next: { tags: ['a', 'b', 'c'] } })

그런 다음, 특정 태그가 달린 데이터를 재검증(무효화) 하려면:

// 'a' 태그가 달린 캐시 항목들을 재검증합니다.
revalidateTag('a')

여러분의 목적에 따라 두 가지 위치에서 revalidateTag를 호출할 수 있습니다:

  1. 라우트 핸들러 (Route Handlers) - 외부 이벤트(예: Webhook 등)의 응답으로 데이터를 재검증할 때. 라우트 핸들러는 특정 라우트에 종속되지 않으므로, 이 방식은 클라이언트의 라우터 캐시를 즉각 무효화하지는 않습니다.
  2. 서버 액션 (Server Actions) - 사용자 액션(예: 폼 제출, 버튼 클릭) 직후에 데이터를 재검증할 때. 이 방식은 해당 라우트와 연관된 클라이언트의 라우터 캐시도 함께 무효화해 줍니다. 화면이 즉시 갱신되겠죠!

revalidatePath

revalidatePath 함수를 사용하면 단 한 번의 작업으로 특정 경로 아래에 있는 데이터 캐시를 수동으로 재검증하면서 동시에 라우트 세그먼트들을 다시 렌더링하게 만들 수 있습니다. revalidatePath를 호출하면 데이터 캐시가 무효화되고, 결과적으로 전체 라우트 캐시까지 무효화됩니다.

// 홈 경로('/')를 재검증합니다.
revalidatePath('/')

revalidatePath 역시 달성하고자 하는 목표에 따라 두 곳에서 사용할 수 있습니다:

  1. 라우트 핸들러 (Route Handlers) - 외부 이벤트(Webhook 등) 응답 시 데이터 재검증.
  2. 서버 액션 (Server Actions) - 사용자 인터랙션(폼 제출 등) 후 데이터 재검증.

더 자세한 정보는 revalidatePath API 레퍼런스를 참고하세요.

revalidatePath vs. router.refresh:

router.refresh를 호출하면 라우터 캐시를 지우고 서버에서 라우트 세그먼트를 다시 렌더링하지만, 데이터 캐시나 전체 라우트 캐시는 건드리지 않습니다 (무효화하지 않음).

차이점을 요약하자면, revalidatePath는 백엔드(서버) 쪽에 있는 데이터 캐시와 전체 라우트 캐시를 싹 날려버리는 강력한 방법이고, router.refresh()는 클라이언트 측 API로서 서버의 캐시는 그대로 둔 채 브라우저 쪽 화면 상태만 새로고침하는 것입니다.

동적 API (Dynamic APIs)

cookies, headers 같은 동적 API나 페이지에서 사용하는 searchParams prop은 런타임에 들어오는 요청의 정보에 의존합니다. 이런 API들을 컴포넌트 내부에서 사용하면 전체 라우트 캐시에서 빠져나가게 되며(opt out), 다시 말해 라우트가 사용자 요청 시마다 동적으로 렌더링(dynamically rendered)됩니다.

cookies

서버 액션 내부에서 cookies.set이나 cookies.delete를 사용하면 쿠키 정보를 사용하는 라우트가 예전 화면을 보여주지 않도록 하기 위해(예: 로그인/로그아웃 상태 반영) 라우터 캐시가 자동으로 무효화됩니다.

cookies API 레퍼런스를 참조하세요.

세그먼트 설정 옵션 (Segment Config Options)

라우트 세그먼트 설정(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) 조건에 매칭되게 됩니다.

React 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 를 확인해 보세요.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글