Next.js의 4가지 캐싱 메커니즘

seo young park·2025년 4월 6일
1
post-thumbnail

App Router 프로젝트에서 Router cache로 인한 이슈가 들어왔다. 사용자가 변경한 옵션이 다른 라우트에 즉시 반영되지 않고 지연이 발생한 문제였다. 공식문서를 읽고 Next.js의 캐싱 메커니즘에 대해 알아보자.🔍


Next.js는 기본적으로는 데이터 요청과 렌더링을 캐싱하여 퍼포먼스를 향상시킨다. Server 단에서 관리되는 Full Route Cache, Request Memoization, Data Cache와 Client 단에서 관리되는 Router Cache까지 4가지 캐싱 메커니즘이 있다.

이미지 출처

1. Request Memoization

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

How Request Memoization Works

첫 요청 시 데이터가 메모리에 없으면 외부에서 데이터를 가져와 저장하고, 이후 동일한 요청은 메모리에서 바로 반환된다. 라우트 렌더링이 끝나면 메모리는 초기화되고 메모이제이션된 데이터는 삭제된다.

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을 통해 중복 네트워크 요청을 방지한다. 따라서 데이터 흐름이 단순해지고, 컴포넌트 간 결합도를 줄여 직관적인 코드를 작성할 수 있게 된다.

Good to know

  1. 메모이제이션은 React의 기능이다. React 소스 코드
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);
  1. generateMetadata, generateStaticParams, Layouts, Pages, 그리고 기타 서버 컴포넌트에서만 적용된다. Route handler에서는 작동하지않는다.

  2. fetch 를 사용할 수 없는 경우, Reactcache 함수를 이용해 메모이제이션 할 수 있다. (Next.js — React cache function)

duration

메모이제이션은 서버 요청이 처리되는 동안만 유지된다. 그리고 컴포넌트 트리의 렌더링이 끝나면 메모리가 초기화된다.

Opting out

메모이제이션은 React의 기본동작이기 때문에 해제하는 것을 권장하지않는다.

2. Data Cache

Next.js는 fetch API로 요청한 데이터를 캐시하여, 서버 요청과 배포 과정에서 해당 데이터를 공유한다. cachenext.revalidate 옵션을 사용해서 동작을 설정할 수 있다.

How the Data Cache Works

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

데이터가 캐시되었든 아니든, React 렌더 패스에서 같은 데이터에 대한 중복 요청을 피하기 위해 항상 메모리에 저장한다.

  • Data Cache와 Request Memoization의 차이점은?
    Request Memoization는 하나의 페이지를 렌더링하는 동안 존재하지만, Data Cache는 웹 서버로 들어오는 여러 요청(요청, 배포 등) 에 대해 유지된다.

Duration

Data cache는 revalidate 혹은 opt-out하지않는 이상 유지된다.

Revalidating

  1. Time-based Revalidation

stale-while-revalidate 와 유사한 방식으로 일정 시간이 지나면 데이터를 자동으로 재검증한다.

// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })
  • 첫 번째 요청은 외부 소스에서 받아오고 데이터를 캐시에 저장한다.
  • 일정 시간 내 요청이 들어오면 캐시된 데이터를 반환한다.
  • 일정 시간 이후 요청이 들어오면 캐시된 데이터를 반환하지만, 백그라운드에서 새로운 데이터를 가져와 업데이트한다.
  • 만약 데이터 갱신에 실패하면, 이전 데이터를 그대로 반환한다.
  1. On-demand Revalidation

revalidatePath, revalidateTag 와 같은 API를 사용해 on-demand 방식으로 재검증한다.

  • 첫 번째 요청은 외부소스에서 받아오고, 데이터를 캐시에 저장한다.
  • on-demand revalidation이 발생하면, 캐시된 데이터를 삭제된다.
    (데이터가 갱신될 때까지 오래된 데이터를 유지하 Time-based Revalidation와 다르다.)
  • 다음 요청이 발생하면, 데이터를 외부 소스에서 받아와 캐시에 저장한다.

Opting out

데이터를 캐시하고 싶지않다면 no-cache 옵션을 설정할 수 있다.

let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })

3. Full Route Cache

Next.js는 빌드 타임에 자동으로 라우트를 렌더링하고 캐시하여, 페이지 로딩 속도를 향상시킨다. Full Route Cache의 동작을 이해하려면, React가 렌더링을 처리 방식과 Next.js가 결과를 캐싱하는 방식을 살펴보는 것이 도움이 된다.

React Rendering on the Server

서버에서 Next.js는 React의 API를 사용하여 렌더링을 관리한다. 렌더링 작업은 개별 라우트 세그먼트, Suspense 바운더리를 기준으로 분할된다.

각 청크는 두 단계로 렌더링 된다.

  1. React는 서버 컴포넌트를 스트리밍에 최적화된 데이터 형식인 RSC Payload(React Server component Payload)로 렌더링한다.
  2. Next.js는 RSC Payload와 클라이언트 컴포넌트의 Javascript instruction을 사용하여 서버에서 HTML을 렌더링한다.

모든 작업이 렌더링될 때까지 기다리지않고, 작업이 완료되는대로 결과를 스트리밍해서 응답한다.

RSC Payload(React Server component Payload란?

렌더링된 React 서버 컴포넌트 트리를 압축된 이진 형식으로 표현한 것으로, 클라이언트에서 React가 브라우저의 DOM을 업데이트할 때 사용된다. RSC Payload의 구성 요소는 다음과 같다.

  • 서버 컴포넌트의 렌더링의 결과물
  • placeholder(클라이언트 컴포넌트를 렌더링할 위치에 대한 자리 표시, 해당 컴포넌트의 자바스크립트 파일 참조 위치)
  • 서버 컴포넌트에서 클라이언트 컴포넌트로 전달되는 Props

Next.js Caching on the Server (Full Route Cache)

Next.js는 라우트의 렌더링 결과(RSC Payload, HTML)를 서버에 캐시하고 빌드타임에 라우트를 정적으로 렌더링할 때, 혹은 재검증할 때 적용한다.

React Hydration and Reconciliation on the Client

  • HTML immediately show
    HTML을 사용해 클라이언트와 서버 컴포넌트의 비상호작용적(non-interactive)인 초기화면을 빠르게 보여준다.

  • Reconcile
    RSC Payload를 사용해 클라이언트와 렌더링된 서버컴포넌트 트리를 재조정하고 DOM을 업데이트한다.

  • Hydration
    Javascript instructions를 사용해 클라이언트 컴포넌트를 상호작용 가능한 어플리케이션으로 만든다.

Next.js Caching on the Client (Router Cache)

RSC Payload는 Client-side Router Cache(라우트 세그먼트별로 분리된 메모리 캐시)에 저장된다. Router Cache는 이전에 방문한 라우트를 저장하고, 이동할 가능성이 있는 라우트를 prefetch해서 네비게이션 사용성을 개선한다.

Subsequent Navigations

후속 네비게이션 혹은 prefetching 과정에서 Next.js는 Router Cache에 RSC Payload가 저장되어있는지 확인한다. 만약 저장되어있으면, 새로운 서버 요청은 건너뛴다. 저장되어있지않다면, 서버에서 RSC Payload를 가져와 저장한다.

Static and Dynamic Rendering


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

정적/동적 라우트의 구분은 Dynamic APIs 사용 여부와 Data 캐시 여부에 따라 달라진다.

Duration

기본적으로 Full Route Cache는 지속적이며, 렌더링 결과물이 사용자 요청 간에도 캐시된다.

Invalidation

Full Route Cache를 무효화하는 두가지 방법이 있다.

  • Revalidating Data
    Data Cache를 재검증하면 서버에서 컴포넌트를 리렌더링하면서 Router Cache도 무효화한다.
  • Redeploying
    Data Cache는 배포 간에도 유지되지만, Full Route Cache는 재배포 시 지워진다.

Opting out

모든 컴포넌트를 동적으로 렌더링하고 Full Router Cache에서 제외시키려면다음 방법을 사용할 수 있다.

  • [Dynamic APIs](Dynamic APIs 사용) 사용
  • dynamic = 'force-dynamic', revalidate = 0 route segment 옵션 사용
  • fetch(..., {cache: 'no-store'}) data cache opt-out

4. Router cache

Next.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별로 분리해서 저장한다.

  • layouts은 캐싱되며, 네비게이션할 때 재사용된다.
  • loading states도 캐싱되며, 네비게이션할 때 재사용된다.
  • pages는 기본적으로 캐시되지않지만, 브라우저 앞으로가기/뒤로가기 때는 재사용된다. (‘staleTimes’ 옵션을 통해 페이지 세그먼트 캐싱을 활성화할 수 있다.)

Next.js는 사용자가 이미 방문한 라우트 세그먼트를 캐시하고, 이동할 가능성이 있는 라우트 세그먼트들을 prefetch한다. 이로 인해 즉각적인 앞으로 가기/뒤로가기, 풀페이지 새로고침 없는 네비게이션이 가능하며, React와 브라우저의 상태를 보존한다. (브라우저의 BF cache와 다르지만 비슷한 효과를 낸다.)

duration

Router Cache는 브라우저의 임시메모리에 저장되며, 캐시 지속 기간은 Session과 Automatic Invalidation Period라는 2가지 변수에 의해 제어된다.

  • Session :캐시는 네비게이션 시에도 유지되지만, 새로고침하면 사라진다.
  • Automatic Invalidation Period: layouts와 loading states의 캐시는 특정 시간이 지나면 자동으로 무효화된다. 리소스를 어떻게 prefetch했는지, 그리고 리소스를 정적/동적으로 생성했는지에 따라 캐시 지속시간이 달라진다. (staleTimes 옵션을 사용하면 무효화 시간을 조정할 수 있다.)

Invalidation

Router cache를 무효화하는 방법은 아래와 같다.

  • revalidatePath(경로별), revalidateTag(태그별)로 데이터를 온디맨드로 재검증할 수 있다.
  • cookies.set, cookies.delete를 사용하면 쿠키를 사용하는 라우트가 오래된 데이터를 사용하지않도록 Router Cache를 무효화한다.
  • router.refresh를 사용하면 Router Cache가 무효화되며 현재 라우트에 대해 서버에 새로운 요청을 보낸다.

Opting out

Next.js 15부터는 페이지 세그먼트가 기본적으로 Router Cache에서 제외된다.

Cache Interactions

캐싱 매커니즘을 설정할 때, 각 캐시 간의 상호작용을 이해하는 것이 중요하다.

Data Cache and Full Route Cache

  • 렌더링 결과가 데이터에 의존하기 때문에, Data Cache를 무효화하거나 캐시에서 제외하면 Full Route Cache가 무효화된다.
  • 반대로 Full Route Cache를 무효화하거나 제외하더라도 Data Cache는 영향을 받지않는다. 캐시된 데이터와 최신 데이터가 혼합된 라우트를 동적으로 렌더링할 수 있다는 뜻이다.

Data Cache and Client-side Router Cache

  • Data Cache와 Router Cache를 즉시 무효화려면 revalidatePath, revalidateTag를 사용할 수 있다.
  • Route Handler에서는 Data Cache를 재검증해도 Router Cache가 무효화되지않는다. Route Handler가 특정 라우트와 묶여있지않기 때문이다. 따라서 hard refresh를 하거나 Automatic Invalidation Period가 지나기 전에는 기존 데이터를 계속 제공한다.

이슈 분석

이번 이슈는 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 a staleTime of 0 for 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으로 설정되어, 앱 내에서 네비게이션 할 때마다 클라이언트가 항상 최신 데이터를 반영한 페이지 컴포넌트를 표시한다. 다만 아래 동작은 유지된다.

  • 공유하고있는 layout 데이터는 서버에서 refetch하지않으며 partial rendering을 지원한다.
  • 뒤로가기/앞으로가기 네비게이션은 캐시에서 복원되며 브라우저가 스크롤 위치를 복원할 수 있다.
  • loading.js 는 여전히 5분 캐시된다.

14 버전과 같은 Client Route Cache 동작을 유지하고싶다면 아래 설정을 추가할 수 있다.

const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,
    },
  },
};
export default nextConfig;

마무리

공식 문서를 통해 Next.js 어플리케이션에서 캐싱이 여러 단계에 걸쳐 관리된다는 점을 알게 되었다. 모든 세부 사항을 다 외우기는 어렵겠지만, 문서를 한 번 읽어봤기 때문에 유사한 이슈가 발생하더라도 원인을 빠르게 파악할 수 있을 것 같다. 그리고 앞으로 프로젝트를 설계할 때, 각 페이지의 특성에 맞게 Next.js의 캐시 기능을 적극적으로 활용해보려고 한다.

추가적으로 궁금한 점은 다음과 같다.

  • Vercel 외의 배포 플랫폼(ex. Amplify 등)에서 서버 캐싱은 어떻게 동작하고 어느 범위까지 지원하는지?
  • Streaming HTML의 구체적인 동작 원리는 무엇인지?
  • 브라우저의 BF Cache는 어떻게 제어할 수 있는지?

이 부분은 추후 다른 글에서 다뤄볼 예정이다.

참고

0개의 댓글