본문 바로가기
개발 공부/Next.js

Next.js의 fetch API로 캐싱된 데이터는 어디에 저장될까?

by 진!!!!! 2025. 4. 29.

고민의 시작

Next.js로 프로젝트를 하던 중, fetch를 사용할 것인지 axios를 사용할 것인지에 대한 고민이 있었다.

fetch 함수를 사용하면 Next.js의 캐싱 기능을 활용할 수 있지만, axios의 interceptor 등 인증을 위한 편리한 기능을 사용할 수 없었다.

 

어차피 캐싱을 하려면 React Query나 SWR을 사용하면 되지 않나? 라는 생각을 했는데, React Query의 캐시는 클라이언트의 인메모리에 저장되기 때문에, SSR에 적합하지 않을 수 있다는 사실을 알게 됐다. (Next.js의 SSR을 활용하려면 서버에서 렌더링해야하는데, React Query는 데이터를 클라이언트에 저장하므로 SSR을 하기에 제약이 생길 것 같았다.)

 

그러던 중, fetch로 캐싱된 데이터는 정확히 어디에, 어떤 원리로 저장되는거지? 의문이 생겼다.

 

결론만 말하자면 DB 혹은 API에서 불러온 데이터는 Data Cache를 통해 서버의 .next/cache/fetch-cache/ 폴더 내에 json 형태로 저장되고, fetch할 때 마다 Request Memoization을 통해 메모리에 임시 저장(캐싱) 해 중복 호출을 막는다.

자세히 알아보자.

 

공식 문서의 Caching APIs 부분의 표를 들고와보았다.

Deep Dive: Caching | Next.js

API Router Cache Full Route Cache Data Cache React Cache
(Request Memozation)
fetch     Cache Cache
fetch options.cache     Cache or Opt out  
fetch options.next.revalidate   Revalidate Revalidate  
fetch options.next.tags   Cache Cache  

 

아래는 fetch 옵션이다. (Next 15의 디폴트는 no-store이다)

  • fetch는 기본적으로 auto no cache 속성이다. 개발 시에는 매번 새로 데이터를 fetch하고, 빌드 시에는 한번만 fetch 한다. 다만 Dynamic API(쿠키, 헤더 등)와 같이 사전 렌더링 시에 알 수 없는 정보는 새로 fetch한다.
  • no-store: 매번 데이터를 새롭게 불러옴
  • force-cache: 캐시하고 캐시된 것에서 데이터 불러옴

같은 fetch 하나에도 캐싱이 3가지나 나누어진다. Full Route Cache, Data Cache, React Cache... 무슨 차이가 있는걸까?

Deep Dive: Caching | Next.js

Next.js의 캐싱 구조 이해하기

fetch가 어떻게 캐싱되는지 알려면 먼저 Next.js의 캐싱 구조에 대해 알아야할 것 같다.

Next.js의 캐싱은 4가지 종류가 있다.

  1. 클라이언트의 인메모리에 저장되는 Router Cache
  2. 서버에서 컴포넌트가 렌더링될 때 인메모리에 저장되는 Request Memozation
  3. 서버에서 영속적으로 저장되는 Full Route Cache
  4. 서버에서 영속적으로 저장되는 Data Cache.

이 글에서는 fetch의 캐싱 위치에 대해 이야기하고 있으므로, Request Memoization과 Data cache에 대해서만 알아볼 것이다.

아래는 공식 문서에 있는 표를 들고와 한국어로 이해하기 쉽게 조금 수정해보았다.

Mechanism 저장하는 것 위치 목적 기간
Request Memoization 함수의 실행 결과 (fetch 결과 포함) 서버 메모리 렌더링 중 중복 요청 방지(React Component tree에서 데이터 재사용) 렌더링 한 사이클 동안만
Data Cache 데이터(fetch 결과) 서버 디스크 사용자 요청 결과 저장 영속적 (재검증 가능)
Full Route Cache HTML, RSC payload 서버 디스크 전체 페이지 렌더링 결과 캐싱 영속적 (재검증 가능)
Router Cache RSC Payload 클라이언트 페이지 간 이동 최적화
(이동 시 서버 request 최소화)
세션 중, 또는 시간 기반

Full Route Cache와 Router Cache는 RSC Payload를 캐싱한다.

 

쉽게 말하자면,

  • Request Memoization은 request의 return 값을 캐싱한다. (request란 fetch API, Layout, Page, Component 등을 포함한다. 주의사항: 하나의 렌더링 사이클 내에서만 유효함)
  • Data Cache는 유저가 요청한 값, 혹은 deployments를 캐싱한다.
  • Full Route Cache는 static(정적) 페이지와 같은 페이지의 렌더링 결과 (HTML, RSC payload)를 캐싱한다.
  • Router Cache는 브라우저에서 앞으로 가기/뒤로가기를 하는 경우 등에서 페이지를 미리 렌더링해서 캐싱한다.

Next.js에서는 퍼포먼스를 위해 최대한 많은 데이터를 캐싱하려고 한다고 한다. 즉, opt out(제외)하지 않으면 거의 캐싱해놓는다.

시나리오로 이해하기

아래 다이어그램을 보면 이해가 쉽다.

  1. 특정 페이지에 접속한다.
    1. 그 페이지가 Router Cache에 캐싱되어 있는 경우 해당 페이지를 보여준다. (SET)
    2. Router Cache에 캐싱되어있지 않는 경우, Full Route Cache에 해당 페이지가 캐싱되어있는지 확인한다.
      1. 캐싱되어 있을 경우, 해당 페이지를 보여준다.
      2. 캐싱되어있지 않는 경우, 렌더링을 진행한다. 렌더링 중, Request Memoization에 렌더링을 위한 함수가 캐싱되어 있는지 확인한다.
        1. 캐싱되어 있는 경우, Payload 혹은 HTML로 렌더링하여 캐싱한다.
        2. 되어있지 않는 경우, 함수를 호출해야한다. Data Cache에 함수 호출을 위한 데이터가 캐싱되어있는지 확인한다.
          1. 캐싱이 되어있으면 데이터 반환
          2. 캐싱이 되어있지 않으면 데이터 소스에서 가져옴

실제로는 중간 과정을 생략하고 바로 Data Source에 접근하는 등 조금 다를 수 있겠지만, 간단하게 4단계의 캐싱 레이어가 있다고 생각하면 될 것 같다.

Request Memoization

Next.js는 fetch API를 확장하여 같은 URL과 option의 요청을 기억한다. 여러 컴포넌트에서 같은 데이터를 호출해도, 한번만 실행한 후 그 결과를 재사용할 수 있다.

 

트리 최상위에서 한번 호출 한 후 props로 넘겨주지 않아도 되고, 여러번 요청해도 성능 저하를 걱정할 필요가 없다고 한다.

그림을 보면 fetch 요청이 서버의 In-memory에 저장됨을 확인할 수 있다. 그런데 여기서 조금 주의해야할 것이, 이게 지속적인 데이터는 아니다. Rendering 중에서만 Memozation이 일어난다.

 

 

Rendering 중에서만 Memozation이 일어난다는 뜻은, 한 번의 렌더링 요청(렌더링 사이클) 내에서만 유효하다는 뜻이다. 우리가 생각하는 영속적인 캐시와는 다르다...

 

어차피 한 번의 렌더링에서만 유효한거면 캐싱을 하는 의미가 있을까? 의문이 생길 수 있다.

 

React의 메모이제이션(예: fetch()cache()로 래핑된 함수)은 서버 컴포넌트 트리 하나가 렌더링되는 동안, 같은 요청이 여러 번 발생하더라도 한 번만 실행되게 해주는 최적화다. (Data를 영속적으로 캐싱하는 것은 이후의 Data Cache 파트에서 더 알아보자)

  • Request Memoization은 Next.js의 기능이 아닌, React의 기능이다.
  • 메모제이션은 fetch의 GET에서만 가능하다.
  • 메모이제이션은 React 컴포넌트 트리에서만 적용된다.
    • generateMetadata, Layout, Page 등에서는 가능하지만, Route Handlers(app/api/* 와 같은 API 핸들러)에서는 사용이 불가능하다. 그럴 때는 React의 cache() 함수를 사용할 수 있다.
  • React 구성 요소 트리가 렌더링을 완료할 때까지, 서버 요청이 살아있는 동안 지속된다.
  • 메모이제이션은 서버 요청 간에 공유되지 않고, 렌더링 중에만 적용되기 때문에, 이를 revalidate(재검증) 할 필요가 없다.

Data Cache

Next.js는 server request와 deployment에서 이루어지는 data fetch의 결과를 영속적으로 저장하는 Data Cache가 있다.

내가 궁금했던.. fetch로 캐싱한 데이터는 어디에 저장되는가? 의 답이 여기에 있었다.

next start로 빌드하면 .next 폴더가 생기는데, .next/cache/fetch-cache/ 폴더에 캐싱된 데이터가 저장된다. 그 폴더에서 JSON 형태로 저장된 데이터를 찾는다.

 

물론 Data cache에서 찾기 전에 Request Memoization에서 먼저 캐싱된 데이터를 찾고, 없을 경우 Data Cache에서 찾는다. cache 옵션이 no-store이거나 데이터가 없는 경우 Data Source에서 데이터를 찾아온다.

  • 데이터 캐싱 옵션과 관계 없이, Request Memoization은 모두 일어난다. 왜냐햐면 렌더링 시 중복 요청이 일어나지 않게 하기 위해서..!

데이터 캐시와 요청 메모이제이션의 차이점

두 캐싱 메커니즘 모두 캐시된 데이터를 재사용하여 성능을 개선하는 데 도움이 되지만, 데이터 캐시는 들어오는 요청과 배포에 걸쳐 지속되는 반면, 메모이제이션은 요청의 수명 동안만 지속된다.

 

Data Cache 직접 확인해보기

그렇다면 캐싱된 데이터는 어디에 어떻게 저장될까?

 

데이터는 서버 디스크 (.next/cache/fetch-cache/)에 영속적으로 저장되며, 빌드 시 혹은 런타임에 캐싱된다. JSON 형태로 저장되며, Revalidate 주기를 설정해, 데이터 만료 후 자동 새로고침이 가능하다.

 

직접 test page를 만든 후 Hello World를 반환하는 api를 연결해봤다. 실행해보자.

import React from "react";

const page = async () => {
  const testData = await fetch("http://localhost:8090/", {
    next: { revalidate: 10 },
  });
  console.log("testData", testData);
  return <div></div>;

};

export default page;

pnpm run dev로 실행했다.

.next/cache/fetch-cache

폴더를 확인해보니 이런 파일이 생성되어 있었다.

캐시 파일 내용 확인해보기

{
  "kind": "FETCH",
  "data": {
    "headers": {
      "access-control-allow-credentials": "true",
      "connection": "keep-alive",
      "content-length": "12",
      "content-type": "text/html; charset=utf-8",
      "date": "Mon, 28 Apr 2025 15:42:17 GMT",
      "etag": "W/\"c-Lve95gjOVATpfV8EL5X4nxwjKHE\"",
      "keep-alive": "timeout=5",
      "vary": "Origin",
      "x-powered-by": "Express"
    },
    "body": "SGVsbG8gV29ybGQh",
    "status": 200,
    "url": "http://localhost:8090/"
  },
  "revalidate": 10,
  "tags": []
}

 

분명 Hello World! 를 반환하는 api를 연결했는데, body값에 SGVsbG8gV29ybGQh가 저장되어있다.


뭘까?

 

왜냐햐면 데이터를 저장할 때 Base64로 인코딩하여 저장하기 때문이다. 디코딩해보면 제대로 뜨는걸 확인할 수 있다.

 

서버리스 환경이라면?

Vercel 같은 서버리스에서는 .next 디렉토리에 직접 파일을 쌓을 수 없다.
대신, 플랫폼 레벨의 캐시 인프라 (ex: Edge Network, CDN 등)에 데이터가 저장된다고 한다.

느낀점

Next.js의 캐싱 방식과 fetch로 캐싱한 데이터는 어디에 저장되는지에 대해 알아보았다. Router Cache와 Full Router Cache에 대해서는 다음 시간에 알아보도록 하자...

 

서버에서 캐싱하는 fetch와 클라이언트에서 캐싱하는 React Query

이전에 Next.js로 프로젝트를 할 때는 fetch로 캐싱을 했다. 그리고 Next.js는 fetch로 캐싱이 되니까 React Query와 같이 서버 상태 관리를 위한 라이브러리를 사용할 필요가 없다고 생각했는데, 둘은 완전히 접근 방식이 달랐다. fetch는 서버에 캐싱이 되고 React Query는 클라이언트에서 캐싱이 된다. 이는 큰 차이점이다.. ㅎ

 

전체 게시글이나 검색 결과와 같은 데이터는 Nextjs의 캐싱기능을 활용하면 효과적으로 최적화할 수 있을 것 같다.

캐시나 헤더에 사용자 정보가 들어가는, 개인화된 데이터를 불러올 때는 fetch로 캐싱 시 private하게 관리 할 수 없으므로 react query와 같은 클라이언트 캐싱 전략을 사용하는게 좋을 것 같다.

 

트위터같은 대형 sns에서 DB조회를 줄이려면 서버에서 redis같은 db를 활용해 사용자 별로 데이터를 따로 캐싱하고, 클라이언트에서도 캐싱을 따로 하지 않을까 싶다.

 

Nextjs도 결국은 서버였음을...

생각해보니까 결국 서버에서 캐싱하는걸 Next.js에서도 똑같이 하는 것이었다... 그렇다... Next.js도 서버가 있으니까... 원리는 같다.
프론트엔드 개발만 해본 상태에서 Next.js로 개발을 할 때는 자꾸 헷갈리고 어딘가 이상했는데 백엔드 개발을 한번 해보고 Next.js를 다시 해보니 이제 좀 당연하고 이상해지지 않는 부분이 있다. 서버 개발을 해보길 잘 한 것 같다.
스마일게이트 데브 캠프를 할 때 캠프장님께서 프론트엔드도 꼭 백엔드를 해보는 걸 추천한다고 여러번 강조하셨었는데, 이렇게 다가오니까 신기하다.

인스턴스가 여러개일 때 생길 수 있는 문제

Next.js의 캐싱 데이터가 .next 폴더에 저장된다는 것을 알고, 인스턴스를 여러개 활용할 경우 캐싱된 데이터가 공유되지 않는 문제가 있을 수 있다는 것을 알게 되었다.

 

서버 인스턴스는 여러개 둘 수 있다는 생각을 해봤는데 Nextjs 프로젝트는 인스턴스를 여러개 둘 수 있다는 생각을 안해봤다. Next.js는 서버도 가능하기에 당연히 인스턴스를 여러개 두는 상황이 생길 수 있는 건데...
찾으면 찾을 수록 생각지 못한게 많이 보이는 것 같다.

앞으로 더 알아보고 싶은 점

캐싱 데이터는 서버의 .next 폴더에 생성되는데, 서버리스 환경에서 배포하면 플랫폼 레벨의 캐시 인프라 (ex: Edge Network, CDN 등)에 데이터가 저장된다고 한다. 그런데 이게 어떤 원리로 이루어지는지는 아직 이해가 안되어서.. ㅎ 좀 더 찾아봐야겠다.