기술은 나의 도구

Next.js (14.1.0) 캐싱에 대하여

원문 Caching in Next.js

Next.js 는 렌더링 작업 및 데이터 요청을 캐싱하여 어플리케이션 성능을 개선할 뿐만 아니라 비용을 줄여줍니다. 이 포스팅에서는 Next.js 캐싱 메커니즘과 캐싱을 구성하는데 사용할 수 있는 API, 그리고 서로 상호작용하는 방식에 대해 자세히 살펴보도록 하겠습니다.

개요

기본적으로 Next.js 는 성능개선과 비용절감을 위해 최대한 캐싱합니다. 선택적으로 캐시를 사용하지 않기로 결정하지 않은 한 라우트는 정적으로 렌더링되고 데이터 요청은 캐싱이 된다는 의미입니다. 아래 다이어그램은 빌드 시 라우트가 정적으로 렌더링되고 정적 라우트에 최초 진입했을 때 기본 캐싱 동작을 보여줍니다.

caching-overview

캐싱동작은 라우트가 어떻게 렌더링되는지 (정적, 동적), 데이터가 캐시되는지 아닌지 여부, 요청이 초기 방문의 일부인지 후속탐색의 일부인지에 따라 달라집니다. 사용 케이스에 따라 개별 라우팅과 데이터 요청에 대한 캐싱 동작을 다르게 구성할 수 있습니다.

Request Memoization

React 는 동일한 URL과 옵션을 가진 요청을 자동으로 메모이제이션하여 fetch API 를 확장합니다. React 컴포넌트 트리의 여러 곳에서 fetch 함수를 호출한다 하더라도 내부적으로는 메모이제이션하여 한번만 요청을 함으로써 동일한 데이터를 가져올 수 있다는 뜻입니다.

deduplicated-fetch-requests

예를 들어, 같은 경로(예: 레이아웃, 페이지 및 여러 컴포넌트)에서 동일한 데이터를 사용해야 하는 경우 트리의 최상단에서 props 으로 하위 컴포넌트에 전달할 필요가 없습니다. 동일한 데이터를 네트워크를 통해 다중 요청하는 것에 대한 성능 영향에 대한 걱정없이 데이터를 요청할 수 있습니다.

async function getItem() {
  // fetch 함수는 자동으로 결과를 메모하고 캐시합니다.
  const res = await fetch('https://.../item/1')
  return res.json()
}

// getItem 함수는 두번 요청되지만 한번 실행됩니다.
const item = await getItem() // cache MISS (cache 에 없는 상태)

// 두번째 요청은 같은 경로의 어디에서든 가능합니다.
const item = await getItem() // cache HIT (cache 에 있는 상태)

Request Memoization 이 동작하는 법

request-memoization

  1. 경로가 렌더링 되는 동안 특정 요청이 처음 호출되면 그 결과는 메모리에 있지 않고 cache MISS 상태일 것입니다.
  2. 그러므로 함수는 실행이 되고 외부소스에서 데이터를 가져와서 메모리에 저장합니다.
  3. 동일한 경로에서 같은 함수가 요청이 되면 cache HIT 상태이므로 함수를 실행하지 않고 메모리로 부터 데이터를 전달받습니다.
  4. 경로가 렌더링 되고 렌더링 패스 (화면에 구성요소가 그려지는 과정) 가 완료되면 메모리는 리셋되고 모든 요청 메모이제이션 항목이 지워집니다.

알아두면 좋아요

  • Request Memoization 은 Next.js 가 아닌 React 의 기능입니다. 다른 캐싱 메커니즘과 어떻게 상호작용하는지 보여주기 위해 여기에 포함시켰습니다.
  • Memoization 은 fetch 요청의 GET 메소드에만 적용됩니다.
  • Memoization 은 React 컴포넌트 트리에만 적용됩니다. generateMetadata, generateStaticParams, Layouts, Pages, 그리고 Server Components 에는 적용되지만 React 컴포넌트 트리의 일부가 아닌 Router Handler 에는 적용되지 않습니다.
  • fetch 가 적합하지 않은 경우 (일부 Database, CMS Clients, GraphQL clients 등) React cache function 으로 함수를 메모할 수 있습니다.

Duration

캐시는 React component tree 렌더링이 끝나는 서버요청 수명동안 지속합니다.

Revalidating

Memoization 은 서버요청 간에 공유되지 않고 렌더링되는 동안만 적용이 되기때문에 revalidate 할 필요가 없습니다.

Opting out

fetch 요청에서 memoization 을 하지 않기 위해서는 AbortController signal 을 요청에 넘기면 됩니다.

const { signal } = new AbortController()
fetch(url, { signal })

Data Cache

Next.js 는 서버 요청, 배포를 통해 들어오는 데이터 결과를 유지하는 Data Cache 가 내장되어 있습니다. 이는 Next.js 가 native fetch API 를 확장하여 서버 상의 각 요청이 자체 영구 캐싱 규칙을 설정할 수 있게 함으로써 가능합니다.

알아두면 좋아요 브라우저에서 fetch 의 cache 옵션은 어떻게 요청이 브라우저의 HTTP cache 와 상호작용할지를 지시하지만 Next.js 에서는 어떻게 서버측 요청이 서버의 Data Cache 와 상호작용할지를 지시합니다.

기본적으로 fetch 를 사용하는 데이터 요청은 캐시처리됩니다. cache 나 next.revalidate 옵션으로 캐시 동작을 구성할 수도 있습니다.

How the Data Cache Works

data-cache

  1. 렌더링되는 동안 최초로 fetch 요청이 호출되면 Next.js 는 Data Cache 에서 캐시된 응답이 있는지 확인합니다.
  2. 캐시된 응답이 확인되면 즉시 반환되고 메모합니다.
  3. 캐시된 응답이 발견되지 않으면 데이터 소스에 요청해 결과를 Data Cache 에 저장 후 메모합니다.
  4. 캐시되지 않은 데이터가 필요하다고 설정할 경우 (e.g {cache: 'no-store'}) 결과는 항상 데이터소스에서 가져와 메모합니다.
  5. 데이터가 캐시되든, 캐시되지 않든 요청은 항상 메모하여 React 렌더링 패스가 진행되는 동안 동일한 데이터에 대한 중복요청을 피할 수 있습니다.

Data Cache 와 Request Memoization 의 차이점 두 캐싱 메커니즘 모두 캐시된 데이터를 재사용하여 성능을 개선하지만 Data Cache 는 들어오는 요청과 배포를 걸쳐 영구적이며, Request Memoization 은 요청의 수명동안만 지속됩니다. Memoization 으로 동일한 렌더링 패스 안에서 렌더링 서버에서 Data Cache 서버 (e.g. CDN, Edge) 나 데이터 소스 (e.g. database, CMS) 로의 네트워크를 통한 중복 요청의 수를 줄일 수 있고 Data Cache 로 원본 데이터 소스에 대한 요청의 수를 줄일 수 있습니다.

Duration

Data Cache 는 revalidate 하거나 사용하지 않겠다고 선택하지 않는 이상 들어오는 요청과 배포에 걸쳐 영구적입니다.

Revalidating

캐시된 데이터는 두가지 방식으로 재검증할 수 있습니다.

  • Time-based Revalidation: 일정 시간이 지난 후 새로운 요청이 이루어지면 데이터를 재검증합니다. 자주 변경되지 않거나 최신 데이터의 중요도가 낮은 경우에 유용합니다.
  • On-demand Revalidation: 이벤트 기반으로 데이터를 재검증합니다. (e.g. form submission) On-demand revalidation 은 태그 기반, 경로 기반 방식으로 한번에 그룹 데이터를 재검증할 때 사용할 수 있습니다. 가능한한 최신 데이터를 원할 때 유용합니다. (e.g. headless CMS 의 내용이 업데이트 될 때)

Time-based Revalidation

일정한 시간 간격으로 재검증하기 위해 fetch 함수의 next.revalidate 옵션으로 리소스의 캐시 수명(초단위)을 지정할 수 있습니다.

// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })

또는 특정 세그먼트 내의 모든 fetch 요청을 구성하거나 fetch 를 사용할 수 없는 경우 Route Segment Config options 을 사용할 수 있습니다.

Time-based Revalidation 동작 방식

time-based-revalidation

  1. revalidate 로 최초 fetch 요청을 하면 외부 데이터소스에서 가져온 데이터가 Data Cache 에 저장됩니다.
  2. 지정한 시간 내의 모든 요청은 캐시된 데이터를 반환합니다.
  3. 지정한 시간 이후의 다음 요청도 여전히 캐시된 데이터 (now stale) 를 반환합니다.
  4. Next.js 는 백그라운드에서 데이터검증을 작동시킵니다.
  5. 데이터를 성공적으로 가져오면 Next.js 는 신규 데이터로 Data Cache 를 갱신합니다.
  6. 백그라운드 재검증이 실패하면 이전 데이터는 변경되지 않은 상태로 유지됩니다.

위의 동작은 stale-while-revalidate 와 유사합니다.

On-demand Revalidation

데이터는 경로 (revalidatePath) 나 캐시 태그 (revalidateTag) 기반의 온디맨드 방식으로 재검증할 수 있습니다.

On-Demand Revalidation 동작 방식

on-demand-revalidation

  1. 최초 fetch 요청이 호출되면 데이터를 외부에서 가져와 Data Cache 에 저장합니다.
  2. On-demand revalidation 이 작동하면 해당 캐시 항목이 캐시에서 제거됩니다. Time-based revalidation 과 다르게 동작합니다.
  3. 다음 요청에서 캐시가 발견되지 않아 (cache MISS) 데이터를 외부에서 가져와 Data Cache 에 저장합니다.

Opting out

개별 데이터 요청의 cache 옵션을 no-store 로 지정할 수 있습니다. 이렇게 되면 fetch 가 호출될 때마다 데이터를 데이터 소스에서 가져옵니다.

// Opt out of caching for an individual `fetch` request
fetch(`https://...`, { cache: 'no-store' })

또한 특정한 경로 세그먼트에 대해 캐싱하지 않으려면 Route Segment Config options 을 사용할 수 있습니다. 이렇게 하면 타사 라이브러리를 포함하여 경로 세그먼트의 모든 데이터 요청에 영향을 미칩니다.

// Opt out of caching for all data requests in the route segment
export const dynamic = 'force-dynamic'

Vercel Data Cache Next.js 애플리케이션이 Vercel에 배포된 경우 Vercel Data Cache 문서를 통해 Vercel 특정 기능에 대해 더 잘 이해하는 것이 좋습니다.

Full Route Cache

관련 용어 Automatic Static Optimization, Static Site Generation, Static Rendering 이라는 용어는 빌드 시 어플리케이션의 라우트를 렌더링하고 캐싱하는 프로세스를 언급할 때 혼용해서 사용될 수 있습니다.

Next.js 는 빌드시점에 자동으로 라우트를 렌더링하고 캐싱합니다. 매 요청마다 서버에서 렌더링하는 대신 캐싱한 라우트를 제공하도록 최적화하여 페이지를 좀 더 빠르게 로딩할 수 있도록 합니다.

Full Route Cache 가 어떻게 동작하는지 이해하기 위해서 React 가 렌더링을 어떻게 다루는지, Next.js 가 렌더링 결과를 어떻게 캐싱하는지를 살펴보는 것이 도움이 됩니다.

1. React Rendering on the Server

서버에서 Next.js 는 React API 를 사용하여 렌더링을 조율합니다. 렌더링 작업은 개별 라우트 세그먼트와 Suspense 경계에 의해 chunk 로 분할됩니다.

각 chunk 는 두 단계로 렌더링됩니다.

  1. React 는 서버 컴포넌트를 스트리밍에 최적화된 특수한 데이터 포맷으로 렌더링하는데 이를 React Server Component Payload 라고 합니다.
  2. Next.js 는 React Server Component Payload 와 Client Component Javascript instructions 를 서버에서 HTML 을 렌더링하기 위해 사용합니다.

이는 모든 작업이 렌더링될 때까지 기다릴 필요없이 응답을 스트리밍할 수 있다는 의미입니다.

React Server Component Payload 란? React Server Component Payload 는 렌더링된 React Server Components tree 의 압축된 바이너리 형태입니다. React Client 에서 브라우저 DOM 을 업데이트하기 위해 사용됩니다. 다음을 포함하고 있습니다.

  • Server Component 의 렌더링된 결과
  • 클라이언트 컴포넌트가 렌더링될 위치에 대한 placeholder 와 해당 자바스크립트 파일에 대한 참조
  • Server Component 에서 Client Component 로 전달된 props

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

full-route-cache

Next.js 에서는 서버 상에서 라우트를 렌더링한 결과 (React Server Component Payload and HTML) 를 캐싱하는 것이 기본 동작입니다. 빌드시에 정적으로 렌더링된 라우트나 재검증중인 경로에 적용됩니다.

3. React Hydration and Reconciliation on the Client

요청 시에 클라이언트에서는 ..

  1. HTML 은 Client, Server Components 의 상호작용이 없는 초기 화면을 즉시 표시하는 데 사용됩니다.
  2. React Server Components Payload 는 Client 와 렌더링된 Server Component trees 를 조정하고 DOM 을 갱신하는 데 사용됩니다.
  3. JavaScript instructions 는 Client Components 를 활성화 (hydrate) 해서 어플리케이션이 상호작용할 수 있도록 합니다.

4. Next.js Caching on the Client (Router Cache)

React Server Component Payload 는 클라이언트 측 Router Cache (개별 라우트 세그먼트에 의해 분할 된 인메모리 캐시) 에 저장됩니다. Router Cache 는 이전에 방문한 경로와 프리페칭된 라우트를 저장해서 탐색 경험을 향상시킵니다.

5. Subsequent Navigations

이후의 탐색이나 프리페칭 중에 Next.js 는 React Server Components Payload 가 Router Cache 에 저장되어 있는지 확인해서 있다면 서버에 새로운 요청을 보내지 않습니다. 라우트 세그먼트가 캐시에 없는 경우 Next.js 는 서버에서 React Server Components Payload 를 가져와서 클라이언트 Router Cache 에 채웁니다.

Static and Dynamic Rendering

라우트가 빌드 시에 캐시될지 아닐지는 정적, 동적 렌더링 여부에 달려있습니다. 기본적으로 정적 라우트는 캐시하고 반면에 동적 라우트는 요청 시에 렌더링 되며 캐시되지 않습니다. 아래 다이어그램은 캐시된 데이터와 캐시되지 않은 데이터를 사용하여 정적으로 렌더링된 라우트와 동적으로 렌더링된 라우트의 차이를 보여줍니다

static-and-dynamic-routes

Duration

기본적으로 Full Route Cache 는 영구적으로 지속됩니다. 렌더링 결과물이 사용자의 요청으로 캐시된다는 의미입니다.

Invalidation

Full Route Cache 를 무효화할 수 있는 두가지 방법이 있습니다.

  • Revalidating Data: Data Cache 를 재검증하여 서버에서 컴포넌트를 다시 렌더링하고 새로운 렌더링 출력을 캐싱함으로써 Router Cache 를 무효화합니다.
  • Redeploying: 배포간에 지속되는 Data Cache 와 다르게 Full Route Cache 는 신규 배포시 초기화됩니다.

Opting out

Full Route Cache 를 선택하지 않는 법, 다시 말해 들어오는 매 요청마다 동적으로 컴포넌트를 렌더링 하는 법은 다음과 같습니다.

  • Dynamic Function 사용: 이렇게 하면 Full Route Cache 에서 경로를 선택 해제하고 요청 시점에 동적으로 렌더링합니다. Data Cache 는 계속 사용할 수 있습니다.
  • dynamic = 'force-dynamic' or revalidate = 0 route segment config options: 이렇게 하면 Full Route Cache 와 Data Cache 를 건너뜁니다. 즉, 서버로 들어오는 모든 요청에 대해 컴포넌트가 렌더링되고 데이터를 가져옵니다. Router Cache 는 클라이언트 측 캐시이므로 계속 적용됩니다.
  • Data Cache 사용하지 않기: 캐시되지 않은 라우트 요청이 있는 경우 Full Route Cache 에서 해당 경로를 제외합니다. 특정 요청에 대한 데이터는 모든 요청에 대해 fetch 됩니다. 캐싱하도록 설정된 요청은 여전히 데이터 캐시에 캐시됩니다. 이렇게 하면 캐시된 데이터와 캐시되지 않은 데이터를 혼합하여 사용할 수 있습니다.
  • #Nextjs
  • #Caching