Next.js의 캐싱 쉽게 이해하기

ClydeHan·2025년 4월 15일

📑 목차

  1. 캐싱 개요 (Overview)
  2. Request Memoization
  3. Data Cache
  4. Full Route Cache
  5. Client-side Router Cache

1. 캐싱 개요 (Overview)


1-1. 캐싱이란?

웹 애플리케이션에서 캐싱(Cache) 이란, 어떤 데이터나 결과를 임시로 저장해 두는 것을 말한다.

예를 들어, 사용자가 "상품 목록 보기" 페이지를 열었을 때, 서버는 상품 데이터를 불러와서 화면에 보여준다. 그런데 같은 사용자가 몇 초 후 다시 같은 페이지로 이동한다면, 다시 데이터를 불러올 필요가 있을까?

❌ 불필요한 데이터 요청 = 서버 낭비 + 로딩 느림

✅ 이미 불러온 데이터를 재사용 = 빠른 반응 + 효율적인 처리

이처럼, 이미 가져온 데이터나 렌더링 결과를 저장해두고, 나중에 같은 요청이 오면 다시 사용하는 기술이 바로 캐싱이다.


1-2. Next.js의 캐싱

Next.js는 브라우저와 서버가 협력해서 동작하는 프레임워크이다.

따라서 캐싱도 "서버"와 "클라이언트"라는 두 환경에서 각기 다르게 동작한다.

참고: 대부분의 캐싱은 서버 환경에서 작동하고, 클라이언트 쪽에는 보조적인 캐시만 존재한다.


🛰️ 서버 캐싱 (Server-side Caching)

서버에서 실행되는 컴포넌트(Server Component)나 API(Route Handler)에서는 데이터 요청, 렌더링 결과, HTML 등을 저장하고 재사용하는 고급 캐싱 기능이 적용된다.

대표적으로 아래 세 가지가 있다.

  • Request Memoization: 서버 렌더링 중 중복된 fetch 요청을 한 번만 실행
  • Data Cache: 외부 API나 DB 데이터를 서버에 저장하고 여러 요청 간 재사용
  • Full Route Cache: HTML과 RSC Payload 전체를 저장해서 페이지 전체를 캐싱

이 세 가지는 모두 서버 컴포넌트(Server Component) 또는 서버 API(Route Handler)에서만 작동한다.

즉, "use client"가 선언된 클라이언트 컴포넌트에서는 적용되지 않는다.

참고: RSC = React Server Component


💻 클라이언트 캐싱 (Client-side Caching)

브라우저 환경에서도 캐시가 존재한다. 하지만 서버처럼 데이터를 따로 저장하거나 fetch를 재사용하진 않는다. 클라이언트에서 사용하는 캐시는 오직 Router Cache 하나뿐이다.

  • 사용자가 어떤 페이지를 방문하면,
  • 서버에서 받은 RSC Payload 전체가 클라이언트(브라우저 메모리)에 저장되고,
  • 이후 다시 해당 경로로 이동하면, 서버에 새 요청 없이 즉시 렌더링된 결과가 재사용된다.

즉, 클라이언트 캐시는 서버 결과물을 전체 덩어리로 저장해두는 방식이다. 서버처럼 fetch() 결과를 따로 캐싱하거나 API 결과를 저장하지는 않는다.

📌 참고:

Full Route CacheRouter Cache는 모두 페이지 전체를 저장하지만,

Full Route Cache서버가 다시 렌더링하지 않기 위해,

Router Cache클라이언트가 서버에 다시 요청하지 않기 위해 존재한다.


1-3. Next.js의 주요 캐싱 메커니즘 4가지

Next.js는 서버에서 HTML을 생성하거나, 데이터를 가져와야 하는 구조이기 때문에, 다음과 같은 이슈가 발생할 수 있다.

  • 사용자가 많아지면 서버 부담이 커짐
  • 같은 데이터를 매번 새로 가져오면 속도가 느림
  • 서버 비용이 높아짐

그래서 Next.js는 내부적으로 다음과 같은 네 가지 캐싱 시스템을 제공한다. 이 네 가지는 각각의 역할과 캐싱 위치, 목적이 다르다.

메커니즘 이름무엇을 캐싱하는가?어디에 저장되는가? (작동 환경)공유 범위목적지속 시간
Request Memoization서버 컴포넌트 안의 fetch() 결과서버 (요청 중 메모리)요청 1건 내에서만렌더링 중 같은 fetch가 여러 번 호출돼도 요청은 한 번만 발생하나의 요청 처리 중에만 유지 (짧음)
Data Cache외부 API나 DB에서 가져온 데이터서버모든 사용자와 공유같은 fetch 요청이 다시 들어올 때, 서버에 저장된 응답을 재사용 (렌더링 이후에도 유효)서버에 지속 보관됨 (시간 설정 또는 수동으로 갱신 가능)
Full Route Cache페이지 렌더링 결과 (HTML + RSC Payload)서버모든 사용자와 공유동일한 경로 요청 시 서버가 다시 렌더링하지 않도록 함서버에 지속 보관됨 (시간 설정 또는 수동으로 갱신 가능)
Router Cache서버에서 받은 RSC Payload 전체클라이언트 (브라우저 메모리)본인만 사용 가능페이지 전환 시 서버에 다시 요청하지 않고 즉시 렌더링탭을 닫을 때까지 or 일정 시간 후 자동 삭제

1-4. 캐싱의 자동화와 제어

Next.js는 대부분의 캐싱을 자동으로 처리해준다.

하지만, 모든 상황에 완벽하게 맞출 수는 없기 때문에, 개발자가 아래와 같은 것들을 직접 설정할 수도 있다.

  • 캐시 사용 안 함 (Opt-out)
  • 일정 주기마다 재검증 (Time-based Revalidation)
  • 특정 이벤트 이후 재검증 (On-demand Revalidation)

이런 설정들을 위해 다양한 API와 옵션들(fetch 옵션, revalidateTag, router.refresh 등)이 제공된다.


1-5. Next.js의 캐싱 구조

Caching structure of Next.js

[유저 요청]
      ↓
[서버]
 ├── Request Memoization (요청 중 중복 제거)
 ├── Data Cache (데이터 저장소)
 └── Full Route Cache (페이지 결과 저장소)
      ↓
[클라이언트]
 └── Router Cache (브라우저 메모리)

이렇게 4단계로 구성된 캐시 시스템은, 사용자가 같은 페이지를 다시 방문할 때 서버나 데이터베이스에 요청하지 않아도 빠르게 응답할 수 있도록 돕는다.


1-6. 요약

  • 캐싱 = 이미 구한 것을 저장해서, 다시 쓰는 기술.
  • Next.js는 4가지 캐시 시스템으로 구성됨.
  • 대부분은 자동으로 처리되며, 필요 시 직접 설정도 가능.

2. Request Memoization – 요청 메모이제이션

Next.js Request Memoization

2-1. 개념 설명

Request Memoization(요청 메모이제이션)은 같은 fetch 요청이 여러 번 발생하더라도, 실제로는 한 번만 실행하고 결과를 재사용하는 기능이다.

예를 들어 어떤 React 컴포넌트에서 fetch("https://api.example.com/item/1")를 호출했다고 할 때, 다른 컴포넌트에서도 같은 URL로 fetch를 호출하면, Next.js는 네트워크 요청을 다시 보내지 않고, 처음 가져온 데이터를 그대로 다시 사용한다.

이것이 바로 “메모이제이션(Memoization)”이라는 기술이다.


2-2. 필요성

React 기반의 서버 컴포넌트 구조에서는 다음과 같은 데이터 요청의 비효율성 문제가 자주 발생한다.

  • 여러 컴포넌트에서 동일한 데이터를 각각 fetch로 요청
  • 동일한 URL에 대해 중복 fetch 발생
  • 서버에서 불필요한 네트워크 요청이 반복됨

예를 들어, 다음과 같은 경우를 생각할 수 있다.

  • Layout 컴포넌트에서 사용자 정보를 요청한다.
  • Page 컴포넌트에서도 동일한 사용자 정보를 다시 요청한다.
  • ProfileCard와 같은 하위 컴포넌트에서도 동일한 정보를 또 요청한다.

이처럼 하나의 페이지에서 동일한 데이터 요청이 여러 번 발생하게 되며, 각 요청은 모두 같은 URL을 대상으로 한다. 결과적으로 서버는 같은 데이터에 대해 여러 번 네트워크 요청을 수행하게 되고, 이는 리소스 낭비로 이어진다.

Next.js는 React의 요청 메모이제이션 기능을 활용하여, 서버 컴포넌트 환경에서 동일한 fetch 요청에 대해 자동으로 결과를 재사용한다.

  • 첫 번째 fetch 요청의 결과를 내부적으로 저장해 둔다.
  • 이후 동일한 URL에 대한 요청이 들어오면, 네트워크를 호출하지 않고 캐시된 결과를 반환한다.

이를 통해 불필요한 중복 요청을 방지하고, 전체 페이지 렌더링의 효율성을 높일 수 있다.

Request Memoization은 Next.js 고유 기능은 아니며, React의 렌더링 최적화 기능에 기반한 동작이다. 다만, Next.js의 Data Cache, Full Route Cache, Router Cache 등 캐싱 시스템과 어떻게 함께 작동하는지를 설명하기 위해 공식 문서에서 언급되었다.


2-3. 작동 방식

🔄 요청 라이프사이클(Request Lifecycle)

Next.js Request Memoization Request Lifecycle

하나의 HTTP 요청이 서버에 도착해서 응답을 마칠 때까지의 전체 과정이다. 메모이제이션은 이 라이프사이클 동안만 유지된다.

1. 컴포넌트 A에서 fetch 실행
    → 아직 저장된 데이터가 없음 → 실제 요청 → 결과 저장 → cache MISS

2. 컴포넌트 B에서 같은 fetch 실행
    → 저장된 결과가 있음 → 네트워크 요청 없이 결과 반환 → cache HIT

3. 컴포넌트 C에서도 같은 fetch 실행
    → 또 cache HIT

4. 페이지 렌더링 완료
    → 모든 캐시 초기화 (요청 사이클 종료)
  • cache MISS(캐시 미스) 캐시에 값이 아직 저장되지 않은 상태로, 네트워크 요청을 실제로 수행한다.
  • cache HIT(캐시 히트) 이전 요청에서 저장된 데이터가 존재할 경우, 해당 데이터를 그대로 반환한다. 네트워크 요청이 일어나지 않는다.

2-4. 적용 범위

Request Memoization(요청 메모이제이션)은 모든 fetch 요청에 자동으로 적용되는 것은 아니다.

1. 적용 대상 요청

  • GET 요청에만 적용된다. 이는 GET 요청이 데이터를 읽기만 하고 상태를 변경하지 않는다는 특성 때문이다.
  • POST, PUT, DELETE변경을 유발하는 요청에는 적용되지 않는다. 이들은 요청마다 결과가 달라질 수 있기 때문에 캐시가 허용되지 않는다.

2. 적용 위치 (React 컴포넌트 트리 내부)

  • ✅ 적용됨
    • Page, Layout, Template, generateMetadata, generateStaticParamsReact 컴포넌트 트리 내부에서 동작하므로 메모이제이션이 적용된다.
  • ❌ 적용되지 않음
    • Route Handler (app/api/*), MiddlewareReact 트리 외부에서 실행되므로 메모이제이션이 적용되지 않는다.

3. 캐시 지속 시간

Request Memoization(요청 메모이제이션)은 요청 처리 중(렌더링 중)에만 적용되며, 요청이 완료되면 캐시도 초기화된다. 즉, 클라이언트 요청 하나에 대한 처리 사이클 동안만 유효하다.

  • 사용자가 페이지를 열고 서버에 요청.
  • 서버는 데이터를 가져오고 페이지를 렌더링.
  • 렌더링 끝나면 캐시는 지워진다.

다음에 같은 사용자가 다시 요청하면 처음부터 다시 캐시를 채운다.


2-5. 옵트아웃 (Opt-out)

💡 옵트아웃(Opt-out)
기본으로 활성화되어 있는 기능을 사용자가 명시적으로 끌 수 있는 선택권을 의미한다.

예를 들어, 어떤 기능이 기본값으로 켜져 있지만, 옵션을 변경하거나 설정을 조정해서 끌 수 있다면 옵트아웃 가능이다.

Request Memoization(요청 메모이제이션)은 명시적으로 비활성화할 수 없다.

이 동작은 React 내부에서 자동으로 수행되는 최적화이므로, 개발자가 명시적으로 끌 수 있는 설정은 존재하지 않는다. 따라서 요청 메모이제이션은 옵트아웃할 수 없다.

특정 요청을 수동으로 중단(Abort)할 수는 있지만, 이는 메모이제이션을 끄는 것이 아니라 요청 실행 도중에 취소하는 것이다.

const controller = new AbortController();
const signal = controller.signal;

fetch('https://api.example.com/data', { signal });

// 어떤 조건일 때 요청을 중단한다
controller.abort();

AbortController는 네트워크 요청을 중단하는 기능이지, 메모이제이션을 끄는 방법은 아니다.


2-6. 요약

  • Request Memoization은 동일한 fetch 요청의 결과를 단일 요청 내에서 재사용하는 기능이다.
  • React의 렌더링 과정에서 자동으로 적용된다. 중복 네트워크 요청을 방지하여 성능을 최적화한다.
  • GET 요청과 React 컴포넌트 트리 내에서만 작동하며, 렌더링 완료 후 캐시가 초기화된다.
  • 명시적 비활성화는 불가능하다.

3. Data Cache – 데이터 캐시

3-1. 개념 설명

Data Cache(데이터 캐시)는 서버에서 fetch()로 가져온 데이터를 요청 처리 중뿐만 아니라, 그 이후에도 서버에 저장해 두고 재사용할 수 있게 하는 기능이다.

즉, 한 번 데이터를 가져오면 서버가 그 결과를 기억해두고, 같은 요청이 다시 들어왔을 때는 외부 API나 DB를 다시 조회하지 않고 캐시된 결과를 그대로 반환한다.

Request Memoization은 단일 요청 내에서만 데이터를 기억하는 반면, Data Cache는 한 번 저장되면, 이후 다른 사용자의 요청이나 다음 페이지 요청에서도 그대로 재사용된다. 심지어 앱이 다시 배포되기 전까지는 그대로 유지된다.


3-2. 필요성

웹 서비스에서는 동일한 데이터에 대한 요청이 빈번히 발생한다. 예를 들어, 블로그 게시글 목록이나 상품 카테고리 같은 데이터는 짧은 시간 동안 변경되지 않는 경우가 많다. 이러한 데이터를 매번 외부 서버에서 가져오면 다음과 같은 문제가 발생한다.

  • 서버 비용이 증가한다.
  • 응답 속도가 느려진다.
  • 네트워크 부하가 커진다.

Data Cache는 데이터를 서버에 저장하여 동일한 요청에 대해 저장된 데이터를 빠르게 반환한다. 이를 통해 성능을 개선하고 비용을 절감한다.


3-3. 작동 방식

Data Cache는 fetch() 요청을 통해 데이터를 저장하고 재사용하는 과정을 거친다.

How Next.js Data Cache Works

1. 데이터 요청 발생
    → Data Cache 확인 → 데이터 있으면 반환 → cache HIT

2. 데이터 없으면
    → 외부 서버에서 가져옴 → 캐시에 저장 → 반환 → cache MISS

3. cache: 'no-store' 설정 시
    → 캐시 사용 안 함 → 외부 서버에서 직접 가져옴

참고: Data Cache는 fetch 요청의 URL과 옵션을 기준으로 데이터를 식별한다. 동일한 요청은 동일한 캐시 항목을 공유한다.


3-4. 캐시 설정 방법

Next.js는 fetch()에 옵션을 추가하여 Data Cache의 동작을 제어한다. 주요 옵션은 cachenext.revalidate이다.

참고: cache 옵션을 설정하지 않으면 Next.js는 기본적으로 no-store처럼 동작한다. 캐시를 사용하려면 force-cache를 명시적으로 지정해야 한다.

📌 cache 옵션

옵션설명
force-cache캐시가 있으면 사용하고, 없으면 데이터를 가져와 캐시에 저장한다.
no-store캐시를 사용하지 않고 매번 외부 서버에서 데이터를 가져온다. 캐시에 저장하지 않는다.
// 캐시된 데이터 요청
fetch('https://api.example.com/news', {
  cache: 'force-cache'
});

// 캐시를 사용하지 않는 요청
fetch('https://api.example.com/user', {
  cache: 'no-store'
});

📌 next.revalidate 옵션

fetch('https://api.example.com/news', {
  next: { revalidate: 60 } // 60초마다 캐시 갱신
});
  • revalidate는 캐시된 데이터의 유효 기간(초 단위)을 지정한다.
  • 지정된 시간이 지나면 다음 요청에서 기존 캐시 데이터를 반환하고, 백그라운드에서 새 데이터를 가져와 캐시를 갱신한다.

3-5. 캐시 갱신 방식

시간 기반 갱신 (Time-based Revalidation)

지정된 시간이 지나면 캐시를 갱신한다.

Next.js Data Cache Time-based Revalidation

1. 지정된 시간 동안
    → 캐시된 데이터 반환

2. 시간 초과 시
    → 기존 데이터 반환 → 백그라운드에서 새 데이터 요청

3. 새 데이터 수신
    → 성공 시 캐시 업데이트 → 실패 시 기존 데이터 유지
fetch('https://api.example.com/data', {
  next: { revalidate: 300 } // 5분마다 갱신
});

요청 기반 갱신 (On-demand Revalidation)

  • 특정 이벤트에 따라 캐시를 즉시 갱신한다.
    • revalidatePath('/news'): 지정된 경로의 캐시를 갱신한다.
    • revalidateTag('news'): 지정된 태그로 묶인 캐시를 갱신한다.

Next.js Data Cache On-demand Revalidation

1. 갱신 요청 발생
    → 해당 캐시 삭제

2. 다음 데이터 요청
    → 새 데이터 가져옴 → 캐시에 저장
// 데이터 요청 시 태그 지정
fetch('https://api.example.com/news', {
  next: { tags: ['news'] }
});

// 태그로 캐시 갱신
revalidateTag('news');

3-6. 사용 가능 위치

위치사용 가능 여부
Page, Layout, Server Component✅ 가능
Route Handler (app/api/*)✅ 가능
Middleware❌ 불가능

참고: Middleware에서는 fetch 요청이 항상 no-store처럼 동작하여 캐시를 지원하지 않는다.


3-7. 요약

  • 데이터 캐시는 서버에서 지속적으로 유지된다.
  • Data Cache는 서버에 데이터를 저장하여 성능을 향상시키고 비용을 절감한다.
  • 시간 기반 갱신과 요청 기반 갱신으로 최신 데이터를 유지한다.
  • 캐시를 사용하려면 fetch 옵션을 명시적으로 설정해야 한다.

4. Full Route Cache – 전체 경로 캐시


4-1. 개념 설명

Full Route Cache는 정적 경로의 페이지 렌더링 결과(HTML과 React Server Component Payload)를 서버에 저장하여 이후 요청에서 재사용하는 기능이다. Full Route Cache는 정적 경로에서만 기본적으로 작동하며, 동적 경로는 캐시되지 않는다.

💡 정적 페이지?
내용이 자주 변하지 않고 모든 사용자에게 동일하게 제공되는 페이지다.
(예: 회사 소개 페이지, 블로그 목록)

Next.js는 이런 페이지를 빌드할 때 미리 만들거나, 첫 요청 시 렌더링한 뒤 저장한다. 이후 같은 페이지 요청이 오면 새로 만들지 않고 저장된 결과를 바로 보낸다.

💡 React Server Component Payload란?
페이지의 구성 요소(컴포넌트)를 React가 서버에서 처리한 결과물로, 압축된 데이터 형식이다. 클라이언트는 이 데이터를 받아 화면을 완성한다. 쉽게 말해, 서버가 미리 그린 "설계도" 같은 것이다.

Data Cache와의 차이점
Data Cache는 fetch로 가져온 데이터만 저장하지만, Full Route Cache는 페이지 전체(HTML과 Payload)를 저장한다.


4-2. 필요성

정적 페이지는 많은 사용자가 자주 방문하지만 내용이 거의 바뀌지 않는다. 매번 새로 렌더링하면 다음과 같은 문제가 생긴다.

  • 서버가 매번 페이지를 만드는 데 시간과 자원을 쓴다.
  • 사용자에게 페이지가 늦게 보인다.
  • 서버 비용이 증가한다.

Full Route Cache는 페이지를 한 번 만들어 저장해두고, 요청이 올 때마다 빠르게 보여줌으로써 성능을 높이고 비용을 줄인다. 예를 들어, 블로그 목록 페이지를 매번 새로 만들지 않고 저장된 버전을 보여주면 서버 부담이 훨씬 적다.


4-3. 작동 방식

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

Full Route Cache는 정적 경로에서 페이지를 미리 만들어 저장하고 재사용한다. 동적 경로는 캐시되지 않는다. 아래는 그 과정이다.

1. 정적 경로의 경우
    → 빌드 시 렌더링 → HTML과 RSC Payload 캐시에 저장

2. 동일 정적 경로 요청
    → 캐시된 HTML과 RSC Payload 반환 → 렌더링 생략

3. 클라이언트 처리
    → HTML로 빠른 화면 표시 → RSC Payload로 상호작용 추가

4. 동적 경로의 경우
    → 요청 시 렌더링 → 캐시 사용 안 함

작동 원리

Static and Dynamic Rendering

  1. 서버에서 렌더링
    • Next.js는 React를 사용해 페이지를 만든다. 페이지의 각 부분(예: 헤더, 본문)은 작은 조각으로 나뉘어 필요할 때 처리된다(Suspense 경계라는 기술 덕분).
    • 정적 경로라면
      • HTML: 브라우저가 바로 보여줄 수 있는 완성된 페이지.
      • RSC Payload: 클라이언트에서 화면을 완성하도록 돕는 데이터(버튼 동작 등 포함).
    • 동적 경로라면 매번 새로 만든다.
  2. 캐시에 저장
    • 정적 경로의 HTML과 RSC Payload가 서버에 저장된다. 이것이 Full Route Cache다.
    • 동적 경로는 저장되지 않고 요청마다 새로 생성된다.
  3. 클라이언트에서 화면 표시
    • 정적 경로 요청 시
      • HTML이 먼저 화면에 띄워진다(정적 내용 보임).
      • RSC Payload가 브라우저에서 처리되어 버튼 클릭 같은 상호작용이 가능해진다(Hydration 과정).
  4. 정적 vs 동적
    • 정적 경로는 캐시를 사용해 빠르다.
    • 동적 경로는 사용자별 데이터(예: 로그인 정보)가 필요하므로 캐시 없이 매번 렌더링된다.

💡 Hydration?
Hydration은 서버에서 보낸 페이지를 클라이언트에서 "살아 움직이게" 만드는 과정이다. 예를 들어, 서버가 정적인 사진 같은 페이지를 주면, 클라이언트가 버튼을 누를 수 있게 동작을 추가하는 식이다.


4-4. 캐시 설정 방법

Full Route Cache는 정적 경로에서 기본적으로 적용된다. 동적 경로로 바꾸거나 캐시 주기를 설정할 수 있다.

📌 정적 렌더링 (기본 동작)

  • 빌드 시 페이지를 렌더링해 캐싱한다.
  • 설정 없이 자동 적용된다.
// app/about/page.tsx
export default async function AboutPage() {
  return <h1>About Us</h1>;
}
  • 이 페이지는 빌드 시 캐시되어 요청 시 빠르게 제공된다.

📌 동적 렌더링 (캐시 비활성화)

동적 경로로 만들면 Full Route Cache가 적용되지 않는다:

옵션설명
dynamic = 'force-dynamic'정적 경로를 동적으로 변경, 캐시 없이 요청 시 렌더링한다.
revalidate = 0정적 경로를 동적으로 변경, 캐시 없이 요청 시 렌더링한다.
cookies(), headers()이런 함수를 사용하면 자동으로 동적 경로가 되어 캐시되지 않는다.
fetch에 cache: 'no-store'데이터가 캐시되지 않으면 페이지도 동적으로 간주된다.
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';

export default async function Dashboard() {
  const res = await fetch('https://api.example.com/user', { cache: 'no-store' });
  const user = await res.json();
  return <p>Welcome, {user.name}!</p>;
}
  • 사용자별 데이터가 필요하므로 캐시를 쓰지 않는다.

📌 캐시 주기 설정 (ISR)

정적 경로에 갱신 주기를 추가할 수 있다.

// app/products/page.tsx
export const revalidate = 3600; // 1시간마다 갱신
  • 캐시되지만 1시간마다 새로 만든다.

📌 혼합 설정

페이지 일부는 캐시하고, 일부는 동적으로 처리할 수 있다.

// app/products/page.tsx
export default async function ProductsPage() {
  const staticData = await fetch('https://api.example.com/products', {
    cache: 'force-cache',
  }).then((res) => res.json());
  const userData = await fetch('https://api.example.com/user', {
    cache: 'no-store',
  }).then((res) => res.json());
  return (
    <div>
      <p>User: {userData.name}</p>
      <ul>
        {staticData.map((item: any) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}
  • 제품 목록은 캐시되고, 사용자 정보는 매번 새로 가져온다.

4-5. 캐시 갱신 방식

Full Route Cache는 정적 경로에서 기본적으로 오래 유지되지만, 갱신 방법이 있다.

시간 기반 갱신 (Incremental Static Regeneration, ISR)

지정된 시간이 지나면 캐시를 갱신한다.

// app/products/page.tsx
export const revalidate = 3600; // 1시간마다 갱신
1. 지정된 시간 동안
    → 캐시된 페이지 반환

2. 시간 초과 시
    → 기존 페이지 반환 → 백그라운드에서 새로 렌더링

3. 렌더링 완료
    → 성공 시 캐시 업데이트 → 실패 시 기존 캐시 유지

요청 기반 갱신 (On-demand Revalidation)

특정 이벤트로 캐시를 즉시 갱신한다.

  • revalidatePath('/products'): 특정 경로 갱신.
  • revalidateTag('products'): 태그로 묶인 캐시 갱신.
// app/api/revalidate/route.tsx
export async function POST() {
  revalidateTag('products');
  return Response.json({ message: 'Cache revalidated' });
}
1. 갱신 요청 발생
    → 해당 캐시 삭제

2. 다음 페이지 요청
    → 새로 렌더링 → 캐시에 저장

배포 시 갱신

새 배포가 발생하면 모든 Full Route Cache가 삭제되고 새로 생성된다.

Data Cache가 갱신되면(예: revalidateTag), 그 데이터를 사용하는 Full Route Cache도 갱신된다.


4-6. 사용 가능 위치

Full Route Cache는 페이지와 레이아웃에서만 동작한다.

위치사용 가능 여부
Page, Layout✅ 가능
Route Handler (app/api/*)❌ 불가능
Middleware❌ 불가능

동적 경로로 설정된 페이지는 캐시되지 않는다.


4-7. 요약

  • Full Route Cache는 정적 경로의 HTML과 RSC Payload를 서버에 저장한다.
  • 정적 경로는 캐시로 빠르게 제공되고, 동적 경로는 캐시 없이 렌더링된다.
  • 시간 기반 갱신(ISR), 요청 기반 갱신, 배포로 캐시를 갱신한다.
  • dynamic = 'force-dynamic' 또는 revalidate = 0으로 정적 경로의 캐시를 끌 수 있다.
  • 데이터와 페이지 캐시를 혼합해 유연하게 사용할 수 있다.

5. Client-side Router Cache – 클라이언트 측 라우터 캐시

5-1. 개념 설명

Client-side Router Cache브라우저 메모리에 저장되는 Next.js의 전용 캐시 시스템이다.

Next.js에서는 페이지를 탐색할 때마다, 서버로부터 받은 RSC Payload를 클라이언트 측에서 메모리에 저장해두고, 이후 동일한 경로로 이동할 때 서버에 다시 요청하지 않고 재사용한다.

이렇게 하면 사용자는 페이지 전환 시마다 서버에 새 요청을 보내지 않고, 즉시 페이지를 렌더링할 수 있다. 브라우저 히스토리와 상태도 보존되므로 UX가 좋아진다.


5-2. Full Route Cache와의 차이점

Full Route Cache는 서버에서 렌더링된 HTML과 RSC Payload를 저장하여, 서버가 동일한 페이지 요청에 대해 다시 렌더링할 필요 없이 빠르게 응답할 수 있도록 해준다. 하지만 이 캐시는 어디까지나 서버를 위한 최적화 수단일 뿐이며, 클라이언트는 페이지를 이동할 때마다 여전히 서버에 새로운 요청을 보낸다.

즉, 서버가 렌더링을 재사용하는 것은 가능하지만, 클라이언트가 서버에 요청을 보내는 시도 자체를 막을 수는 없다. 이 지점에서 클라이언트 라우터 캐시가 필요해진다. 클라이언트 측에서 직접 RSC Payload를 기억하고 있다면, 아예 서버에 요청을 보내지 않고도 이전에 본 페이지를 즉시 렌더링할 수 있게 되기 때문이다.

따라서 Next.js는 서버의 응답 속도를 위한 Full Route Cache, 그리고 클라이언트의 인터랙션 속도와 상태 유지를 위한 Client-side Router Cache서로 보완적인 역할로 함께 제공한다. 두 시스템은 같은 페이지 결과물을 저장한다는 공통점이 있지만, 저장 위치, 사용 시점, 캐시 대상, 목적이 완전히 다르다.


5-3. 작동 방식

Client-side Router Cache는 아래와 같이 동작한다.

1. 경로 방문
    → RSC 페이로드 캐싱
2. 재방문
    → 캐시 사용, 서버 요청 생략
3. 프리페치
    → 이동 가능 경로 캐싱
4. 새로고침
    → 캐시 삭제

5-4. 작동 범위 및 지속 시간

Client-side Router Cache는 사용자가 사이트를 탐색하면서 각 경로 세그먼트(Layout, Loading, Page 등)의 RSC Payload를 클라이언트 메모리에 저장하는 방식으로 동작한다. 이 캐시는 일시적이기 때문에, 페이지를 새로 고침하거나 브라우저 탭을 닫으면 모두 삭제된다.

  • 레이아웃(Layout 세그먼트)과 로딩 상태(Loading 세그먼트)는 자동으로 캐시되고, 이후 페이지 탐색 시 재사용되어 빠른 전환을 가능하게 한다.
  • 페이지(Page 세그먼트)는 기본적으로 캐시되지 않지만, 브라우저 뒤로/앞으로 탐색 시에는 캐시된 페이지가 재사용된다. staleTimes 옵션을 사용하면 페이지도 캐시 대상으로 지정할 수 있다.

하지만 단순 탐색 중에는 캐시가 일정 시간 동안 유지되며, 이 유지 시간은 "해당 세그먼트가 prefetch(사전 로딩) 되었는가, 그리고 어떻게 되었는가"에 따라 달라진다. 즉, 프리페치(prefetch) 설정 방식에 따라 세그먼트별 자동 캐시 만료 시간이 달라진다.

  • prefetch={null} 또는 생략: 정적 세그먼트는 5분 유지, 동적 세그먼트는 캐시되지 않음 (0초)
  • prefetch={true} 또는 router.prefetch(): 정적·동적 세그먼트 모두 5분간 유지

프리페치로 캐시된 각 세그먼트는 프리패치된 시점부터 개별적으로 만료되며, 전체 페이지 단위가 아닌 세그먼트 단위로 관리된다. 즉, 하나의 페이지가 여러 세그먼트로 구성되어 있다면, 어떤 세그먼트는 캐시가 유지되고, 다른 세그먼트는 만료될 수도 있다.

프리페치(prefetch)는 사용자가 아직 클릭하지 않았지만, 앞으로 클릭할 가능성이 있는 경로의 데이터를 미리 불러오는 기능이다.

Next.js에서는 <Link> 컴포넌트가 자동으로 이 작업을 수행한다. 사용자가 스크롤로 <Link> 태그를 화면에 보이게 하거나, 마우스를 올리면 Next.js가 백그라운드에서 해당 라우트의 데이터를 미리 요청한다.


5-5. 캐시 무효화 (Invalidation)

Router Cache는 자동으로 만료되기도 하지만, 특정 동작에 의해 즉시 무효화(초기화) 될 수도 있다.

주로 서버 액션 또는 클라이언트 동작을 통해 발생하며, 다음과 같은 경우 캐시가 강제로 비워지고, 다음 요청 시 서버에 새로 요청하게 된다.


1. router.refresh() 호출 시

클라이언트 컴포넌트에서 useRouter()를 통해 router.refresh()를 호출하면,

현재 라우트의 클라이언트 캐시가 무효화된다.

'use client'
import { useRouter } from 'next/navigation'

export default function RefreshButton() {
  const router = useRouter()
  return <button onClick={() => router.refresh()}>새로고침</button>
}

이 경우, 브라우저는 서버에 새 요청을 보내고, 최신 RSC Payload를 받아 다시 렌더링한다.

단, 이 호출은 Data Cache나 Full Route Cache에는 영향을 주지 않는다. 즉, 클라이언트 캐시만 무효화된다.


2. cookies.set() 또는 cookies.delete() 사용 시

Server Action에서 인증 관련 쿠키를 설정하거나 삭제하면, 해당 라우트의 Router Cache가 무효화된다.

이는 인증 상태에 따라 페이지가 달라져야 하는 경우, 오래된 클라이언트 상태를 계속 보여주지 않기 위한 안전장치다.


3. revalidatePath() 또는 revalidateTag() 호출 시

서버 액션에서 특정 경로(path)나 태그(tag)를 기준으로 데이터를 다시 가져오도록 요청하면, 해당 라우트의 Data Cache뿐 아니라 Router Cache까지도 무효화된다. 즉, 데이터가 바뀌면 화면도 반드시 최신 상태로 바뀌도록 보장해주는 흐름이다.

📌 단, 이 캐시 무효화는 Server Action 안에서 호출될 때 Router Cache까지 영향을 준다.

Route Handler에서 호출한 경우에는 Router Cache에는 영향을 주지 않는다.


5-6. 요약

  • 클라이언트에서 서버 요청 없이 빠르게 페이지 전환을 가능하게 하는 캐시 시스템이다.
  • 서버에서 받은 RSC Payload브라우저 메모리에 저장해두고, 이후 같은 경로로 다시 이동할 때 서버 요청 없이 즉시 렌더링한다.
  • 이는 브라우저 히스토리 이동(back/forward) 시 특히 효과적이며, React 상태나 스크롤 위치 등도 유지된다.
  • 서버에 요청을 보내지 않도록 막는 유일한 캐시이며, Full Route Cache와는 역할이 다르다.
  • Layout, Loading 세그먼트는 항상 캐시되고 재사용되며, Page 세그먼트는 기본적으로 캐시되지 않지만 설정(staleTimes)으로 활성화할 수 있다.
  • 페이지를 새로 고침하면 캐시가 사라지며, 자동 만료 시간은 프리페치 설정에 따라 달라진다.
  • router.refresh(), cookies.set(), revalidatePath() 등의 호출은 이 Router Cache를 즉시 무효화할 수 있다.

6. Cache Interactions – 캐시 간 상호작용

Next.js는 다양한 캐시 메커니즘을 제공하는데, 이들 사이에는 상호작용(interactions)이 존재한다. 즉, 하나의 캐시를 무효화하거나 갱신하는 행위가, 다른 캐시에 연쇄적으로 영향을 줄 수도 있고, 전혀 영향을 주지 않을 수도 있다.

아래는 각 캐시 간의 구체적인 상호작용 관계를 설명한 내용이다.


6-1. Data Cache ↔ Full Route Cache

Data Cache가 무효화되면 → Full Route Cache도 무효화됨

fetch()로 가져온 데이터가 페이지 렌더링에 사용되기 때문에, 해당 데이터가 바뀌면 페이지 전체를 다시 렌더링해야 한다. 따라서 Data Cache가 무효화되면, 이 데이터를 사용한 페이지의 Full Route Cache도 함께 무효화된다.


Full Route Cache를 무효화해도 → Data Cache는 그대로 유지됨

반대로, Full Route Cache만 무효화하고 새로 렌더링하더라도, 내부에서 사용하는 fetch() 요청의 결과가 여전히 Data Cache에 저장되어 있다면 그 데이터를 그대로 다시 사용한다. 즉, 렌더링은 다시 하지만, fetch는 재사용됨.

이렇게 하면 한 페이지 안에서 일부 fetch만 캐시를 끄고, 나머지는 캐시를 그대로 사용할 수 있는 하이브리드 캐싱 구조를 만들 수 있다.


6-2. Data Cache ↔ Router Cache

Server Action에서 revalidatePath, revalidateTag 호출 시 → Router Cache도 무효화됨

Server Action에서 revalidatePath() 또는 revalidateTag()를 호출하면, 해당 경로의 Router Cache까지 무효화된다.

'use server'

import { revalidatePath } from 'next/cache'

export async function submitForm() {
  await updateDB()
  revalidatePath('/posts')
}

위 코드 실행 시

  • /posts 관련 Data Cache
  • /postsFull Route Cache
  • /postsRouter Cache (클라이언트 메모리 캐시)

모두 무효화되어 다음 방문 시 서버에서 새로 데이터와 페이지를 받아온다.


Route Handler에서는 Router Cache 무효화되지 않음

만약 동일한 revalidatePath() 또는 revalidateTag()Route Handler(app/api/*) 내부에서 호출하면, Data Cache와 Full Route Cache는 무효화되지만 Router Cache는 무효화되지 않는다.

왜냐하면 Route Handler는 특정 라우트와 1:1로 연결되어 있지 않기 때문이다.

결과적으로 클라이언트는 이전 페이지의 캐시된 상태를 유지하고, 자동 갱신되지 않으며, 사용자가 새로고침하거나 자동 무효화 시간이 지나야 서버에 재요청이 발생한다.


6-3. Full Route Cache ↔ Router Cache

이 둘은 구조상 서로 다른 위치(서버 vs 브라우저)에 존재하지만, 간접적으로 연결되어 있다.

  • 서버의 Full Route Cache가 무효화되면 → 클라이언트에서 Router Cache를 통해 해당 경로를 다시 방문할 때 서버에서 새로운 Payload를 받아오게 된다.
  • 하지만 이미 클라이언트에 저장된 Router Cache가 있으면, 기존 내용을 먼저 보여주고, 이후 자동 무효화가 발생하거나 router.refresh()로 수동 갱신할 수 있다.

결국 Router Cache는 자동으로 갱신되진 않기 때문에, 정확한 반영이 필요한 경우에는 명시적으로 새로고침을 유도해야 한다.


6-4. Request Memoization과의 관계

Request Memoization은 서버 렌더링 중 fetch를 중복 제거하는 일시적 캐시이기 때문에, 다른 캐시들과는 직접적인 상호작용은 없다.

  • 단일 요청 처리 중에만 동작하며
  • 요청이 끝나면 메모이제이션도 초기화된다

하지만, Data Cache 내부에서 같은 fetch가 여러 번 호출되면, 여전히 memoization은 중복 요청 방지로 작동한다.


참고문헌

4개의 댓글

comment-user-thumbnail
2025년 4월 22일

우와! Next.js 캐싱을 이렇게 자세하게 설명하는 기사를 처음 봤어요! 이 글 덕분에 서버 측 캐싱과 클라이언트 측 캐싱의 차이점을 확실히 이해하게 되었습니다. 저는 보통 AiCloneUI를 사용해 개발을 했지만, 이게 뭔지 잘 모르겠어요.

1개의 답글
comment-user-thumbnail
2025년 4월 27일

요즘 next.js 공부중이여서 글을 보고 확실히 도움 받았습니다👍👍👍👍

1개의 답글