NextJS 공식문서 파헤치기 (Caching)

0️⃣ Next.js의 캐싱 매커니즘 분류

매커니즘 설명 위치 목적 지속 기간
Request
Memoization
서버에서 함수의 반환 값을 메모이제이션 하여 저장한다. Server 리액트 컴포넌트 트리에서 데이터를 재사용하기 위함 요청 라이프 사이클 동안 (각 요청별로 유지)
Data Cache 서버에서 데이터 자체를 저장한다. Server 사용자 요청과 배포 간에 데이터를 유지하여 불필요한 데이터 재요청을 줄인다. 영구적이며, 필요에 따라 재검증(revalidation) 할 수 있다.
Full Route
Cache
서버에서 HTML과 RSC Payload를 캐시한다. Server 경로를 미리 렌더링하여 렌더링 비용을 줄이고 성능을 향상 시킨다. 영구적이며, 필요에 따라 재검증(revalidation) 할 수 있다.
Router Cache 클라이언트 측에서 RSC Payload를 캐시한다. Client 사용자가 페이지를 탐색할 때 서버 요청을 줄여 성능을 개선한다. 사용자가 브라우저를 종료하기 전까지 유지되거나, 지정한 시간이 지나면 갱신되는 방식
  • 기본적으로 Next.js는 성능을 개선하고 비용을 줄이기 위해 가능한 한 많이 캐시한다.
  • 경로는 정적으로 렌더링 되고, 데이터 요청은 옵트아웃 하지 않는 한(기본 설정을 거부하지 않는 한) 캐시된다.
  • 캐싱 동작은 경로가 정적으로 렌더링되는지 동적으로 렌더링되는지, 데이터가 캐시되는지 캐시되지 않는지, 요청이 초기 방문의 일부인지 후속 탐색의 일부인지에 따라 달라진다.
  • 아래는 Next.js의 기본 캐싱 동작을 나타낸 다이어그램이며, 빌드 시 경로가 정적으로 렌더링될 때와 정적 경로가 처음 방문될 때 이다.

1️⃣ Request Memoization

개요

  • React는 동일한 URL과 옵션이 있는 요청을 자동으로 메모하기 위해서 fetch API를 확장한다.
  • 따라서 React 컴포넌트 트리의 여러 곳에서 동일한 데이터에 대한 fetch 함수를 호출하면서도 한 번만 실행할 수 있다.
  • ex) 경로 전체에서 동일한 데이터를 사용해야하는 경우(레이아웃 페이지 및 여러 컴포넌트) 트리의 맨 위에서 데이터를 fetch 하고 컴포넌트 간에 props를 전달하지 않아도된다. 동일한 데이터에 대해서 여러 요청을 하는 성은에 대한 걱정 없이 데이터 페칭 가능
  • fetch API의 GET 메서드에만 적용된다. POST,DELETE 는 메모이제이션 되지 않음

async function getItem() {
  // The `fetch` function is automatically memoized and the result
  // is cached
  const res = await fetch('https://.../item/1')
  return res.json()
}

// 첫번째 실행에서는 원래대로 동작함 => cache MISS
const item = await getItem();

// 어느곳에서나 두번째 실행에서는 캐싱된 값을 사용홤 => cache HIT
const item = await getItem(); // cache HIT

Request Memoization 작동 방식

  1. 경로를 렌더링하는 동안 특정 요청이 처음 호출되면 결과는 메모리에 저장되지 않고 캐시에 저장된다. (cache MISS)
  2. 따라서 해당 함수가 실행되고, 외부 소스에서 데이터를 가져오고, 그 결과가 메모리에 저장된다.
  3. 동일한 렌더 패스에서 요청에 대한 후속 함수 호출은 캐시가 되고, 함수를 실행하지 않고도 메모리에서 데이터가 반환된다. (cache HIT)
  4. 경로가 렌더링되고 렌더링 패스가 완료되면 메모리가 '재설정'되고 모든 요청 메모 항목이 지워진다.
    (요청 메모이제이션 작동방식 사진 첨부하기)

Good to knows

  • Request Memoization은 Next.js 기능이 아닌 React 기능이다.
  • 메모이제이션은 fetch 요청의 GET 메서드에만 적용 된다.
  • 메모이제이션은 React 컴포넌트 트리에만 적용된다.
    • generateMetadata, generateStaticParams, Layouts, Pages, Server Components 에 적용 가능
    • Route Handlers 요청에는 적용되지 않는다 -> React 컴포넌트가 아니기 때문
  • fetch API 가 적합하지 않은 경우(some database clients, CMS clients, GraphQL clients) React cache 함수를 사용할 수 있다.

2️⃣ Data Cache

개요

  • Next.js에는 들어오는 서버 요청과 배포에서 데이터 페치 결과를 유지하는 내장형 데이터 캐시가 있다.
  • 이는 Next.js가 네이티브 fetch API를 확장하여 특정 요청의 캐싱 동작을 설정하거나, 가져온 데이터를 일정 기간 동안 저장(캐싱)해 둘 수 있다.
  • 이 캐시는 단순히 한 요청 사이클 동안만 유지되는 것이 아니라, 서버간 요청이나 배포 간에도 유지된다. 따라서 서버에 새 요청이 들어와도, 동일한 데이터에 대해 매번 새롭게 요청하지 않고 기존 데이터를 재사용한다.
  • fetch API를 통해 각 요청마다 캐싱 동작을 제어할 수 있다.
    • 데이터를 항상 최신 상태로 유지하고 싶다면 cache:'no-stroe' 로 설정하거나, revalidate를 짧게 설정한다.
    • 데이터를 자주 바뀌지 않는 것으로 간주하면 더 긴 기간 캐시를 유지한다.
    • 정적인 데이터라면 cahce:'force-cache' 로 설정한다.

데이터 캐시 작동 방식

  • 처음에 렌더링 중에 'force-cache' 옵션이 포함되어있는 fetch 요청이 호출된다면, Next.js는 데이터 캐시를 캐시된 응답이 있는지 체크한다.
  • 캐시된 응답을 찾았다면, 즉시 반환하고 메모화된다.
  • 캐시된 응답을 찾지 못했다면 데이터 소스에 요청이 이루어지고, 그 결과를 데이터 캐시에 저장하고 메모화 한다.
  • 캐시되지 않은 데이터(cache 옵션이 정의되지 않았거나 {cache:'no-store'} 옵션을 사용)의 경우 결과는 항상 데이터 소스에서 가져와서 메모화 된다. => Data Cache는 스킵된다.
  • 데이터가 캐시되든 캐시되지 않든, 요청은 항상 메모화 되어 React 렌더 단계 중에 동일한 데이터에 대한 중복 요청이 발생하지 않도록 한다.

Data Cache와 Request Memoization의 차이점

  • Data Cache는 들어오는 요청과 배포 전체에서 영구적으로 유지된다.
  • Request Memoization는 요청의 수명 동안만 지속된다.

재검증(Revalidating)

 

  • Time-based Revalidation
    • 일정 시간이 지나고 새로운 요청이 이루어진 후 데이터를 재검증한다. (변경 빈도가 낮고 신선도가 그렇게 중요하지 않은 데이터에 유용)
    • fetch 에서 리소스의 캐시 수명을 설정하는 next.revalidate 옵션을 사용한다.
    • fetch를 사용하지 못하는 경우 Route Segment Config 옵션 설정을 통해 세그먼트의 모든 GET 요청을 구성할 수 있다.
    // Revalidate at most every hour 
    fetch('https://...', { next: { revalidate: 3600 } })
    
    // app/products/route.js 
    // /products 경로에 대한 모든 GET 요청은 1시간 동안 캐시된다 
    // fetch를 명시적으로 사용하지 않아도 이 설정이 해당 경로의 모든 요청에 대해 적용된다. 
    export const revalidate = 3600; // (Route Segment Config)`
    • 작동 방식
      • 처음 호출되면 외부에서 데이터를 가져와 데이터 캐시에 저장
      • 지정된 시간내에 호출되는 요청은 캐시된 데이터 반환
      • 해당 시간이 지난 후 다음 요청은 캐시되었던 데이터 반환
      • 백그라운드에서 데이터 재검증 트리거 -> 데이터를 성공적으로 가져왔을때 데이터 캐시를 최신 데이터로 업데이트, 재검증 실패시 이전 데이터 유지
        (시간기반 재검증 작동방식 사진 첨부하기)
  • On-demand Revalidation
    • 이벤트에 따라 데이터를 재검증한다. (최신 데이터가 가능한 한 빨리 표시되도록 하려는 경우에 유용)
    • 경로(revalidatePath) or 캐시 태그(revalidateTag) 를 통해 재검증할 수 있다.
    • 작동 방식
      • 처음 호출되면 외부에서 데이터를 가져와 데이터 캐시에 저장
      • on-demand 재검증이 트리거 되었을때, 해당 캐시 항목이 캐시에서 제거된다.
      • 다음에 요청이 이루어지면 다시 캐시가되고, 데이터는 외부에서 데이터를 가져와 데이터 캐시에 저장된다.
        (on-demand 재검증 작동방식 사진 첨부하기)

 

3️⃣ Full Route Cache

개요

  • Next.js는 빌드 시 자동으로 경로를 렌더링하고 캐시한다.
  • 모든 요청에 대해 서버에서 렌더링하는 대신 캐시된 경로를 제공할 수 있는 최적화로서 결과적으로 페이지 로드가 더 빨라진다.
  • Full Route Cache가 어떻게 작동하는지 이해하려면 React가 렌더링을 처리하는 방식과 Next.js가 결과를 캐시하는 방식을 살펴보는 것이 좋다.

3-1. 서버에서 React 렌더링

  • 서버에서 Next.js는 React의 API를 사용하여 렌더링을 조정한다. 이때 렌더링 작업은 개별 경로 세그먼트와 Suspense 경계에 따라 청크로 나뉜다.
  • 각 청크는 두 단계로 렌더링 된다.
    1. React는 스트리밍에 최적화된 특수 데이터 형식인 React Server Component Payload 로 서버 구성요소를 렌더링한다.
    2. Next.js는 React Server Component Payload와 Client Component Javascript 명령어를 사용하여 서버에서 HTML을 렌더링한다.
  • 즉 작업을 캐싱하거나 응답을 보내기 전에 모든 것이 렌더링될 때까지 기다릴 필요가 없다. 대신 작업이 완료되면 응답을 스트리밍할 수 있다.
❓ React Server Component Payload 란 무엇일까 ❓

- RSC Payload는 렌더링된 React Server Components 트리의 컴팩트한 바이너리 표현이다.
- 클라이언트에서 React가 브라우저의 DOM을 업데이트하는 데 사용된다.
- RSC Payload의 구성요소
    - 서버 컴포넌트의 렌더링된 결과
    - 클라이언트 구성요소를 렌더링해야 하는 위치와 해당 Javascript 파일에 대한 참조
    - 서버 컴포넌트에서 클라이언트 컴포넌트로 전달된 모든 props
(서버 컴포넌트에 대한 자세한 내용은 추후에 다루도록 하겠다..!)

3-2. 서버에서 Next.js 캐싱(전체 경로 캐시)

  • Next.js의 기본 동작은 경로의 렌더링된 결과(RSC Payload, HTML)를 서버에 캐시하는 것이다.
  • 이는 빌드 시 또는 재검증 시 정적으로 렌더링된 경로에 적용된다.

3-3. 클라이언트에게 Hydration 및 Reconciliation을 시킨다.

  1. HTML은 클라이언트 및 서버 컴포넌트의 비대화형 초기 미리보기를 빠르게 보여주는 데 사용된다.
  2. RSC Payload는 클라이언트와 렌더링된 서버 컴포넌트 트리를 Reconciliation 하고 DOM을 업데이트 하는데 사용된다.
  3. Javascript 지침은 Hydration에 사용된다. 클라이언트 컴포넌트를 사용하여 애플리케이션을 대화형으로 만든다.

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

  • RSC Payload는 클라이언트 측 Router Cache에 저장된다.
  • 이는 개별 경로 세그먼트로 분할된 별도의 메모리 내 캐시이다.
  • Router Cache는 이전에 방문한 경로를 저장하고 향후 경로를 미리 페치하여 탐색 환경을 개선하는 데 사용된다.

3-5. 이후의 동작

  • 이후 탐색이나 prefetch 중에 Next.js는 RSC Payload가 Router Cache에 저장되어 있는지 확인한다. 그렇다면 서버에 새 요청을 보내는 것을 건너뛴다.
  • 경로 세그먼트가 캐시에 없으면 Next.js는 서버에서 RSC Payload를 가져와 클라이언트의 Router Cache에 채운다.

정적 및 동적 렌더링

  • 경로가 빌드 시간에 캐시되는지의 여부는 정적 또는 동적으로 렌더링 되는지에 따라 달라진다.
    • 정적 경로는 기본적으로 캐시된다. ex) /products, /about
    • 동적 경로는 요청 시간에 렌더링되고 캐시되지 않는다. ex) /products/[id], /user/[username]
  • 아래의 다이어그램은 캐시된 데이터와 캐시되지 않은 데이터를 사용하여 정적 및 동적으로 렌더링된 경로 간의 차이점을 보여준다.
    • 정적 경로는 접근한 경로에 대하여 Full Route Cache에서 캐싱됨
    • 동적 경로는 접근한 경로에 대하여 Full Route Cache를 건너뛰고, Data Cache나 Data Source에 캐싱됨

전체 경로 캐시를 무효화할 수 있는 방법

  • 데이터 캐시를 재검증
    • Full Route Cache는 HTML과 React Server Component(RSC) Payload를 캐싱한다.
    • 데이터 캐시(Data Cache)는 Full Route Cache의 렌더링 결과를 구성하는 데 사용됩니다.
    • 따라서 데이터 캐시를 재검증하면 새로운 데이터를 기반으로 서버가 페이지를 다시 렌더링하고, Full Route Cache를 업데이트한다.
    • 데이터에 의존하지 않는 정적 경로는 데이터 캐시와 무관하므로 이 경우에는 Full Route Cache가 무효화되지 않습니다.
  • 재배포
    • Full Route Cache는 배포마다 초기화된다.
    • 배포 시에 캐싱된 HTML과 RSC Paylaod가 모두 삭제되고 서버가 처음 요청을 받을 때 다시 렌더링을 통해 새롭게 Full Route Cache를 채우기 때문

옵트아웃

  • 모든 수신 요청에 대해 구성요소를 동적으로 렌더링(full route cache를 해제) 하는 방법
  • 동적 API 사용(cookies,header 등등)
  • dynamic = 'force-dynamic'또는 revalidate = 0 경로 세그먼트 구성 옵션 사용 (full route cache, data cache 를 건너뜀)
  • 데이터 캐시에서 옵트아웃: 특정 fetch 요청에서 데이터 캐시를 사용하지 않으므써 full route cache도 비활성화 하는 방법 (fetch 에 옵션을 주어 SSR 또는 ISR 방식 채택)

4️⃣ Router Cache

개요

  • Next.js는 레이아웃, 로딩상태, 페이지로 분할된 경로 세그먼트의 RSC Payload를 저장하는 메모리 내 클라이언트 측 라우터 캐시를 갖고있다.
  • 사용자가 경로 사이를 탐색할 때 Next.js는 방문한 경로 세그먼트를 캐시하고, 사용자가 탐색할 가능성이 높은 경로를 미리 가져온다.
  • 그 결과 즉각적인 뒤로/앞으로 탐색이 이루어지고, 탐색 사이에 전체 페이지를 다시 로드하지 않으며, React 상태와 브라우저 상태가 보존된다.
  • 라우터 캐시의 사용
    • 레이아웃은 캐시되어 탐색 시 재사용된다(부분 렌더링)
    • 로딩 상태는 캐시되어 즉각적인 탐색을 위해 탐색 시 재사용된다.
    • 페이지는 기본적으로 캐시되지 않지만 브라우저 뒤로 및 앞으로 탐색하는 동안 재사용된다.
  • 캐시는 브라우저의 임시 메모리에 저장된다. 따라서 브라우저 새로고침시 캐시가 지워지게된다.
  • 레이아웃, 로딩 상태 캐시는 특정 시간 후에 자동으로 무효화된다.
    • 기본 프리페칭(prefetch={null}, 미지정): 동적 페이지는 캐시되지 않음, 정적 페이지의 경우 5분
    • 전체 프리페칭(prefecth={true} 또는 router.prefetch): 정적 및 동적 페이지 모두 5분

라우터 캐시 작동방식

  • 사용자가 경로 사이를 탐색할 때 Next.js는 방문한 경로 세그먼트를 캐시하고, 사용자가 탐색할 가능성이 높은 경로를 미리 가져온다
    ( 컴포넌트의 뷰포트 기반)
  • 방문한 경로가 캐시되므로 즉시 뒤로/앞으로 탐색 가능하고, 프리페칭 및 부분 렌더링을 통해 새로운 경로로 빠르게 탐색 가능하다.
  • 탐색 간에 전체 페이지를 다시 로드하지 않으며 React 상태와 브라우저 상태가 보존된다.

❓ Router Cache와 Full Route Cache의 차이점 ❓

- Router Cache
    - 사용자 세션 동안 브라우저에 RSC Payload를 일시적으로 저장한다.
    - 정적으로 렌더링된 경로와 동적으로 렌더링된 경로 모두에 적용된다.
- Full Route Cache
    - 여러 사용자 요청에 걸쳐 RSC Payload와 HTML을 서버에 지속적으로 저장한다.
    - 정적으로 렌더링된 경로만 캐시한다.

라우터 캐시를 무효화하는 방법

  • 이미 클라이언트에 캐싱된 데이터를 무효화하여 새로운 데이터를 서버에서 가져오도록 강제하는 과정
  • 캐싱된 데이터가 더 이상 유효하지 않을 때 수행할 수 있다.
  • 서버에서 무효화
    • revalidatePath를 사용하여 특정 경로의 캐시를 무효화(on-demand revalidation)
    • revalidateTag을 사용하여 특정 태그가 포함된 데이터 캐시 무효화
    • cookies 를 사용한 라우터 캐시 무효화
  • 클라이언트에서 무효화
    • router.refresh를 통한 무효화 => 클라이언트 측에서 Router Cache를 무효화하고, 현재 경로의 데이터를 다시 서버에서 가져오도록 요청

📝 정리

📌 Request Memoization
    - React 컴포넌트 트리의 여러 곳에서 동일한 데이터에 대한 fetch 함수를 호출할 수는 있지만 실제로 한 번만 실행된다.
    - React 서버 컴포넌트의 렌더링 중에 발생하는 fetch 요청을 메모이제이션한다.
    - 메모이제이션된 `fetch` 요청은 렌더링 패스가 끝나면 초기화된다.
    - GET 요청시에만 적용된다.
    
📌 Data Cache
    - 데이터를 일정 기간 동안 캐싱하고, 필요에 따라 재검증(revalidation)을 수행하고 싶을 때 사용한다.
    - 서버 간 요청과 배포 간에도 데이터를 유지하여 불필요한 데이터 요청을 줄인다.
    - `fetch` API의 `revalidate` 나 `cache` 옵션을 통해 캐싱 동작을 제어할 수 있다.
    - cache: 'force-cache': 데이터를 항상 캐싱(SSG).
    - cache: 'no-store': 데이터를 실시간으로 가져오며, 캐싱하지 않음(SSR).
    - next: { revalidate: N }: N초마다 데이터 재검증(ISR).
    
📌 Full Route Cache
    - 서버에서 HTML과 RSC Payload를 생성한 뒤, 이를 여러 사용자 요청에 재사용하고 싶을 때 사용한다.
    - 서버에서 경로의 HTML과 React Server Component(RSC) Payload를 캐싱하여 정적인 경로에서 빠른 페이지 로드를 제공한다.
    - 데이터 캐시가 변경되거나 재배포 시 캐시가 초기화된다.
    
📌 Router Cache
    - 클라이언트에서 페이지 탐색 시 빠른 응답 속도가 필요하거나, 사용자 세션 동안 데이터를 캐싱하고 뒤로/앞으로 탐색 시 즉각적으로 데이터를 로드하려는 경우 사용한다.
    - 클라이언트 측에서 RSC Payload를 캐싱하여 서버 요청을 줄이고 페이지 탐색 속도를 높인다.
    - 사용자 세션 동안 유지되며, 브라우저 새로고침 시 초기화된다.
    - Link 컴포넌트를 이용한 프리페칭이 대표적인 예시
🔥 전체적인 캐싱 흐름

1️⃣ 빌드 단계 
    - 정적인 HTML과 RSC Payload를 생성하여 Full Route Cache 에 저장
    - 정적인 경로, 정적인 페이지들이 적용 대상이다.

2️⃣ 요청 처리 단계(Server-side)
    - Request Memoization 을 이용해 서버 렌더링 과정 중 동일한 fetch 요청이 여러 번 호출되더라도 한 번만 실행되도록 자동으로 메모이제이션 한다. 
    - Data Cache를 이용해 fetch 요청 결과를 저장하여 동일한 데이터 요청을 처리하고, revalidate 설정을 통해 갱신 주기 제어 

3️⃣ 렌더링 단계(런타임)
    - 내부 데이터의 재검증이 필요하다면 Full Route Cache가 사용된다. (ISR)
    - 동적인 경로이거나, 매 요청마다 데이터를 갱신해야 한다면 Full Route Cache를 건너뛴다. (SSR, '/products/[id]')

4️⃣ 클라이언트 탐색 단계
    - Router Cache를 이용해 RSC Payload를 클라이언트 메모리에 캐싱하여 동일한 경로를 탐색할때 서버 요청을 건너뛰고 빠르게 렌더링한다.

5️⃣ 무효화 단계
    - 캐시가 더 이상 유효하지 않을때, 데이터를 강제로 갱신한다.
    - 서버에서의 무효화: revalidatePath/revalidateTag(Data Cache, Full Route Cache 무효화), 쿠키 변경 메서드(Router Cache 무효화)
    - 클라이언트에서의 무효화: router.refresh(Router Cache 무효화)

6️⃣ 배포 단계
    - 빌드된 결과물을 CDN을 통해 제공
    - Full Route Cache 초기화
    - Data Cache는 배포간에 유지되므로 데이터가 변경되지 않았다면 새로운 요청에서도 동일한 데이터 사용 가능

📚 REFERENCE