

App Router 프로젝트에서 Router cache로 인한 이슈가 들어왔다. 사용자가 변경한 옵션이 다른 라우트에 즉시 반영되지 않고 지연이 발생한 문제였다. 공식문서를 읽고 Next.js의 캐싱 메커니즘에 대해 알아보자.🔍
Next.js는 기본적으로는 데이터 요청과 렌더링을 캐싱하여 퍼포먼스를 향상시킨다. Server 단에서 관리되는 Full Route Cache, Request Memoization, Data Cache와 Client 단에서 관리되는 Router Cache까지 4가지 캐싱 메커니즘이 있다.


Next.js에서 fetch API를 사용하면 동일한 URL과 옵션을 가진 요청을 자동으로 메모이제이션한다. React component 트리 여러 곳에서 같은 데이터를 호출하면 네트워크 요청은 1번만 발생한다.

첫 요청 시 데이터가 메모리에 없으면 외부에서 데이터를 가져와 저장하고, 이후 동일한 요청은 메모리에서 바로 반환된다. 라우트 렌더링이 끝나면 메모리는 초기화되고 메모이제이션된 데이터는 삭제된다.
async function getItem() {
// The `fetch` function is automatically memoized and the result
// is cached
const res = await fetch('https://.../item/1')
return res.json()
}
// This function is called twice, but only executed the first time
const item = await getItem() // cache MISS
// The second call could be anywhere in your route
const item = await getItem() // cache HIT
기존 Page Router에서는 트리 최상단에서 데이터를 불러와 데이터를 사용하는 모든 컴포넌트에 props로 전달해야 했다. 반면, App Router에서는 필요한 컴포넌트에서 데이터를 바로 호출하고, 라우트 렌더링 중 데이터 호출이 여러번 발생해도 Request Memoization을 통해 중복 네트워크 요청을 방지한다. 따라서 데이터 흐름이 단순해지고, 컴포넌트 간 결합도를 줄여 직관적인 코드를 작성할 수 있게 된다.
if (
(request.method !== 'GET' && request.method !== 'HEAD') ||
request.keepalive
) {
// We currently don't dedupe requests that might have side-effects.
return originalFetch(resource, options);
}
cacheKey = generateCacheKey(request);
generateMetadata, generateStaticParams, Layouts, Pages, 그리고 기타 서버 컴포넌트에서만 적용된다. Route handler에서는 작동하지않는다.
fetch 를 사용할 수 없는 경우, React의 cache 함수를 이용해 메모이제이션 할 수 있다. (Next.js — React cache function)
메모이제이션은 서버 요청이 처리되는 동안만 유지된다. 그리고 컴포넌트 트리의 렌더링이 끝나면 메모리가 초기화된다.
메모이제이션은 React의 기본동작이기 때문에 해제하는 것을 권장하지않는다.
Next.js는 fetch API로 요청한 데이터를 캐시하여, 서버 요청과 배포 과정에서 해당 데이터를 공유한다. cache와 next.revalidate 옵션을 사용해서 동작을 설정할 수 있다.

force-cache 옵션이 붙은 첫 번째 요청이 렌더링 중에 실행될 때, 데이터 캐시에서 이미 저장된 데이터를 찾는다.
캐시된 데이터가 있다면, 즉시 반환하고 메모리에 저장한다.
캐시된 데이터가 없다면, 데이터를 외부 소스에서 받아오고 그 결과를 메모리에 저장한다.
cache 옵션이 없거나 { cache: 'no-store' }와 같이 설정한 경우에는 항상 데이터를 외부 소스에서 새로 받아오며, 그 결과를 메모리에 저장한다.
(여기서 메모는 Request Memoization을 의미한다.)
데이터가 캐시되었든 아니든, React 렌더 패스에서 같은 데이터에 대한 중복 요청을 피하기 위해 항상 메모리에 저장한다.
Data cache는 revalidate 혹은 opt-out하지않는 이상 유지된다.

stale-while-revalidate 와 유사한 방식으로 일정 시간이 지나면 데이터를 자동으로 재검증한다.
// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })

revalidatePath, revalidateTag 와 같은 API를 사용해 on-demand 방식으로 재검증한다.
데이터를 캐시하고 싶지않다면 no-cache 옵션을 설정할 수 있다.
let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })
Next.js는 빌드 타임에 자동으로 라우트를 렌더링하고 캐시하여, 페이지 로딩 속도를 향상시킨다. Full Route Cache의 동작을 이해하려면, React가 렌더링을 처리 방식과 Next.js가 결과를 캐싱하는 방식을 살펴보는 것이 도움이 된다.
서버에서 Next.js는 React의 API를 사용하여 렌더링을 관리한다. 렌더링 작업은 개별 라우트 세그먼트, Suspense 바운더리를 기준으로 분할된다.
각 청크는 두 단계로 렌더링 된다.
모든 작업이 렌더링될 때까지 기다리지않고, 작업이 완료되는대로 결과를 스트리밍해서 응답한다.
RSC Payload(React Server component Payload란?
렌더링된 React 서버 컴포넌트 트리를 압축된 이진 형식으로 표현한 것으로, 클라이언트에서 React가 브라우저의 DOM을 업데이트할 때 사용된다. RSC Payload의 구성 요소는 다음과 같다.

Next.js는 라우트의 렌더링 결과(RSC Payload, HTML)를 서버에 캐시하고 빌드타임에 라우트를 정적으로 렌더링할 때, 혹은 재검증할 때 적용한다.
HTML immediately show
HTML을 사용해 클라이언트와 서버 컴포넌트의 비상호작용적(non-interactive)인 초기화면을 빠르게 보여준다.
Reconcile
RSC Payload를 사용해 클라이언트와 렌더링된 서버컴포넌트 트리를 재조정하고 DOM을 업데이트한다.
Hydration
Javascript instructions를 사용해 클라이언트 컴포넌트를 상호작용 가능한 어플리케이션으로 만든다.
RSC Payload는 Client-side Router Cache(라우트 세그먼트별로 분리된 메모리 캐시)에 저장된다. Router Cache는 이전에 방문한 라우트를 저장하고, 이동할 가능성이 있는 라우트를 prefetch해서 네비게이션 사용성을 개선한다.
후속 네비게이션 혹은 prefetching 과정에서 Next.js는 Router Cache에 RSC Payload가 저장되어있는지 확인한다. 만약 저장되어있으면, 새로운 서버 요청은 건너뛴다. 저장되어있지않다면, 서버에서 RSC Payload를 가져와 저장한다.

Full Route Cache는 여러 사용자 요청에 걸쳐 서버에서 RSC Payload와 HTML을 지속적으로 저장한다. 이 캐시는 정적 라우트에만 적용된다.
Router Cache는 사용자의 세션 동안 브라우저에 RSC Payload를 임시로 저장하며, 정적 라우트와 동적 라우트 모두에 적용된다.
정적/동적 라우트의 구분은 Dynamic APIs 사용 여부와 Data 캐시 여부에 따라 달라진다.

기본적으로 Full Route Cache는 지속적이며, 렌더링 결과물이 사용자 요청 간에도 캐시된다.
Full Route Cache를 무효화하는 두가지 방법이 있다.
모든 컴포넌트를 동적으로 렌더링하고 Full Router Cache에서 제외시키려면다음 방법을 사용할 수 있다.
dynamic = 'force-dynamic', revalidate = 0 route segment 옵션 사용fetch(..., {cache: 'no-store'}) data cache opt-outNext.js has an in-memory client-side router cache that stores the RSC payload of route segments, split by layouts, loading states, and pages.
공식문서에서는 Router cache를 “in-memory client-side router cache”라고 칭한다. 여기서 “in-memory”란 데이터를 하드 디스크와 같은 저장 장치가 아니라 메모리(RAM)에 저장한다는 개념으로 빠르게 데이터를 읽고 쓸 수 있다는 장점과 휘발된다는 단점이 있다.
Router cache는 라우트 세그먼트의 RSC(Route Segment Cache) Payload를 layouts, loading states, pages별로 분리해서 저장한다.

Next.js는 사용자가 이미 방문한 라우트 세그먼트를 캐시하고, 이동할 가능성이 있는 라우트 세그먼트들을 prefetch한다. 이로 인해 즉각적인 앞으로 가기/뒤로가기, 풀페이지 새로고침 없는 네비게이션이 가능하며, React와 브라우저의 상태를 보존한다. (브라우저의 BF cache와 다르지만 비슷한 효과를 낸다.)
Router Cache는 브라우저의 임시메모리에 저장되며, 캐시 지속 기간은 Session과 Automatic Invalidation Period라는 2가지 변수에 의해 제어된다.

Router cache를 무효화하는 방법은 아래와 같다.
Next.js 15부터는 페이지 세그먼트가 기본적으로 Router Cache에서 제외된다.
캐싱 매커니즘을 설정할 때, 각 캐시 간의 상호작용을 이해하는 것이 중요하다.
이번 이슈는 Next.js 14의 Router Cache 기본 30초 캐시 설정으로 인해, 사용자가 변경한 옵션이 다른 라우트에 즉시 반영되지 않고 지연이 발생하는 문제였다. revalidatePath를 사용해도 간헐적으로 revalidation이 동작하지않았고, 결국 해당 라우트는 클라이언트 사이드에서 데이터를 호출하는 것으로 변경되었다.
However, this will still temporarily store the route segments for 30s to allow instant navigation between nested segments, such as tab bars, or back and forward navigation.
Next.js 14 cache
Next.js 15 버전부터는 Page Segment의 Router Cache 기본 30초 캐시 설정이 제거된다고 한다.
Client Router Cache no longer caches Page components by default
In Next.js 15, this flag still remains accessible, but we are changing the default behavior to have astaleTimeof0for Page segments. This means that as you navigate around your app, the client will always reflect the latest data from the Page component(s) that become active as part of the navigation.
Next.js 15 release note
15버전부터 Page Segement에 대한 staleTimes 이 0으로 설정되어, 앱 내에서 네비게이션 할 때마다 클라이언트가 항상 최신 데이터를 반영한 페이지 컴포넌트를 표시한다. 다만 아래 동작은 유지된다.
14 버전과 같은 Client Route Cache 동작을 유지하고싶다면 아래 설정을 추가할 수 있다.
const nextConfig = {
experimental: {
staleTimes: {
dynamic: 30,
},
},
};
export default nextConfig;
공식 문서를 통해 Next.js 어플리케이션에서 캐싱이 여러 단계에 걸쳐 관리된다는 점을 알게 되었다. 모든 세부 사항을 다 외우기는 어렵겠지만, 문서를 한 번 읽어봤기 때문에 유사한 이슈가 발생하더라도 원인을 빠르게 파악할 수 있을 것 같다. 그리고 앞으로 프로젝트를 설계할 때, 각 페이지의 특성에 맞게 Next.js의 캐시 기능을 적극적으로 활용해보려고 한다.
추가적으로 궁금한 점은 다음과 같다.
이 부분은 추후 다른 글에서 다뤄볼 예정이다.