Tech Sep 23, 2026

Next.js에서 만나는 웹 캐시: 무엇을 다시 하지 않기 위해 저장할까

상품 상세 페이지를 열려면 서버에서 상품 정보를 조회하고, 화면을 만들고, 이미지와 코드를 브라우저로 전송해야 합니다. 사용자가 목록으로 돌아갔다가 같은 상품을 다시 열면 조금 전에 했던 작업을 상당 부분 반복하게 됩니다. 상품 정보와 코드가 바뀌지 않았다면 그 결과를 재사용해 기다리는 시간을 줄일 수 있습니다.

그런데 웹에서는 캐시 하나가 이 모든 작업을 맡지 않습니다. 파일을 다시 다운로드하지 않기 위한 캐시, 화면 전환에 쓸 결과를 보관하는 캐시, 서버의 데이터 조회를 줄이는 캐시가 서로 다른 곳에서 동작합니다. 각 캐시에 무엇을 저장하는지와 함께 어떤 작업을 줄이려는지 살펴보면, 캐시가 필요한 이유와 적용할 위치를 이해할 수 있습니다.

이 글에서는 상품 페이지를 여는 흐름을 따라 브라우저, CDN, Next.js 서버의 캐시를 살펴본 뒤, 저장된 값을 언제까지 사용하고 어떻게 갱신할지 정리해 보려고 합니다. Next.js는 App Router를 기준으로 설명하며, 기존 캐시 모델과 Next.js 16의 Cache Components를 구분합니다.

페이지를 다시 열 때 반복되는 작업

브라우저가 상품 페이지를 요청하면 서버는 상품 데이터를 조회해 응답을 만들고, 브라우저는 받은 HTML과 코드를 바탕으로 화면을 구성합니다. 이 과정에서 어떤 작업이 반복되는지 나눠 보면, 각 캐시가 어디에서 비용을 줄이는지 알 수 있습니다.

반복되는 작업재사용할 수 있는 결과저장하기 좋은 위치
같은 파일을 다시 다운로드HTTP 응답브라우저
방문했던 화면으로 다시 이동화면 상태나 화면 전환에 필요한 결과브라우저·클라이언트 라우터
여러 사용자가 같은 응답을 요청공개 HTTP 응답CDN
서버가 같은 데이터를 다시 조회API·DB 조회 결과애플리케이션 서버
같은 페이지를 다시 렌더링완성된 페이지의 렌더 결과애플리케이션 서버

캐시(cache)는 다시 가져오거나 계산하는 비용을 줄이기 위해 결과를 저장해 두고 재사용하는 장치입니다.

캐시에 사용할 값이 있으면 적중(hit), 없어서 원래 작업을 해야 하면 미적중(miss)이라고 부릅니다. 브라우저에서 적중하면 서버까지 요청을 보내지 않을 수 있고, 서버의 데이터 캐시에서 적중하면 요청은 받되 DB 조회를 생략할 수 있습니다. 둘 다 캐시 적중이지만 줄이는 비용은 다릅니다.

먼저 브라우저가 이미 받은 응답과 화면을 어떻게 재사용하는지 살펴보겠습니다.

브라우저: 이미 받은 응답과 화면을 재사용합니다

HTTP 캐시로 같은 파일의 다운로드를 줄입니다

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

상품 목록과 상세 페이지에서 같은 로고와 CSS를 사용한다고 가정해 보겠습니다. 페이지를 이동할 때마다 이 파일들을 다시 다운로드하면 내용은 그대로인데 전송 비용과 대기 시간이 반복됩니다. 브라우저의 HTTP 캐시는 받은 HTTP 응답을 저장해, 다시 요청한 자원을 로컬에서 사용할 수 있게 합니다. 이처럼 이미 받은 응답을 재사용하는 기능은 HTTP/1.0 명세에도 포함되어 있었습니다.

이 캐시는 브라우저 프로필마다 따로 있으므로, 한 사용자가 받은 응답은 그 사용자만 재사용합니다. 최근 브라우저는 요청한 사이트별로도 캐시를 나눕니다. 캐시 적중 여부를 이용해 다른 사이트의 방문 기록을 추측하지 못하게 하기 위해서입니다. 그래서 두 쇼핑몰이 같은 CDN 주소의 라이브러리를 쓰더라도, 한쪽에서 받은 파일을 다른 쪽에서 꺼내 쓰지 않습니다.

저장 대상은 이미지나 정적 파일에 한정되지 않습니다. HTML과 API 응답도 캐시 정책이 허용하면 저장할 수 있으며, 재사용할 수 있는 동안에는 네트워크 전송을 생략합니다.

개발자 도구에 표시되는 memory cache와 disk cache는 이 응답을 메모리와 디스크 중 어디에서 가져왔는지를 보여줍니다. 두 저장 위치를 별개의 캐시 API로 설정하지는 않습니다. 디스크에 저장된 응답은 브라우저를 다시 열어도 남을 수 있으며, 신선도와 저장 공간 조건에 따라 재사용됩니다. MDN HTTP 캐싱

다만 상품 이미지와 가격 정보에 같은 재사용 기간을 적용하기는 어렵습니다. 바뀌지 않는 파일은 오래 사용해도 되지만 가격은 갱신 여부를 더 자주 확인해야 합니다. 브라우저가 이 판단을 할 수 있도록 서버가 응답에 Cache-Control 같은 정책을 담으며, 구체적인 설정은 뒤에서 살펴보겠습니다.

뒤로가기를 빠르게 만드는 bfcache

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

파일을 다시 받지 않아도 화면을 만드는 작업은 남습니다. HTML을 해석해 DOM을 구성하고 JavaScript를 실행해야 한다면, 같은 페이지로 돌아올 때도 준비 시간이 필요합니다. 특히 상품 목록을 스크롤하다 상세 페이지를 열고 뒤로 돌아왔을 때는 목록을 처음부터 만드는 것보다 떠나기 직전 상태를 복원하는 편이 자연스럽습니다. Chrome 사용 데이터에서도 데스크톱 탐색의 10분의 1, 모바일 탐색의 5분의 1이 뒤로·앞으로 이동이었습니다.

브라우저의 bfcache(back/forward cache)는 이런 문서 간 뒤로·앞으로 이동을 위해 페이지의 상태를 보관합니다. 개별 HTTP 응답을 저장하는 대신 DOM과 JavaScript 힙을 포함한 상태를 유지해 두었다가 복원하므로, 파일을 받아 다시 실행하는 과정까지 줄일 수 있습니다.

bfcache에 저장한 상태는 각 탭의 방문 기록에 연결되어 그 탭의 뒤로·앞으로 이동에만 쓰입니다. 페이지 전체를 메모리에 보관하므로 일정 시간이 지나거나 메모리가 부족하면 브라우저가 제거합니다. web.dev의 bfcache 설명

복원한 화면에는 떠날 때의 상품 정보도 남아 있을 수 있습니다. 복귀 시 최신 상태를 확인해야 하는 화면은 pageshow 이벤트의 persisted 값으로 bfcache 복원을 감지해 필요한 데이터를 다시 조회할 수 있습니다. 이렇게 페이지 복원과 데이터 갱신을 나누면 화면 전체를 처음부터 만들지 않고도 바뀐 정보를 반영할 수 있습니다.

Next.js 화면 전환을 위한 Router Cache

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

Next.js 앱 안에서 링크를 누를 때는 다른 문서를 통째로 여는 대신 현재 화면의 일부를 바꾸는 클라이언트 탐색이 일어날 수 있습니다. 상품 목록에서 상세로 이동해도 공통 헤더와 레이아웃은 유지하고, 바뀌는 영역에 필요한 서버 결과만 받아 반영하는 방식입니다. 이때는 문서 전체를 복원하는 bfcache와 별개로 라우터가 재사용할 데이터가 필요합니다.

App Router는 서버 컴포넌트의 렌더 결과를 RSC Payload라는 형식으로 전달합니다. 여기에는 서버 컴포넌트의 결과와 클라이언트 컴포넌트를 연결하는 데 필요한 정보가 들어 있으며, 클라이언트는 이를 사용해 화면을 갱신합니다.

Router Cache는 이 결과를 라우트를 구성하는 세그먼트 단위로 브라우저 메모리에 보관합니다. 다음 탐색에서 필요한 세그먼트가 캐시에 있으면 서버에서 다시 가져오지 않고 재사용할 수 있습니다. 한 탭의 앱 안에서만 유지되므로 새로고침하거나 새 탭에서 열면 빈 상태로 시작합니다. Next.js의 화면 전환

링크를 누르기 전에 목적지 데이터를 가져오는 prefetch의 결과도 Router Cache에 보관됩니다. 사용자가 링크를 누를 때 필요한 결과가 이미 준비되어 있으면 화면 전환에 걸리는 시간을 줄일 수 있습니다.

Router Cache가 재사용하는 범위와 기간은 버전과 탐색 방식에 따라 달라집니다. Next.js 15에서는 일반 탐색 시 page 세그먼트 재사용의 기본 동작이 바뀌었지만, 공통 레이아웃이나 뒤로·앞으로 탐색에서의 재사용까지 모두 없어진 것은 아닙니다. Next.js 15 업그레이드 가이드

브라우저에서 재사용하는 것맡는 캐시줄이는 작업
이미지·코드·API의 HTTP 응답HTTP 캐시자원 다운로드
떠나기 직전의 문서 상태bfcache뒤로·앞으로 이동 시 페이지 복원 비용
앱 내부 탐색에 필요한 RSC 세그먼트Router Cache화면 전환에 필요한 서버 결과 요청

화면 안에서 반복하는 API 조회는 쿼리 캐시로 공유합니다

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

라우터가 화면 전환을 처리하더라도 화면 안의 데이터 조회는 계속 발생합니다. 상품 상세의 가격 표시와 장바구니 버튼이 같은 상품 API를 사용하거나, 탭을 바꿀 때마다 같은 정보를 가져오는 경우가 그렇습니다. 각 컴포넌트가 따로 요청하고 결과를 보관하면 중복 요청뿐 아니라 로딩·오류·갱신 상태도 각각 관리해야 합니다. 서버 데이터는 비동기로 도착하고 시간이 지나면 원본과 달라지므로, 일반 전역 상태에 저장하더라도 로딩과 오류 처리, 데이터 갱신은 직접 구현해야 합니다.

TanStack Query와 SWR은 이런 서버 데이터를 전담하는 라이브러리로, 조회 결과를 쿼리 키로 식별해 여러 컴포넌트가 공유할 수 있게 합니다. 예를 들어 ['product', '42', 'ko']라는 키는 한국어로 조회한 상품 42의 결과를 나타냅니다. 동일한 키를 사용하는 컴포넌트는 같은 데이터 상태를 구독하므로, 결과가 갱신되면 그 데이터를 보여주는 UI도 함께 갱신할 수 있습니다.

useQuery({
  queryKey: ['product', productId, locale],
  queryFn: async () => {
    const response = await fetch(
      `/api/products/${encodeURIComponent(productId)}?locale=${encodeURIComponent(locale)}`,
    );
    if (!response.ok) throw new Error('상품 조회에 실패했습니다.');
    return response.json();
  },
  staleTime: 60_000,
  gcTime: 10 * 60_000,
});

TanStack Query에서 staleTime은 데이터를 신선하게 취급하는 기간이고, gcTime은 더 이상 사용 중인 컴포넌트가 없는 쿼리를 메모리에 보관하는 기간입니다. 위 설정에서는 결과를 1분 동안 신선하게 취급하며, 비활성 상태가 된 뒤에는 10분 동안 보관합니다.

결과는 QueryClient의 메모리에 저장됩니다. 같은 QueryClient를 사용하는 한 탭의 앱 안에서 공유되며, 새로고침하면 사라집니다. 신선도 기간이 끝났다고 즉시 삭제하거나 타이머가 매번 API를 호출하는 것은 아니며, 마운트·창 포커스·재연결 같은 계기에 재조회할 수 있습니다. TanStack Query 기본 동작

쿼리 캐시와 HTTP 캐시는 하나의 조회 과정에서 함께 동작할 수 있습니다. 쿼리 캐시가 결과를 재사용하면 queryFn을 실행할 필요가 없고, queryFn이 실행되더라도 그 안의 fetch는 브라우저 HTTP 캐시에서 응답을 받을 수 있습니다. 따라서 쿼리를 재조회했다는 사실만으로 원본 서버가 새 데이터를 반환했다고 판단할 수는 없습니다.

Service Worker로 오프라인 응답을 직접 제어합니다

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

네트워크가 끊겨도 설명서를 열거나, 이미지 요청이 실패했을 때 저장해 둔 대체 이미지를 보여주려면 앱이 요청 처리 방식을 직접 결정해야 합니다. HTTP 캐시는 서버가 보낸 헤더를 보고 브라우저가 재사용 여부를 판단하므로, 네트워크가 없을 때 무엇을 보여줄지 앱이 정할 수 없습니다.

Service Worker는 페이지와 별도의 실행 환경에서 동작하며, 자신이 제어하는 페이지의 요청을 가로채 응답할 수 있습니다. Cache Storage는 Service Worker 등이 요청과 응답을 저장하는 데 사용하는 저장소입니다. 둘을 함께 사용하면 네트워크 요청을 보내기 전에 저장된 응답을 찾거나, 네트워크 실패 시 그 응답으로 대체할 수 있습니다. MDN Service Worker API

전략요청을 처리하는 순서활용할 수 있는 상황
cache-first저장된 응답을 먼저 찾고, 없으면 네트워크 요청버전이 고정된 정적 자산
network-first네트워크를 먼저 시도하고 실패하면 저장된 응답 사용오프라인에서도 열어야 하는 문서
stale-while-revalidate저장된 응답을 먼저 주면서 네트워크로 사본 갱신이전 내용을 잠시 보여줘도 되는 목록

Cache Storage는 브라우저의 HTTP 캐시와 별도로 출처(origin)별 디스크 공간에 저장됩니다. Cache API는 응답의 Cache-Control을 보고 자동으로 만료시키지 않으므로, 앱이 지우거나 브라우저가 저장 공간 부족으로 정리하기 전까지 항목이 남습니다. 따라서 앱에서 만료와 삭제를 관리해야 합니다. 정적 자산이라면 캐시 이름에 버전을 붙이고 새 버전을 사용할 때 이전 저장소를 정리할 수 있습니다. MDN Cache API

localStorage와 IndexedDB는 캐시를 담을 수 있는 저장소입니다

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

HTTP 응답이 아니라 최근 본 상품 ID나 사용자가 내려받은 문서 데이터를 보관하고 싶을 수도 있습니다. 이때는 localStorage, sessionStorage, IndexedDB를 사용할 수 있습니다. 세 저장소 모두 출처별로 나뉘어 같은 사이트의 페이지끼리만 읽을 수 있습니다. 이들은 쿠키보다 큰 데이터를 요청마다 서버로 보내지 않고 브라우저에 보관할 수 있는 범용 저장소입니다. 캐시로 활용하려면 데이터를 저장하는 것 외에 만료와 갱신 정책도 직접 정해야 합니다.

저장소데이터와 접근 방식보관 범위의 특징
localStorage문자열 키·값, 동기 접근브라우저를 다시 열어도 유지되며 기본 만료 시간이 없음
sessionStorage문자열 키·값, 동기 접근탭의 페이지 세션 단위로 분리
IndexedDB구조화된 데이터, 비동기 접근큰 데이터와 인덱스 기반 조회에 활용

상품 데이터를 캐시로 쓴다면 값과 함께 만료 시각을 저장하고, 읽을 때 유효한지 판단할 수 있습니다. 저장소에 오래 남는 데이터는 앱을 업데이트한 뒤에도 읽게 되므로 형식이 바뀌었는지 판단할 버전도 필요합니다. 사용자별 정보를 보관했다면 로그아웃이나 계정 전환 시 정리하는 정책도 함께 둡니다. MDN 브라우저 저장소

브라우저 안의 JavaScript 변수나 Map에도 값을 저장할 수 있습니다. 이렇게 저장한 값은 현재 실행 중인 앱에서 재사용하기 편하지만, 새 문서를 로드하면 사라집니다. useMemo, useCallback, memo 역시 계산 결과·함수 참조·렌더링의 재사용을 돕지만, 영속 저장소나 서버 데이터 갱신 정책을 제공하는 기능은 아닙니다. React useMemo

CDN: 다른 사용자가 받은 응답도 재사용합니다

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

브라우저의 캐시는 한 사용자가 이미 받은 결과를 재사용하는 데 유용합니다. 그러나 만 명이 같은 상품 이미지를 처음 열면 각자의 브라우저에는 그 이미지가 없으므로 서버가 같은 파일을 반복해서 보내야 합니다. 서버가 멀리 있다면 파일을 가져오는 네트워크 지연도 사용자마다 발생합니다.

CDN(Content Delivery Network)은 여러 지역의 서버를 통해 콘텐츠를 전달하는 네트워크입니다. CloudFront 같은 CDN의 캐시는 사용자 요청을 받는 엣지 위치에 응답을 저장해 두고, 이후 같은 응답이 필요한 요청에 재사용합니다. 한 엣지의 사본은 그 엣지로 요청을 보내는 모든 사용자가 나눠 쓰지만, 엣지마다 사본이 따로 있어서 서울에서 적중한 응답이 도쿄 엣지에서는 미적중일 수 있습니다.

CDN이 응답을 받아오는 서버를 오리진(origin)이라고 부릅니다. CDN은 오리진에서 S3의 파일이나 Next.js 서버가 만든 응답을 가져와 보관할 수 있습니다. Amazon CloudFront

캐시가 비어 있으면 CDN이 오리진에서 응답을 가져와 사용자에게 전달하고, 저장 가능한 응답이면 보관합니다. 다음 요청이 같은 캐시 항목을 사용할 수 있으면 CDN에서 응답하므로 오리진의 전송량과 요청 처리를 줄일 수 있습니다. 저장된 응답은 TTL 동안 재사용되지만, 요청이 드문 항목은 기간이 남아도 공간을 확보하려고 먼저 제거될 수 있습니다. 여러 사용자가 응답을 공유한다는 점에서 브라우저 HTTP 캐시와 역할이 다릅니다.

여러 사용자에게 같은 상품 이미지를 제공할 수는 있지만, 사용자마다 다른 주문 내역까지 공유해서는 안 됩니다. 같은 URL로 요청하더라도 응답 내용이 다를 수 있기 때문입니다. 어떤 요청끼리 응답을 나눠 쓸 수 있는지는 뒤에서 캐시 키와 공유 정책으로 구체화하겠습니다.

CDN과 비슷하게 서버 앞에서 요청을 받는 리버스 프록시나 API 게이트웨이에도 응답 캐시를 둘 수 있습니다. 리버스 프록시는 요청을 실제 애플리케이션 서버로 전달하는 중간 서버이고, 여기에 캐시를 두면 앱에 들어오는 반복 요청을 줄입니다. CDN은 여기에 지리적으로 분산된 전달망이 더해진 형태로 이해할 수 있으며, 프록시를 설치했다고 응답 캐시가 자동으로 활성화되는 것은 아닙니다.

Next.js 서버: 데이터 조회와 렌더링을 재사용합니다

CDN이 응답하지 못한 요청은 Next.js 서버에 도착합니다. 서버는 상품 정보를 읽어 화면을 만들어야 하지만, 사용자마다 다른 화면을 만들더라도 모든 작업을 새로 할 필요는 없습니다. 개인 할인 영역은 사용자마다 달라도 상품 이름과 설명은 여러 요청이 같은 조회 결과를 사용할 수 있습니다.

서버 캐시는 이처럼 응답을 만드는 내부 작업을 줄입니다. 같은 렌더에서 반복되는 호출, 다음 요청에서도 반복되는 데이터 조회, 완성된 페이지의 재생성을 서로 구분해 보겠습니다.

한 화면에서 중복되는 조회를 합치는 Request Memoization

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

상품 제목, 상세 설명, 추천 영역에서 각각 같은 상품 정보가 필요하다고 가정해 보겠습니다. 서버 컴포넌트에서는 부모가 데이터를 모아 내려주기보다 각 컴포넌트가 필요한 데이터를 직접 조회하는 방식을 권장합니다. 이렇게 하면 각 컴포넌트를 독립적으로 구성할 수 있지만, 한 화면을 렌더링하는 동안 같은 상품을 여러 번 조회할 수 있습니다. Request Memoization은 한 번의 렌더링 안에서 같은 호출의 결과를 재사용해 중복 조회를 줄입니다.

React 서버 컴포넌트 트리 안에서는 여러 컴포넌트가 같은 URL과 옵션으로 GET fetch를 호출하면 결과를 공유할 수 있습니다. 각 컴포넌트가 필요한 곳에서 데이터를 조회해도 실제 요청은 중복해서 보내지 않는 것입니다. 다만 이 동작은 React의 서버 렌더링 안에서만 적용되므로, 컴포넌트 트리 밖에 있는 Route Handler에서는 요청을 자동으로 합쳐 주지 않습니다. Next.js 요청 중복 제거

DB를 직접 조회하는 함수도 React의 cache로 감싸면 결과를 재사용할 수 있습니다. 아래처럼 모듈에서 한 번 감싼 함수를 여러 서버 컴포넌트가 가져다 쓰면, 같은 문자열 ID로 호출할 때 조회 결과를 공유합니다.

import { cache } from 'react';
import { db } from '@/lib/db';

export const getProductForRender = cache(async (id: string) => {
  return db.product.findUnique({ where: { id } });
});

이 결과는 한 요청을 처리하는 동안에만 재사용합니다. 서버 메모리에 요청별로 보관했다가 렌더링이 끝나면 버리기 때문에, 같은 사용자가 새로고침하거나 다른 사용자가 페이지를 열면 다시 조회합니다.

여러 컴포넌트가 결과를 공유하려면 cache로 감싼 동일한 함수를 같은 인수로 호출해야 합니다. 특히 객체를 인수로 넘길 때는 내용이 같아도 참조가 다르면 다른 입력으로 취급될 수 있다는 점에 주의해야 합니다. React cache

다음 요청에서도 데이터를 재사용하는 Data Cache

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

한 번의 렌더링 안에서 중복 조회를 줄여도 다음 사용자가 같은 상품을 열면 다시 조회해야 합니다. 상품 설명처럼 자주 바뀌지 않는 데이터라면 요청 사이에서도 결과를 저장해 외부 API나 DB의 부하를 줄일 수 있습니다. 기존 Next.js 캐시 모델의 Data Cache가 이 역할을 맡습니다.

Data Cache는 서버의 확장된 fetch로 가져온 응답이나 unstable_cache로 감싼 함수의 결과를 재사용합니다. 예를 들어 상품 상세와 검색 결과 페이지가 같은 상품 정보를 읽는다면, 페이지가 달라도 해당 조회의 캐시를 활용할 수 있습니다. 전체 화면을 저장하지 않고 데이터만 보관하므로 개인화된 화면의 공통 데이터에도 적용할 수 있습니다.

저장된 항목은 같은 데이터를 조회하는 여러 요청과 사용자가 공유하며, 재검증하거나 캐시를 끄기 전까지 유지됩니다. 직접 운영하는 서버에서는 기본적으로 각 인스턴스의 메모리와 디스크에 저장됩니다.

// Cache Components를 사용하지 않는 서버 측 코드입니다.
export async function getProduct(id: string) {
  const response = await fetch(`https://api.example.com/products/${id}`, {
    cache: 'force-cache',
    next: { revalidate: 60, tags: [`product:${id}`] },
  });
  if (!response.ok) throw new Error('상품 조회에 실패했습니다.');
  return response.json();
}

force-cache는 저장된 결과를 찾아 재사용하도록 하고, revalidate: 60은 시간 기반 갱신 간격을 설정합니다. tags는 관련 항목을 찾아 갱신할 수 있도록 붙이는 이름표입니다. 이 코드에서는 상품 42를 수정했을 때 product:42 태그를 통해 해당 데이터의 갱신을 요청할 수 있습니다. Next.js의 캐시와 재검증

같은 fetch 옵션이라도 실행 위치를 구분해야 합니다. 브라우저에서 cache: 'no-store'는 브라우저 HTTP 캐시를, Next.js 서버에서는 Data Cache 사용을 제어합니다. 서버에서 데이터 캐시를 끈다고 최종 응답의 CDN 캐시나 외부 API의 캐시까지 함께 꺼지지는 않습니다. Next.js fetch API

Request Memoization과 Data Cache는 모두 같은 조회를 반복하지 않게 하지만, 재사용하는 범위가 다릅니다. 아래에서 두 사용자의 요청이 각각 같은 상품을 세 번 조회할 때, 캐시 방식에 따라 DB까지 가는 호출이 몇 번인지 비교할 수 있습니다.

같은 상품을 여섯 번 조회할 때 DB까지 가는 호출

캐시 방식을 바꾸면 어떤 호출이 결과를 재사용하는지 달라집니다.

요청 1 · 사용자 A
  • 상품 제목 DB 조회
  • 상세 설명 DB 조회
  • 추천 영역 DB 조회
요청 2 · 사용자 B
  • 상품 제목 DB 조회
  • 상세 설명 DB 조회
  • 추천 영역 DB 조회
DB 2 / 6 호출
설명용 모형 · 두 요청 모두 getProduct('42')를 세 컴포넌트에서 호출합니다.

Request Memoization의 결과는 요청이 끝나면 사라지므로 요청마다 첫 호출은 DB를 조회합니다. Data Cache를 함께 사용하면 요청 1에서 저장한 결과를 요청 2의 첫 호출에서도 재사용할 수 있습니다.

화면을 만드는 작업까지 줄이는 Full Route Cache

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

Data Cache에서 상품 정보를 가져왔어도 서버는 컴포넌트를 렌더링해 응답을 만들어야 합니다. 공개 상품 소개처럼 같은 데이터로 같은 화면을 만든다면, 조회 결과뿐 아니라 완성된 렌더 결과도 재사용할 수 있습니다. 요청마다 같은 화면을 렌더링하는 데 드는 CPU 작업과 대기 시간을 줄이기 위해서입니다. 기존 모델의 Full Route Cache는 정적 렌더링으로 만든 HTML과 RSC Payload를 서버에 저장해, 해당 경로를 요청한 모든 사용자에게 같은 결과를 제공합니다.

앞에서 본 Router Cache와는 저장 위치가 다릅니다. Full Route Cache는 서버가 여러 요청에 반환할 결과를 보관하고, Router Cache는 브라우저가 화면 전환에 사용할 세그먼트를 보관합니다. 서버에 렌더 결과가 있어도 브라우저에는 아직 없을 수 있고, 브라우저에 필요한 결과가 있으면 서버에 요청하지 않고 사용할 수도 있습니다. Next.js 캐시 모델

이 렌더 결과를 언제 만들고 갱신하느냐에 따라 SSG와 ISR을 구분합니다. SSG는 빌드 시점에 페이지를 생성하는 방식이고, ISR은 배포 후에도 정적 결과를 다시 생성해 갱신하는 방식입니다. ISR을 사용하면 저장된 페이지를 계속 제공하면서 필요할 때 다시 생성할 수 있습니다. 이렇게 만든 결과는 새로 배포하면 처음부터 다시 생성됩니다.

데이터와 렌더 결과 사이에는 의존 관계도 있습니다. 상품 데이터가 바뀌면 이를 사용한 페이지 결과도 다시 만들어야 하지만, 페이지 결과를 지웠다고 다른 페이지에서 재사용하는 데이터까지 모두 버릴 필요는 없습니다. 이 때문에 Data Cache와 Full Route Cache의 무효화는 같은 작업이 아닙니다. 캐시 간 관계

공통 영역만 재사용하도록 표시하는 Cache Components

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

한 페이지에는 공통 상품 설명과 사용자별 배송 정보가 함께 들어갈 수 있습니다. 페이지 전체를 정적 또는 동적으로 구분하는 것만으로는 공통 부분을 어디까지 재사용할지 표현하기 어렵습니다. 게다가 기존 모델에서는 fetch 옵션과 동적 API 사용 여부에 따라 캐시가 암묵적으로 켜지고 꺼져, 어느 부분이 재사용되는지 코드만 보고 알기도 어려웠습니다. Next.js 16의 Cache Components는 함수나 컴포넌트에 use cache를 표시해 재사용할 부분을 명시하고, 요청 시점에 필요한 부분과 함께 구성하도록 합니다. Cache Components 가이드

다음 코드는 공통 상품 설명을 가져오는 함수의 결과를 캐시합니다. cacheLife는 재사용과 갱신 시점을, cacheTag는 상품 수정 시 갱신할 대상을 지정합니다.

// next.config.ts
import type { NextConfig } from 'next';

export default {
  cacheComponents: true,
} satisfies NextConfig;
// lib/products.ts
import { cacheLife, cacheTag } from 'next/cache';
import { db } from '@/lib/db';

export async function getPublicProduct(id: string) {
  'use cache';
  cacheLife({ stale: 60, revalidate: 60, expire: 300 });
  cacheTag(`product:${id}`);
  return db.product.findUnique({
    where: { id },
    select: { id: true, name: true, description: true },
  });
}

이 함수는 같은 입력에 대한 결과를 재사용하며, 캐시 키에는 함수 식별자와 인수, 빌드를 구분하는 값 등이 들어갑니다. 그래서 같은 입력으로 호출한 요청은 사용자가 달라도 결과를 공유하고, 새로 배포하면 이전 결과를 쓰지 않습니다. 결과는 빌드 시점에 페이지의 정적 셸에 포함되거나, 런타임에 서버 메모리의 LRU 저장소에 보관됩니다.

여러 사용자가 결과를 공유하므로, 일반 use cache 안에서는 cookies()나 headers()를 직접 읽지 않고 필요한 값을 바깥에서 읽어 인수로 전달합니다. 공통 상품 정보와 사용자별 조회를 나누면 어떤 결과를 공유하는지 코드에서도 드러납니다. use cache API

Next.js 자료를 읽을 때는 버전과 Cache Components 사용 여부를 함께 확인해야 합니다. 위에서 설명한 Data Cache·Full Route Cache는 기존 모델의 용어이며, Cache Components는 재사용 범위를 명시하는 새 모델입니다.

환경캐시 설정을 읽는 기준
Next.js 14 App Routerfetch 기본 캐시를 설명하는 자료가 많으며 동적 API 사용 등의 예외가 있음
Next.js 15 이후, Cache Components 미사용fetch와 GET Route Handler는 기본적으로 캐시되지 않으며 필요한 곳에서 활성화
Next.js 16, Cache Components 사용use cache, cacheLife, cacheTag로 재사용 범위와 정책을 명시

기본값 변경은 Next.js 15 업그레이드 가이드에 정리되어 있습니다. 캐시되지 않는 fetch도 정적 페이지 생성 중 빌드 시점에 실행될 수 있으므로, 저장 여부와 실행 시점은 나누어 봐야 합니다.

서버가 여러 대라면 메모리 캐시와 공유 캐시를 구분합니다

브라우저
  • HTTP 캐시
  • bfcache
  • Router Cache
  • 쿼리 캐시
  • Cache Storage
  • Web Storage·IndexedDB
CDN
  • 엣지 캐시
Next.js 서버
  • Request Memoization
  • Data Cache
  • Full Route Cache
  • use cache
  • 메모리·Redis
API / DB

Next.js 기능 외에도 서버 프로세스의 Map에 데이터를 저장하거나 Redis 같은 저장소를 사용할 수 있습니다. 프로세스 메모리는 접근이 빠르지만 각 서버가 자기 사본을 가지므로, 서버 A가 값을 갱신해도 서버 B의 사본은 그대로일 수 있습니다. 인스턴스가 종료되면 그 메모리에 있던 값도 사라집니다.

Redis 같은 공유 캐시는 여러 서버가 같은 저장소에 접근하도록 해 조회 결과를 함께 사용하게 합니다. 네트워크 통신과 저장소 운영에 비용이 들지만, 인스턴스마다 데이터를 중복 저장하거나 서로 다르게 갱신하는 문제를 줄일 수 있습니다. 상품 집계처럼 여러 서비스에서 사용하는 값이라면 Next.js 페이지 캐시와 별도로 이런 데이터 캐시를 둘 수 있습니다. Microsoft의 로컬·분산 캐시 설명

Next.js의 일반 use cache도 런타임에는 기본적으로 메모리 핸들러를 사용합니다. 원격 공유 저장소가 필요하면 use cache: remote와 플랫폼의 핸들러 지원을 확인하고, 여러 인스턴스에 무효화 정보가 어떻게 전달되는지도 함께 봐야 합니다. use cache: private는 요청별 정보를 다루는 별도 변형이므로 일반 공유 캐시와 구분해 해당 버전의 제약을 확인합니다. use cache의 런타임 저장, 자체 호스팅 가이드

요청이 어디까지 가는지 연결해 보기

같은 상품을 다시 열어도 브라우저가 화면 전환 결과를 갖고 있는지, CDN에 응답이 남아 있는지, 서버가 데이터를 재사용하는지에 따라 실행되는 작업이 달라집니다. 아래 구조도에서 응답 위치를 바꿔 보면서 어느 경계까지 요청이 전달되는지 비교할 수 있습니다.

어디에서 요청이 끝나는가

응답 위치를 선택해 요청이 넘어가는 경계와 돌아오는 경로를 비교합니다.

요청 경로응답 경로미방문
브라우저 사용자별 로컬 상태
CLIENT
미방문
Router Cache RSC 세그먼트 화면 전환 시 재사용
MISS
HTTP 캐시 HTTP 응답 네트워크 전송 생략
CloudFront 여러 사용자에게 응답 공유
CDN · EDGE
응답 반환
엣지 응답 캐시 HTML · RSC · 정적 자산 원본 서버 요청 생략
Next.js 서버 라우트 응답 생성
APPLICATION
미방문
Full Route Cache HTML + RSC 완성된 렌더 결과 재사용
미방문
데이터 / 함수 캐시 Data Cache 또는 use cache 렌더링 중 조회 결과 재사용
API / DB 현재 데이터 조회
DATA SOURCE
미방문
원본 데이터 상품 · 문서 · 사용자 데이터 캐시 미적중 시 조회
응답 지점CloudFront에서 응답 · Next.js 서버 요청 0회
화살표를 따라 요청이 도달한 계층과 응답이 돌아오는 경로를 비교합니다.

캐시가 여러 곳에 있어도 항상 모든 계층을 확인하는 것은 아닙니다. 브라우저에서 필요한 결과를 재사용하면 CDN에 요청하지 않고, CDN이 응답하면 Next.js의 렌더링과 데이터 조회를 실행하지 않습니다. 서버까지 도착한 요청도 데이터 캐시에 적중하면 DB 조회를 생략할 수 있습니다.

캐시 정책: 누구에게 같은 값을 언제까지 줄 것인가

캐시에 저장한 상품 설명을 재사용하려면 어떤 요청에 언제까지 같은 값을 반환할지 정해야 합니다. 같은 상품이라도 언어와 통화에 따라 응답이 달라질 수 있고, 저장 이후 설명이 수정되면 캐시의 값과 원본이 달라집니다. 캐시 정책은 이 두 문제, 즉 어떤 요청을 같다고 볼지와 언제 다시 확인할지를 다룹니다.

캐시 키로 재사용할 대상을 구분합니다

캐시 키는 저장된 결과를 찾는 식별자입니다. 상품 42의 한국어 설명과 영어 설명이 다르다면 두 요청이 서로 다른 키를 사용해야 합니다. 상품 ID만 키로 삼으면 먼저 저장한 언어의 결과를 다른 언어 요청에도 반환할 수 있습니다.

product:42:ko:KRW
product:42:en:USD

응답에 영향을 주지 않는 값까지 키에 넣으면 같은 내용이 여러 항목으로 나뉘어 저장됩니다. 유입 경로를 기록하는 utm_source가 달라도 상품 내용이 같다면, 이를 모두 구분하는 것은 저장 공간을 늘리고 재사용 기회를 줄입니다. 응답이 달라지는 입력은 빠뜨리지 않되 내용과 무관한 입력은 제외하는 것이 키 설계의 기준입니다. CloudFront 캐시 키

아래 네 요청은 언어와 유입 경로만 다릅니다. 키에 넣을 입력을 바꿔 보면, 언어를 빼면 다른 언어의 응답이 나가고 utm_source를 넣으면 같은 내용을 원본에서 다시 가져오는 것을 확인할 수 있습니다.

캐시 키에 어떤 입력을 넣을까

키에 넣을 입력을 켜고 끄면서 네 요청의 결과를 비교합니다.

키에 포함
  1. 1 /products/42?utm_source=insta ko product:42 원본 조회
  2. 2 /products/42?utm_source=insta en product:42 원본 조회
  3. 3 /products/42?utm_source=kakao ko product:42 원본 조회
  4. 4 /products/42?utm_source=kakao en product:42 원본 조회
1 원본 조회
2 다른 언어로 응답
설명용 모형 · 응답 내용은 언어에 따라서만 달라집니다.

HTTP에서는 Vary로 URL 외에 응답을 구분할 요청 헤더를 알립니다. Vary: Accept-Language가 있다면 언어 헤더에 따라 다른 표현을 제공한다는 뜻입니다. 쿼리 라이브러리의 queryKey, 서버 함수의 인수, CDN의 캐시 키는 서로 다른 설정이지만, 모두 응답을 바꾸는 입력을 일관되게 구분해야 합니다.

저장 가능 여부와 신선도는 별도로 정합니다

캐시에 저장할 수 있다는 것과 확인 없이 다시 사용할 수 있다는 것은 다릅니다. 상품 설명을 저장해 두되 매번 변경 여부를 확인할 수도 있고, 일정 시간 동안은 확인을 생략할 수도 있습니다. HTTP는 Cache-Control 응답 헤더로 이 조건을 전달합니다.

응답 지시어정하는 조건
no-store응답을 저장하지 않도록 지시
private브라우저 같은 개인 캐시에 저장할 수 있지만 공유 캐시에는 저장하지 않도록 지시
public공유 캐시가 응답을 저장할 수 있도록 명시
no-cache저장한 응답을 재사용하기 전에 검증하도록 지시
max-age=N응답을 N초 동안 신선하게 취급
s-maxage=N공유 캐시에서 max-age보다 우선하는 신선도 기간

no-cache를 지정해도 저장된 응답은 남을 수 있습니다. 이름과 달리 저장 금지가 아니라 재사용 전 검증을 요구하므로, 변경 여부를 확인한 뒤 기존 본문을 계속 사용할 수 있습니다. 저장 자체를 허용하지 않으려면 no-store를 사용합니다. MDN Cache-Control

Cache-Control: public, max-age=0, s-maxage=60

이 응답은 브라우저에서는 곧바로 오래된 상태가 되지만, 공유 캐시에서는 60초 동안 신선하게 취급됩니다. 따라서 브라우저가 다시 요청해도 CDN에서 응답할 수 있습니다. 브라우저가 가진 사본의 재사용 기간과 여러 사용자가 공유하는 CDN 사본의 재사용 기간을 따로 정한 것입니다.

캐시의 유효 기간을 흔히 TTL(Time To Live)이라고 부르지만, HTTP의 신선도 만료가 곧 삭제를 뜻하지는 않습니다. 오래된 응답도 변경 여부를 확인하거나 갱신 중 이전 값을 제공하는 데 사용할 수 있습니다. 저장 공간이 부족하면 반대로 신선도 기간이 남은 항목이 먼저 제거되기도 합니다. RFC 9111, CloudFront 만료와 제거

재검증으로 바뀌지 않은 본문의 다운로드를 피합니다

신선도 기간이 끝났다고 상품 설명이 실제로 바뀐 것은 아닙니다. 바뀌지 않은 본문을 매번 전송하는 대신 서버가 부여한 검증자를 보내 변경 여부만 확인할 수 있습니다. ETag는 이를 위해 응답의 특정 표현을 식별하는 값입니다.

처음 받은 상품 응답에 다음 헤더가 있다고 가정해 보겠습니다.

HTTP/1.1 200 OK
Cache-Control: private, max-age=60
ETag: "product-42-v7"

{"id":"42","name":"키보드"}

60초 이후 다시 사용할 때 브라우저는 저장된 ETag를 If-None-Match에 넣어 요청할 수 있습니다. 서버가 같은 표현이라고 판단하면 본문 대신 304 Not Modified로 응답하고, 브라우저는 저장된 본문을 재사용합니다.

GET /api/products/42 HTTP/1.1
If-None-Match: "product-42-v7"
HTTP/1.1 304 Not Modified
Cache-Control: private, max-age=60
ETag: "product-42-v7"

이 과정을 재검증이라고 부릅니다. Last-Modified와 If-Modified-Since를 사용해 수정 시각으로 확인할 수도 있습니다. 재검증은 본문 전송을 줄이지만 요청과 응답의 왕복은 남으므로, 신선한 캐시를 네트워크 없이 재사용한 경우와 비용이 다릅니다. HTTP 캐시 검증

또한 공유 캐시에서 받은 응답은 이미 저장된 시간이 있을 수 있습니다. Age 등의 정보를 반영해 응답의 나이를 계산하므로, CDN에서 브라우저로 도착한 순간부터 max-age 전체가 새로 시작되는 것은 아닙니다.

오래된 값을 먼저 줄지, 새 값을 기다릴지 결정합니다

재검증하는 동안 이전 응답을 보여줄지, 새 응답을 기다리게 할지도 정해야 합니다. 상품 설명은 잠시 이전 값을 보여줘도 괜찮을 수 있지만, 변경된 권한처럼 이전 값을 사용하면 안 되는 데이터도 있습니다. 데이터의 성격에 따라 응답 속도와 최신 상태 확인 중 무엇을 우선할지 달라집니다.

stale-while-revalidate는 신선도 기간이 지난 응답을 일정 시간 더 제공하면서 백그라운드에서 갱신하는 방식입니다. 다음 설정에서는 처음 60초 동안 신선한 응답을 재사용하고, 그 뒤 30초 동안은 이전 응답을 먼저 제공하며 갱신할 수 있습니다. must-revalidate는 오래된 응답을 검증 없이 재사용하지 않도록 요구합니다.

Cache-Control: max-age=60, stale-while-revalidate=30

아래 데모에서 시간을 진행한 뒤 요청해 보면, 60초 이후 이전 응답을 먼저 받는 경우와 재검증을 기다리는 경우를 비교할 수 있습니다.

만료 뒤에 기다릴지, 이전 응답을 받을지

시간을 진행한 뒤 요청하고, 갱신을 완료해 비교합니다.

Cache-Control: max-age=60, stale-while-revalidate=30
캐시 나이0초
저장된 버전v1
원본 조회 횟수0
요청신선한 캐시원본 v2 · 대기

60초 미만에서는 v1을 재사용합니다.

설명용 모형 · 원본 v2, 캐시 v1에서 시작합니다.

HTTP 캐시의 백그라운드 갱신은 이후 요청이 사용할 사본을 바꿉니다. 이미 응답을 받은 화면까지 자동으로 다시 렌더링하지는 않으므로, UI에 새 값을 반영하려면 앱이 재요청하거나 데이터 갱신을 구독해야 합니다. TanStack Query·SWR 같은 라이브러리가 데이터 상태와 화면 갱신을 관리하는 이유도 여기에 있습니다.

장애 중 이전 응답을 제공할 수 있는지도 따로 정할 수 있습니다. stale-if-error는 지원되는 오류 상황에서 오래된 응답을 사용할 기간을 지정하며, immutable은 신선한 동안 내용이 바뀌지 않는 자산의 불필요한 재검증을 줄입니다. 내용 해시가 들어간 파일은 내용이 바뀔 때 URL도 바뀌므로 immutable과 장기 캐시를 적용하기 좋습니다. Cache-Control 지시어

Next.js의 재검증 시간도 자동 실행 주기는 아닙니다

앞에서 상품 함수에 설정한 revalidate: 60도 시간이 지나면 다음 요청에서 갱신할 수 있게 하는 정책입니다. 정확히 60초마다 원본을 읽는 예약 작업이 아니므로, 아무도 요청하지 않는 동안에는 갱신이 시작되지 않을 수 있습니다. 이전 응답을 제공하며 갱신하는 방식과 새 결과를 기다리는 방식을 구분하면 서버 캐시 설정도 읽기 쉬워집니다.

Cache Components의 cacheLife는 이 구분을 세 값으로 표현합니다.

속성앞선 상품 함수의 설정영향을 주는 동작
stale60초클라이언트가 서버 확인 없이 재사용할 수 있는 기간
revalidate60초서버에서 이 시간이 지난 뒤의 요청이 백그라운드 갱신을 유발
expire300초이 기간까지 새 결과를 만들지 못했다면 다음 요청이 새 결과를 기다림

stale은 클라이언트 재사용을, revalidate와 expire는 서버의 갱신과 대기를 제어합니다. 각 값이 제어하는 대상과 동작이 다르므로, 세 값을 하나의 만료 시간으로 해석해서는 안 됩니다. 서버에서 데이터를 갱신해도 클라이언트의 재사용 기간이 남아 있다면 브라우저에는 이전 결과가 보일 수 있습니다. cacheLife API

CloudFront에 정책을 적용할 때 확인할 것

HTTP 응답 헤더에 캐시 정책을 지정했더라도 CDN이 이를 그대로 적용하는지 확인해야 합니다. CloudFront에서는 경로별 Cache Behavior에 오리진과 정책을 연결합니다. 정적 자산과 로그인 사용자 API의 경로를 서로 다른 behavior로 나누면 각각에 맞는 정책을 적용할 수 있습니다.

캐시 키와 원본에 전달하는 정보

CloudFront의 Cache Policy는 TTL과 키에 포함할 쿼리 문자열·헤더·쿠키를 정합니다. Origin Request Policy는 키에는 포함하지 않으면서 오리진에 추가로 전달할 정보를 정합니다. 따라서 오리진이 받는 정보가 모두 캐시 키에 포함되는 것은 아닙니다. CloudFront 정책의 관계

오리진에 currency 쿠키를 전달해 원화와 달러 가격을 구분하지만 캐시 키에는 포함하지 않았다고 가정해 보겠습니다. CDN은 서로 다른 통화의 요청을 같은 항목으로 판단해 먼저 저장한 가격 응답을 재사용할 수 있습니다. 응답에 영향을 주는 값은 키에 포함하거나 그 응답을 공유 캐시에서 제외해야 합니다.

Vary 헤더를 반환하는 것만으로 CDN의 키 설정을 대신할 수도 없습니다. 언어에 따라 결과가 달라지면 CloudFront 정책에서도 그 입력을 구분하는지 확인합니다. 개인화 경로는 쿠키가 있다는 사실에 의존하기보다 공유 캐시 제외 여부를 명시적으로 정하는 편이 분명합니다. CloudFront Cache Policy

원본의 TTL에 하한과 상한을 적용합니다

CloudFront는 원본이 보낸 신선도 정보와 배포 정책을 함께 사용합니다. 원본에 헤더가 없을 때의 기본값뿐 아니라, 너무 짧거나 긴 기간을 제한하는 값도 설정할 수 있습니다.

설정적용 방식
Minimum TTL캐시 유지 기간의 하한
Default TTL원본이 유효한 신선도 정보를 보내지 않을 때 적용
Maximum TTL원본이 더 긴 기간을 지정해도 적용되는 상한

특히 Minimum TTL이 0보다 크면 원본이 no-cache, no-store, private를 보내더라도 그 기간 동안 캐시할 수 있습니다. 개인 응답을 캐시하지 않으려면 응답 헤더와 함께 해당 behavior의 캐시 비활성 정책도 확인해야 합니다. CloudFront TTL 규칙

브라우저에서 강제로 다시 요청하는 동작도 CDN 무효화와는 다릅니다. CloudFront는 viewer 요청의 Cache-Control: no-cache를 오리진 재조회 강제 수단으로 지원하지 않으므로, 브라우저가 재요청해도 CDN의 저장된 응답이 돌아올 수 있습니다. CloudFront 만료 규칙

Next.js의 HTML과 RSC 응답을 구분합니다

App Router는 첫 문서 요청에 필요한 HTML과 클라이언트 탐색에 필요한 RSC 결과를 전달합니다. 같은 페이지 경로라도 요청 종류에 따라 응답 형식이 달라질 수 있으므로, CDN이 이들을 같은 항목으로 저장하면 잘못된 종류의 응답을 반환할 수 있습니다.

Next.js는 _rsc 쿼리와 관련 요청 헤더를 사용해 이러한 응답을 구분합니다. CDN에서 쿼리를 일괄 제거하거나 헤더를 임의로 누락시키지 말고, 설치한 Next.js 버전의 CDN 연동 규칙을 따라야 합니다. 특히 정적 prefetch와 일반 탐색은 구분 방식이 다를 수 있습니다. Next.js CDN 가이드

서버 데이터 캐시와 응답의 공유 가능 여부도 별개입니다. 개인 주문 페이지가 공통 상품 설명을 캐시에서 읽더라도, 완성된 페이지에는 사용자별 정보가 들어갑니다. 이 경우 내부 데이터는 재사용하면서 최종 HTTP 응답은 공유 캐시에서 제외할 수 있습니다.

상품을 수정하면 어떤 캐시를 갱신해야 할까

캐시에 상품 설명 v1이 저장된 뒤 DB를 v2로 수정했다고 가정해 보겠습니다. DB에는 새 값이 있지만 다른 계층의 사본은 자동으로 바뀌지 않습니다. TTL이 끝날 때까지 기다릴 수 없다면 변경 이벤트에 맞춰 기존 값을 더 이상 재사용하지 않도록 무효화해야 합니다.

갱신 방식은 사용자가 언제 새 값을 봐야 하는지에 따라 달라집니다. 수정한 사용자가 저장 직후 새 설명을 확인해야 한다면 새 조회 결과를 기다려야 합니다. 다른 사용자에게는 잠시 이전 설명을 보여줘도 괜찮다면, 기존 응답을 제공하면서 백그라운드에서 갱신할 수 있습니다.

서버 데이터를 갱신하고 다시 읽습니다

Cache Components에서 상품 조회에 cacheTag('product:42')를 붙였다면 수정 후 그 태그를 만료시킬 수 있습니다. 태그는 캐시 키처럼 항목 하나를 식별하는 대신, 함께 갱신할 캐시 항목을 묶습니다. 상품 상세와 관련 목록이 같은 변경에 영향을 받는다면 각 조회에 필요한 태그를 붙여 연결합니다.

수정한 당사자가 저장 직후 새 값을 읽도록 하려면 Server Action에서 updateTag를 사용할 수 있습니다. 아래 코드는 인증과 입력 검증을 거쳐 DB에 변경 내용을 반영한 뒤 태그를 만료시키고 상품 페이지로 이동합니다.

'use server';

import { updateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
import { requireProductEditor } from '@/lib/auth';
import { parseProductDescription } from '@/lib/validation';

export async function editProduct(id: string, formData: FormData) {
  await requireProductEditor(id);
  const description = parseProductDescription(formData.get('description'));
  await db.product.update({ where: { id }, data: { description } });
  updateTag(`product:${id}`);
  redirect(`/products/${encodeURIComponent(id)}`);
}

updateTag는 Server Action에서 사용하며 해당 태그의 캐시를 즉시 만료시켜 다음 읽기가 새 결과를 기다리게 합니다. db, 인증 함수, 검증 함수는 서비스에서 구현한 모듈입니다. updateTag API

CMS에서 콘텐츠를 수정한 뒤 webhook으로 알리는 경우에는 revalidateTag(tag, 'max')를 사용할 수 있습니다. 이는 해당 데이터를 오래된 상태로 표시해 이후 요청에서 이전 값을 제공하며 갱신하는 방식입니다. Route Handler에서도 사용할 수 있고, 즉시 만료가 필요한 외부 이벤트에는 { expire: 0 } 옵션을 검토할 수 있습니다. revalidateTag API

특정 페이지나 레이아웃을 갱신하려면 revalidatePath를 사용합니다. Route Handler에서 호출하면 다음 방문 시 재검증하도록 표시합니다. 같은 데이터를 사용하는 다른 경로까지 갱신해야 한다면 경로 하나를 지정하는 것과 데이터 태그를 만료시키는 것을 구분해야 합니다. revalidatePath API

화면 재요청과 서버 캐시 무효화는 다른 작업입니다

router.refresh()는 현재 라우트의 서버 결과를 다시 요청해 화면에 반영합니다. 서버 데이터를 만료시키지는 않으므로, 서버가 v1을 캐시에서 읽으면 재요청한 화면에도 v1이 표시됩니다. 앞선 코드가 DB 수정 후 updateTag를 호출하는 이유는 다시 읽을 때 사용할 데이터부터 갱신하기 위해서입니다. useRouter API

TanStack Query로 표시한 데이터는 해당 쿼리를 무효화하거나 수정 응답으로 갱신해야 합니다. 서버의 태그 캐시와 클라이언트의 쿼리 캐시는 서로 다른 저장소이므로, 서버에서 데이터를 바꿨다는 사실이 클라이언트 쿼리에 자동으로 전달되는 것은 아닙니다.

변경하려는 대상사용할 수단
Server Action 이후 다음 읽기가 최신 데이터를 기다리게 하기updateTag
태그가 붙은 서버 데이터를 이전 값 제공과 함께 갱신하기revalidateTag(tag, 'max')
특정 페이지·레이아웃의 결과를 재검증하기revalidatePath
현재 라우트의 서버 결과를 다시 받아 화면에 반영하기router.refresh()
클라이언트 쿼리를 오래된 상태로 표시하고 재조회하기invalidateQueries()
외부 CDN에 저장된 응답을 제거하기CDN invalidation·purge

CDN에도 이전 응답이 남아 있을 수 있습니다

서버 데이터를 v2로 바꾸어도 CloudFront에 v1 응답이 남아 있으면 사용자의 요청이 서버에 도달하지 않을 수 있습니다. 반대로 CDN만 먼저 삭제하면 Next.js가 아직 저장하고 있던 v1으로 응답을 만들고 CDN이 그 결과를 다시 보관할 수 있습니다. 따라서 외부 CDN을 운영한다면 DB 반영과 서버 캐시 갱신, CDN 무효화를 하나의 변경 흐름으로 연결해야 합니다.

아래에서 두 순서를 한 단계씩 진행해 보면, CDN을 먼저 비웠을 때 두 작업 사이에 들어온 요청이 서버의 v1으로 CDN을 다시 채우는 과정을 볼 수 있습니다.

무효화 순서에 따라 CDN에 남는 버전

순서를 고른 뒤 다음을 눌러 한 단계씩 진행합니다.

  1. 1 DB 반영
  2. 2 CDN 무효화
  3. 3 사용자 요청
  4. 4 서버 캐시 만료
  5. 5 사용자 요청
사용자 받은 응답 —
CloudFront 엣지 캐시 v1
Next.js 서버 데이터 캐시 v1
DB 원본 v1

모든 계층에 v1

설명용 모형 · 상품 설명 v1이 모든 계층에 저장된 상태에서 시작합니다.

이전 값을 제공하면서 서버 캐시를 갱신하는 방식이라면 첫 요청은 여전히 v1을 받을 수 있습니다. 이때는 CDN에 응답을 다시 저장하기 전에 새 결과가 준비되었는지도 확인해야 합니다. Next.js의 태그·경로 무효화가 외부 CDN을 자동으로 삭제하지는 않습니다. Next.js와 CDN 무효화

CloudFront invalidation도 브라우저에 이미 저장된 응답을 지우지는 않습니다. JavaScript·CSS·이미지처럼 내용이 바뀔 때 URL을 바꿀 수 있는 자산은 버전이나 해시를 붙여 새 파일을 요청하게 만드는 편이 좋습니다. 이전 HTML이 옛 URL을 참조할 수 있으므로 이전 자산은 호환 기간 동안 유지합니다. CloudFront 파일 버전과 invalidation

자원의 성격에 맞춰 보관 방식을 선택합니다

상품 페이지 안에서도 설명, 개인 할인, 재고는 같은 신선도를 요구하지 않습니다. 설명을 잠시 이전 버전으로 보여주는 것은 허용할 수 있어도 결제 금액을 이전 값으로 확정할 수는 없습니다. 페이지 전체에 하나의 캐시 정책을 적용하기보다 데이터별로 누가 조회하고 얼마나 최신이어야 하는지 나눠 보는 편이 좋습니다.

자원정책의 출발점변경을 반영하는 방법
내용 해시가 있는 JS·CSSpublic, max-age=31536000, immutable내용이 바뀌면 새 URL 배포
공개 상품 설명 API브라우저 max-age=0, CDN s-maxage=60상품 태그 갱신과 필요한 CDN 무효화
공개 문서 페이지정적 생성과 재검증, 허용 범위 내 SWR문서와 목록의 관련 캐시 갱신
개인 주문·계정 응답HTTP private, no-store, 공유 캐시 제외계정 전환 시 클라이언트 상태 정리
결제 직전 가격·재고서버에서 최종 조건 확인거래를 확정하는 시점에 검증
오프라인 설명서Service Worker의 응답 재사용 전략저장소 버전 교체와 내용 갱신

공개 API에 적용할 헤더와 Next.js가 HTML·RSC에 설정하는 헤더는 구분합니다. 페이지에는 개인화 결과가 포함될 수 있으므로 내부 조회의 캐시 여부만 보고 응답 전체를 public으로 바꾸지 않습니다. 해시가 있는 Next.js 정적 자산의 장기 캐시는 CDN 가이드에 설명되어 있습니다.

가격과 재고는 TTL을 짧게 줄여도 결제 시점의 정확성을 보장할 수 없습니다. 조회가 끝난 뒤 다른 주문이 들어올 수 있기 때문입니다. 화면에서는 캐시로 탐색 비용을 줄이되, 거래를 확정하는 서버 로직은 현재 조건을 다시 검증해야 합니다.

저장 공간이 부족하면 어떤 항목을 버릴까

캐시의 저장 공간은 제한되어 있으므로 유효 기간이 남은 항목도 제거해야 할 수 있습니다. 이때 무엇을 먼저 버릴지 정하는 것이 퇴출 정책입니다.

LRU는 최근에 사용하지 않은 항목을, LFU는 사용 빈도가 낮은 항목을 우선 제거하는 접근입니다. 인기 상품에 요청이 집중된다면 해당 항목을 남겨 두는 것이 다음 조회를 줄이는 데 유리합니다. Redis에서는 최대 메모리와 퇴출 정책을 설정할 수 있으며, LRU·LFU는 근사 알고리즘을 사용합니다. Redis key eviction

TTL이 값의 유효 기간을 정한다면, 퇴출 정책은 공간이 부족할 때 제거할 항목을 정합니다. 그래서 TTL을 길게 설정했더라도 값이 반드시 그 기간 내내 저장되어 있다고 가정할 수는 없습니다.

직접 만드는 데이터 캐시는 쓰기 경로도 설계합니다

Next.js가 관리하는 캐시 대신 Redis에 상품 집계 결과를 저장한다면, 캐시를 채우고 비우는 코드도 필요합니다. 흔히 사용하는 cache-aside는 앱이 먼저 캐시를 읽고, 없으면 DB를 조회해 그 결과를 저장하는 방식입니다. DB를 수정할 때는 관련 캐시를 삭제하고 다음 읽기가 새 값으로 채우게 할 수 있습니다.

읽기: 캐시 조회 → 적중하면 반환
               → 미적중이면 DB 조회 → 캐시 저장 → 반환

쓰기: DB 반영 → 관련 캐시 삭제

다만 DB 반영과 캐시 삭제는 하나의 원자적 작업으로 실행되지 않습니다. 삭제가 실패하거나 변경 전에 시작한 조회가 늦게 끝나 이전 값을 다시 저장할 수 있으므로, 무효화 재시도나 데이터 버전 비교가 필요할 수 있습니다. Cache-Aside 패턴

쓰기와 함께 캐시도 갱신하는 write-through, 원본 반영을 뒤로 미루는 write-behind, 캐시를 갱신하지 않고 원본에 쓰는 write-around 같은 방식도 있습니다. 특히 write-behind는 빠르게 응답하는 대신 원본 반영 전 장애나 쓰기 순서 문제를 다뤄야 합니다. 이런 방식을 선택할 때는 애플리케이션이 데이터를 읽고 쓰는 순서와 실패 시 복구 방법을 함께 설계해야 합니다.

동시에 만료되거나 실패한 결과도 고려합니다

인기 상품의 캐시가 만료되는 순간 많은 요청이 들어오면, 같은 값을 채우기 위해 DB 조회가 한꺼번에 실행될 수 있습니다. 이런 cache stampede를 줄이려면 진행 중인 조회를 공유하는 single-flight, 항목별 TTL에 작은 차이를 주는 jitter, 만료 전 갱신 등을 사용할 수 있습니다. 캐시가 정상일 때의 적중률뿐 아니라 비어 있거나 장애가 난 상태에서 원본이 감당할 요청량도 확인해야 합니다. AWS 캐시 운영 전략

존재하지 않는 상품을 반복해서 조회해도 원본 서버에 부하가 생깁니다. negative caching은 오류나 데이터가 없다는 결과를 잠시 저장해 이런 재조회를 줄입니다. 다만 상품을 새로 생성하거나 오류를 복구한 뒤에도 이전 실패 응답이 남을 수 있으므로 성공 응답과 다른 기간을 두는 것이 유용합니다.

CloudFront는 일부 오류 응답도 캐시하며, 오류별 Error Caching Minimum TTL 등으로 기간을 관리합니다. 파일을 업로드했는데 여전히 404가 보인다면 성공 응답의 TTL뿐 아니라 이 오류 캐시도 확인해야 합니다. CloudFront 오류 캐시

이미지 변환과 빌드에도 캐시를 사용합니다

캐시는 페이지 데이터와 HTTP 응답뿐 아니라 이미지 변환, 코드 빌드, 서버 주소 조회에도 쓰입니다. 각 작업의 결과를 보관해 같은 변환이나 계산, 조회를 반복하는 비용을 줄입니다.

이미지 최적화 결과를 저장합니다

같은 상품 이미지라도 휴대폰에는 작은 크기, 큰 화면에는 더 큰 크기가 필요합니다. 원본을 매번 다운로드해 크기와 포맷을 변환하면 CPU 작업도 반복됩니다. Next.js의 이미지 최적화 캐시는 이런 변환 결과를 저장해 같은 조건의 요청에 다시 사용합니다. 결과는 서버의 .next/cache/images 디렉터리에 저장되어, 같은 원본·너비·품질·포맷을 요청한 모든 사용자가 공유합니다.

변환 결과를 서버가 캐시해도 브라우저에는 아직 그 이미지가 없을 수 있습니다. 서버에서는 변환 비용을 줄이고, CDN과 브라우저에서는 변환된 파일의 전송을 줄이는 식으로 서로 다른 계층이 같은 이미지에 관여합니다. 원본을 같은 URL에 덮어썼을 때 이전 변환 결과가 보이는 이유도 이 사본들이 각각 남아 있기 때문입니다.

Next.js의 이미지 캐시 기간은 minimumCacheTTL과 원본 응답 정책의 영향을 받습니다. 직접적인 무효화 수단이 제한되므로 이미지가 바뀌면 src를 버전 URL로 바꾸는 방법을 검토합니다. CDN을 앞에 두었다면 포맷 협상에 쓰이는 Accept 전달과 변형 구분도 맞춰야 합니다. Next.js Image API

개발과 배포의 반복 작업을 줄입니다

소스 코드 일부만 수정했는데 매번 전체를 다시 컴파일하면 개발과 배포에 시간이 더 걸립니다. 빌드 캐시는 이전 컴파일·번들링 작업의 결과를 보관해 다시 쓸 수 있는 부분을 재사용합니다. Next.js에서 CI 실행 사이에 .next/cache를 보존하는 것은 이 비용을 줄이기 위한 설정입니다. Next.js CI 빌드 캐시

개발 중에는 HMR 캐시가 서버 데이터 요청 결과를 재사용할 수 있습니다. 코드를 수정하고 화면을 확인할 때마다 같은 데이터를 다시 요청하는 일을 줄이기 위해서입니다. 이 동작은 no-store로 설정한 요청에도 나타날 수 있으므로, 실제 캐시 정책을 검증할 때는 next dev와 프로덕션 빌드를 나눠 확인합니다. Next.js fetch의 개발 모드 동작

서버 주소와 디스크의 데이터도 재사용합니다

브라우저가 서버에 연결하려면 도메인 이름에 대응하는 IP 주소를 알아야 합니다. DNS 캐시는 이름을 주소로 바꾸는 조회 결과를 일정 기간 보관해, 연결할 때마다 같은 이름 해석을 반복하지 않도록 합니다. 도메인이 가리키는 서버를 바꾼 뒤에도 일부 요청이 이전 주소로 간다면 DNS 캐시에 이전 IP 주소가 남아 있는지 확인할 수 있습니다. MDN DNS

DB의 버퍼 풀과 OS 페이지 캐시는 자주 사용하는 디스크 데이터를 메모리에 남겨 I/O를 줄입니다. 이 경우 DB 조회가 실행되어도 모든 데이터를 디스크에서 새로 읽는 것은 아닙니다. 애플리케이션의 JSON 캐시를 지우는 것과 DB·OS의 내부 캐시를 관리하는 것은 다른 작업이며, 웹 응답을 갱신하려고 이 계층까지 일괄 삭제할 필요는 없습니다.

이전 값이 보이면 요청 경로를 확인합니다

상품 설명을 수정한 뒤에도 이전 값이 보인다면, 어느 계층이 그 값을 반환했는지부터 확인해야 합니다. 화면에 데이터 버전이나 수정 시각을 표시하고 요청·응답과 비교하면 추측으로 모든 캐시를 지우는 대신 남아 있는 사본을 찾을 수 있습니다.

확인할 것알 수 있는 것
브라우저 Network 탭의 요청 유무앱이 새 요청을 했는지, 로컬 상태를 재사용했는지
Cache-Control, Age, ETag, Vary응답의 재사용 조건과 검증 정보
CDN의 X-Cache 같은 진단 헤더CDN 계층에서 적중했는지
서버의 렌더링·조회 로그요청이 앱과 원본 데이터 조회까지 도달했는지
각 계층이 반환한 데이터 버전어느 사본에 이전 값이 남았는지

200 응답도 캐시에서 올 수 있고, 304는 본문을 재사용했다는 뜻이지 DB 조회를 증명하지는 않습니다. CDN의 MISS 역시 그 CDN에 대한 결과이므로, 뒤에 있는 Next.js 데이터 캐시나 Redis가 비었다고 판단할 수는 없습니다. 따라서 한 계층의 로그만으로 전체 요청 경로를 판단하기보다, 다음 계층의 로그와 함께 확인해야 합니다.

응답 헤더는 다음처럼 확인할 수 있습니다.

curl -sS -D - -o /dev/null 'https://example.com/products/42'

로그인 쿠키나 RSC 요청 헤더가 없는 요청은 브라우저와 다른 응답을 받을 수 있으므로, 문제를 재현할 때는 URL뿐 아니라 응답을 구분하는 헤더와 쿠키도 원래 요청과 맞춰야 합니다. DevTools에서 브라우저 캐시를 비활성화하는 것 역시 CDN이나 서버 캐시까지 함께 끄는 동작은 아닙니다.

실험할 때는 공개 상품 하나에 버전을 붙여 첫 조회, 반복 조회, TTL 이후 조회, 수정 후 조회를 비교해 볼 수 있습니다. 여기에 화면 전환과 새로고침을 각각 실행하면 브라우저가 보관한 결과와 서버가 재사용한 결과를 구분할 수 있습니다. 이때 각 요청이 어디까지 전달됐고 어떤 버전의 데이터를 받았는지 기록하면, 이전 값을 반환한 캐시와 갱신할 대상을 좁힐 수 있습니다.