Next.js를 사용해 첫 프로젝트를 진행하면서 가장 깊게 고민했던 부분 중 하나가 '어떤 페이지에 어떤 데이터 캐싱 전략을 적용해야 할까?'였습니다. 단순히 빠른 로딩 속도 뿐만 아니라 UX와 서버 리소스에도 직접적으로 연결되는 중요한 문제였기에 Request Memoization, Data Cache, Full Route Cache와 같은 Next.js의 핵심적인 캐싱 방법을 사용하기 위한 나만의 기준을 잡아야겠다는 생각이 들어 찾아보았습니다.
이 글에서 주요 캐싱 전략의 개념, 사용 방법과 실제 프로젝트에서 적용한 예시를 함께 담아보았습니다. 공부한 내용을 기반으로 앞으로는 각 페이지의 특성과 데이터의 변화 주기에 맞춰 캐싱 전략을 설정하고, 앞으로도 사용자에게 더 빠르고 부드러운 UX를 제공하면서 서버 부하를 줄이는 효율적인 개발을 실현하려고 합니다.
웹 서버의 성능은 왜 중요할까요?
웹 서버의 성능은 곧 사용자 경험(UX)에 직결되어있고 서버 리소스에도 영향을 주기 때문인데요, 페이지 로딩이 1초 느려질 때마다 사용자 이탈률이 크게 증가하거나, 아마존 등 대형 기업에서도 100ms 지연이 매출에 손해를 준다고 발표했습니다.
이렇게 웹 서버의 성능은 웹페이지의 로딩 속도를 좌우하는 가장 핵심적인 요소 중 하나입니다. 사용자가 웹사이트에 접속하면, 브라우저는 서버에 요청을 보내고 응답을 기다리는 과정을 거치게 되는데요. 이 과정에서 특히 중요한 지표가 바로 TTFB입니다.
TTFB (Time to first byte)
: 리소스 요청과 응답의 첫 번째 바이트가 도착하기 시작하는 시점 사이의 시간을 측정하는 항목

다이어그램에서 노란색으로 표시된 영역이 TTFB에 해당하는 부분인데요, 이 구간은 서버가 얼마나 빠르게 요청을 처리하고 응답을 보내는지에 따라 결정되며 웹 서버의 성능이 직접적으로 영향을 미치는 부분입니다.
따라서 이 TTFB 구간을 줄이는 것, 즉 서버의 응답 속도를 개선하는 것이 전체 페이지 로딩 속도를 높이는 데 매우 중요합니다.
그리고 이때 가장 효과적으로 활용할 수 있는 방법 중 하나가 바로 Next.js에서 제공하는 다양한 캐싱 전략입니다. 캐싱을 잘 설계하면, 매번 서버가 새로 데이터를 생성하거나 요청을 처리할 필요 없이 이미 생성해둔 응답을 빠르게 반환할 수 있기 때문에 TTFB를 획기적으로 줄일 수 있고, 동시에 서버 부하를 줄여서 비용까지 절감할 수 있게 됩니다.
방법 1) Request Memoization
React Server Component 환경에서 fetch() 함수 호출을 최적화하기 위한 렌더링 단위 캐싱
먼저 첫번째로 설명드릴 캐싱 전략은 Request Memoization인데요, 동일한 fetch 요청이 한 번의 서버 컴포넌트 렌더링 중 여러 번 호출될 경우 첫 번째 요청의 결과(Promise)를 재사용하는 최적화 방법입니다. 즉, 같은 입력값으로 여러 번 fetch()를 호출하더라도 실제 네트워크 요청은 한 번만 일어나고, 이후 호출은 결과를 메모리에서 재사용하게 됩니다.

작동 방식
// 동일한 fetch 요청이 여러 컴포넌트에서 발생
const api1 = fetch('/api/user');
const api2 = fetch('/api/user'); // 실제 요청은 한 번만 발생
위 코드에서 api1과 api2는은 동일한 fetch 요청이므로, Next.js는 Request Memoization을 통해 두 번째 호출은 처음 호출의 결과를 재사용합니다. 이는 같은 URL, 같은 옵션(headers, method), 같은 fetch context(cache, next.revalidate, credentials)등의 조건을 만족할 때만 동작합니다.
특징
1. prop drilling 없이 각 컴포넌트에서 fetch하도록 구현해도 최초 1회만 요청
어떤 페이지에서 사용자 정보를 여러 컴포넌트가 필요로 할 때 기존에는 상위 컴포넌트에서 데이터를 받아서 하위로 전달하는 prop drilling 방식이 일반적인 방법입니다. 하지만 Request Memoization을 활용하면, 각 컴포넌트가 직접 fetch를 수행하더라도
서버에서는 최초 1회만 요청하고 이후에는 동일한 응답을 공유합니다.
2. GET 요청만 대상이며, POST/DELETE에는 적용 X
Request Memoization은 서버에서 순수하게 결과만 가져오는 요청인 GET 메서드에만 적용되므로 POST나 DELETE 같은 데이터를 변경하는 요청에는 적용되지 않으며, 클라이언트 사이드에서 발생하는 fetch에도 해당되지 않습니다.
3. 한 번의 서버 렌더링 동안만 유효하고, revalidate는 의미 X
또한 이 기능은 캐시(Cache)와는 또 다른데, Request Memoization은 하나의 서버 렌더링 사이클 동안만 유효한 일시적 최적화이며,
revalidate나 ISR 같은 장기적인 캐시 전략과는 별개로 작동합니다. 정적 캐시가 아니라 서버 컴포넌트 렌더링 중 중복 호출을 방지하기 위한 최적화 기법입니다.
방법 2) Data Cache
fetch()를 통해 가져온 데이터를 cache 해두고, 이후 동일한 요청에 대해 네트워크 없이 데이터를 재사용할 수 있도록 해주는 기능
두 번째로 소개드릴 전략은 Data Cache입니다. 이 전략은 fetch()를 통해 가져온 데이터를 서버 측에서 일정 시간 동안 캐싱해두고, 동일한 요청이 들어왔을 때 네트워크 없이 응답값을 재사용할 수 있도록 하는 기능입니다.

작동 방식은 비교적 단순한데요, Next.js에서 fetch() 호출 시 revalidate 옵션을 설정해주기만 하면 됩니다.
const res = await fetch('/api/report', { next: { revalidate: 60 } });
위와 같이 revalidate: 60을 설정하면 이 요청은 60초 동안 캐싱되고 그 시간 내에는 같은 경로로 fetch()를 호출해도 실제 API 요청 없이 캐시된 응답이 그대로 반환됩니다.
여기서 중요한 점은 앞서 설명한 Request Memoization과는 다르게, 서버 렌더링 1회에 국한되지 않고 지속적으로 들어오는 모든 요청에 대해 동작한다는 점입니다. 예를 들어, revalidate를 1초로 설정하면 그 1초 동안 1,000명의 사용자가 같은 페이지에 접근하더라도 서버는 실제 API 요청을 단 한 번만 전송하고, 모든 사용자에게 캐시된 동일한 응답을 전달하게 됩니다. 이처럼 Data Cache는 자주 바뀌지 않는 데이터를 반복적으로 제공해야 하는 페이지에서 매우 유용하고, 서버 부하를 빠르게 줄이면서도 빠른 응답 속도를 보장할 수 있습니다.
여기서 한 가지 더 중요한 동작 방식을 말씀드리면, revalidate 시간이 지나더라도 Next.js는 첫 번째 요청에 대해 여전히 기존에 캐싱된 응답을 반환합니다. 이 만료된 상태를 STALE 상태라고 하는데요, 만료되었더라도 일단 기존 데이터를 사용자에게 보내고 그 뒤에서 백그라운드에서 API를 다시 호출해 최신 데이터를 갱신하게 됩니다.
이러한 방식은 사용자 입장에서는 빠른 응답을 받을 수 있다는 장점이 있지만, 데이터가 바로 최신화되지 않는다는 점에서 개발자의 의도와 다르게 동작할 수 있습니다. 그래서 민감한 데이터나 실시간성이 중요한 페이지에서는 Data Cache 적용 여부를 신중히 판단할 필요가 있습니다.
그리고 한 가지 실무에서 자주 혼동되는 부분도 짚고 넘어가야 하는데요, router.refresh()를 사용해도 Data Cache는 강제로 revalidate되지 않습니다. 이 경우에는 반드시 revalidatePath()나 revalidateTag() 같은 Next.js의 전용 API를 통해 강제로 재검증(revalidate) 해야 합니다. 이 API를 사용하면 캐시가 즉시 갱신되기 때문에, 그 다음 요청부터는 갱신된 최신 데이터가 바로 사용자에게 전달됩니다.
방법 3) Full Route Cache
특정 페이지의 HTML, RSC Payload 전체를 캐싱하여 서버 렌더링 비용을 줄이고 빠르게 응답할 수 있도록 하는 Next.js의 캐싱 전략
마지막으로 소개드릴 전략은 Full Route Cache입니다. 말 그대로, 특정 페이지 전체를 통째로 캐싱해서 서버 렌더링 비용을 줄이고, 정적 페이지처럼 빠르게 응답할 수 있도록 하는 방식입니다.

다른 캐싱 방식들이 데이터 단위, 요청 단위로 작동하는 반면 Full Route Cache는 페이지 단위로 작동합니다. 즉, 사용자가 해당 페이지에 접근했을 때 서버가 다시 렌더링하는 것이 아니라 미리 생성해둔 HTML과 RSC Payload를 그대로 즉시 반환합니다. 페이지 단위로 캐시되기 때문에, 사용자 요청이 들어와도 서버 렌더링 없이 정적 파일처럼 즉시 응답이 가능하다는 장점이 있습니다.
그럼 이 전략을 왜 사용할까요?
자주 접근되는 페이지를 미리 캐싱해두면 SSR 비용을 줄일 수 있고, HTML과 데이터가 모두 준비된 상태로 반환되기 때문에 성능이 비약적으로 향상되며, 정적 파일처럼 즉시 응답하기 때문에 사용자는 로딩 지연 없이 빠른 화면을 볼 수 있기 때문입니다.
작동 방식
먼저 사용자가 특정 페이지에 접근하면 Next.js는 해당 페이지가 generateStaticParams, revalidate, fetch(..., { cache: 'force-cache' }) 같은 설정이 되어 있는지를 확인합니다. 이 조건을 만족하면 해당 페이지는 Full Route Cache 대상이 되고, HTML과 RSC Payload가 서버 파일 시스템 또는 Edge 환경에 저장됩니다.
그 이후 동일한 페이지 요청이 들어오면, Next.js는 서버가 아닌 캐시된 정적 결과를 바로 반환하게 됩니다. 이로 인해 서버 부하가 크게 줄어들고, 사용자는 정적 페이지처럼 매우 빠른 로딩 속도를 경험할 수 있습니다. 따라서 Full Route Cache는 모든 사용자가 동일한 내용을 보게 되는 페이지에서 가장 효과적이며, 리포트 페이지나 메인 콘텐츠처럼 자주 바뀌지 않는 공용 페이지에 적합한 전략입니다.
캐싱 제어 방법
export const revalidate = 60; // ISR 주기 설정 - 60초마다 Full Route Cache 재생성
예를 들어, export const revalidate = 60처럼 설정하면 이 페이지는 60초마다 Full Route Cache가 재생성되며, 그 외 요청에 대해서는 캐시된 정적 결과가 그대로 반환됩니다.
Request Memoization / Data Cache와의 차이
다른 캐싱 전략들과의 차이점도 정리해보겠습니다.
| 항목 | Full Route Cache | Data Cache | Request Memoization |
| 캐싱 단위 | 페이지 전체 (HTML + RSC) | fetch 결과 (JSON 등) | fetch 결과 (렌더링 중 메모이제이션) |
| 유효 범위 | 전체 라우트 | fetch 단위 | 렌더링 동안만 |
| 수명 제어 | revalidate, dynamic 등 | next.revalidate | 없음 (자동) |
| 서빙 위치 | 서버/Edge (정적 파일처럼) | 서버 (메모리/디스크) | 없음 |
- Full Route Cache는 HTML과 RSC를 포함한 전체 페이지 단위로 캐시되고, 캐시 제어는 revalidate, dynamic과 같은 설정을 통해 이뤄집니다. 서버나 Edge에 저장되어 정적 파일처럼 동작합니다.
- Data Cache는 fetch()의 응답 데이터만 캐시하고, 캐싱 범위도 API fetch 단위에 국한됩니다. 서버 디스크나 메모리 캐시를 사용하며 next.revalidate로 제어됩니다.
- Request Memoization은 렌더링 중 중복 fetch를 방지하기 위한 메커니즘으로, 실제 네트워크 요청은 단 한 번만 발생하고 나머지는 동일한 Promise를 재사용합니다. 별도의 설정 없이 자동으로 작동하지만 렌더링 동안에만 유효합니다.
튜닝 프로젝트 캐싱 전략
이제 앞서 말씀드린 캐싱 전략을 튜닝 서비스에 어떻게 적용했는지 설명드리겠습니다.
1) 리포트 페이지 캐싱 전략 (Full Route Cache + ISR (Incremental Static Regeneration))

튜닝 리포트는 사용자 모두에게 동일하게 보여지는 공통적인 콘텐츠이기 때문에 이 페이지에 Full Route Cache + ISR 전략을 함께 적용했습니다. 먼저 캐싱 대상은 페이지 전체, 즉 HTML과 RSC Payload 전체입니다. 목표는 정적 생성을 통해 빠른 로딩 속도를 확보하면서도, 콘텐츠가 바뀌었을 때 자동으로 갱신되도록 하는 것입니다.
왜 이 전략이 적합했을까요?
- 리포트는 공통 콘텐츠이기 때문에 SSR보다는 SSG 방식이 더 효율적입니다.
- 콘텐츠는 일주일에 두 번만 갱신되기 때문에 revalidate 값을 짧게 설정할 필요가 없고, 대신 ISR을 활용하면 콘텐츠가 실제로 수정되었을 때만 새로운 버전이 생성돼서 성능과 실시간성을 둘 다 잡을 수 있었습니다.
→ 따라서 변경이 적고, 모두에게 동일한 콘텐츠를 제공하는 페이지 특성상 Full Route Cache + ISR 전략이 가장 적절하다고 판단했습니다!
2) 프로필 상세페이지 캐싱 전략

튜닝 서비스의 프로필 상세페이지에는 Request Memoization과 Data Cache를 함께 활용했습니다.
먼저, 해당 페이지는 여러 컴포넌트가 동일한 userId를 기준으로 동일한 사용자 정보를 각각 fetch하는 구조를 가지고 있습니다. 이때 fetch()가 중복으로 발생하지 않도록 Request Memoization을 적용하여, React 트리 렌더링 동안 동일 요청은 한 번만 처리되도록 구성했습니다.
또한 사용자 정보는 자주 변경되지 않기 때문에, Data Cache를 통해 일정 시간 동안 서버 응답을 재사용하도록 설정했습니다. 이를 통해 렌더링 성능을 향상시키는 동시에, 서버 부하도 줄이는 효과를 얻을 수 있었습니다.
이 전략이 적절하다고 판단한 이유는 다음과 같습니다.
- 여러 컴포넌트에서 동일한 userId 데이터를 참조하기 때문에 Request Memoization을 통해 불필요한 중복 fetch를 방지할 필요가 있었습니다.
- 프로필 상세페이지는 매칭 여부에 따라 콘텐츠가 다르게 보여지는 개인화된 뷰이기 때문에, 모든 사용자에게 동일한 콘텐츠를 제공하는 Full Route Cache(SSG) 방식은 적합하지 않았습니다.
- Data Cache를 활용하면 일정 시간 동안 동일한 응답을 재사용할 수 있어 성능 최적화와 서버 비용 절감 측면 모두에서 효과적인 선택이었습니다.
→ 따라서 프로필 상세페이지의 특성과 요구사항을 종합적으로 고려했을 때, Request Memoization과 Data Cache의 조합이 가장 적절한 캐싱 전략이라고 판단했습니다.
요약
| 메커니즘 | 대상 | 장소 | 목적 | 기간 |
| Request Memoization | fetch()의 결과 (Promise) | 서버 | 동일 요청에 대해 중복 fetch 방지 | React 트리 렌더링 동안 (same request lifecycle) |
| Data Cache | fetch된 Data 자체 | 서버 | 자주 사용하는 데이터를 재사용하여 서버 부하 감소 | 지속적 (ISR, revalidate, no-store 등으로 제어 가능) |
| Full Route Cache | HTML, RSC Payload | 서버 | 전체 페이지를 캐싱하여 성능 극대화 | 지속적 (revalidate 옵션에 따라 갱신) |
| Router Cache | RSC Payload | 클라이언트 | 클라이언트 라우팅 간 동일 데이터 재사용으로 요청 최소화 |
- Request Memoization은 fetch() 호출이 React Server Component에서 중복되었을 때 동일한 요청은 한 번만 처리되도록 하는 메커니즘입니다. 기간은 '렌더링 사이클 내'에서만 유지됩니다. (렌더링이 끝나면 사라짐)
- Data Cache는 fetch()에 cache: 'force-cache' | 'no-store' | 'force-no-store'와 같은 옵션으로 제어 가능하며, revalidate 설정에 따라 ISR처럼 동작할 수 있습니다.
- Full Route Cache는 build 시점 또는 ISR을 통해 생성된 전체 페이지(HTML + RSC payload)를 캐싱해 놓고, 정적 자산처럼 서빙하는 전략입니다.
- Router Cache는 클라이언트 측에서 브라우저 히스토리 및 React 상태를 유지하며 RSC 결과를 메모리에 저장해, 백엔드 재요청 없이 빠른 전환이 가능하도록 해주는 방법입니다.
카카오테크 부트캠프 프론트엔드 동아리 NEXT LEVEL에서 자발적으로 발표를 진행했었던 주제라서 영상 첨부해두겠습니다! 긴 글 읽어주셔서 감사합니다 :)
https://www.youtube.com/watch?v=dLj9OlgNYy8
'👩🏻💻 Develop > Performance Optimization' 카테고리의 다른 글
| [Next.js] SSR 성능 최적화: Core Web Vitals 지표 개선하기 (1) | 2025.09.20 |
|---|---|
| [Next.js] Next.js 이미지 최적화로 성능 60% 개선하기 (0) | 2025.09.19 |
| [Next.js] Dynamic Import로 번들 사이즈 25% 최적화하기 (0) | 2025.09.18 |
| [Next.js] Zustand를 활용한 성능 개선하기 (1) | 2025.09.13 |
| [Next.js] 메모이제이션 및 리렌더링을 활용한 TBT 43% 개선기 (3) | 2025.09.01 |