본문 바로가기
카테고리 없음

Caching in Next.js (App Router)

by logsome 2025. 3. 10.

들어가며

Next.js의 App Router는 다양한 캐싱 전략을 제공하여 성능을 최적화합니다. 이 글에서는  Nextjs 공식 문서의  caching 문서 내용을 바탕으로 Request Memoization(요청 메모이제이션), Data Cache(데이터 캐시), Full Route Cache(전체 경로 캐시), 그리고 Client-side Router Cache(클라이언트 측 라우터 캐시)의 동작 원리와 활용 방법을 상세히 설명합니다.

14버전 기준의 공식 문서를 기반으로 작성했으며 15버전과 다를수 있으니 자세한 내용은 공식 문서를 참고해보시면 좋을 것 같습니다. 

Overview

매커니즘 설명 위치 목적 지속시간
Request Memoization 함수의 반환 값 서버 React 컴포넌트 트리에서 데이터를 재사용 요청당 생명 주기
Data Catche 데이터 서버 사용자 요청과 배포 간 데이터를 저장 영구적 (재검증 가능)
Full Route Cache HTML 및 RSC 페이지 로드 서버 렌더링 비용을 줄이고 성능 을 개선 영구적 (재검증 가능)
Router Cache RSC 페이로드 클라이언트 탐색 시 서버 요청을 줄임 사용자 세션 또는 시간 기반

 

React 서버 컴포넌트 페이로드(React Server Component Payload)란?

React 서버 컴포넌트 페이로드는 렌더링된 서버 컴포넌트 트리의 압축된 바이너리 표현입니다. 이는 클라이언트에서 React가 브라우저의 DOM을 업데이트하는 데 사용된다. 이 페이로드에는 다음이 포함됩니다.

  • 서버 컴포넌트의 렌더링 결과
  • 클라이언트 컴포넌트가 렌더링되어야 하는 자리 표시자(Placeholders) 및 해당 JavaScript 파일에 대한 참조
  • 서버 컴포넌트에서 클라이언트 컴포넌트로 전달되는 모든 props

기본 캐싱동작을 보여주는 다이어그램

1. Request Memoization (요청 메모이제이션)

Next.js는 fetch API를 확장하여 동일한 URL과 옵션을 가진 요청을 자동으로 메모이제이션합니다. 즉, 같은 요청이 여러 번 호출되더라도 실제 네트워크 요청은 한 번만 실행됩니다.

Request Memoization 동작 방식

요청 메모이제이션 동작 방식

How Request Memoization Works

  1. 최초 fetch 요청이 발생하면 **캐시 미스(cache MISS)**가 발생하고 데이터를 가져옵니다.
  2. 동일한 요청이 다시 호출되면 **캐시 히트(cache HIT)**가 되어 기존 데이터를 반환합니다.
  3. 렌더링 패스가 완료되면 캐시는 초기화되며, 이후 동일한 요청을 다시 실행하면 새로운 데이터를 가져옵니다.
async function getItem() {
  const res = await fetch('https://.../item/1');
  return res.json();
}

// 두 번 호출되지만 실제 네트워크 요청은 한 번만 발생
const item1 = await getItem(); // cache MISS
const item2 = await getItem(); // cache HIT

요청 메모이제이션 특징

  • Next.js가 아닌 React 자체의 기능입니다.
  • GET 요청에만 적용되며, POST, PUT, DELETE 요청은 메모이제이션되지 않습니다.
  • 서버 컴포넌트 내에서만 동작하며, Route Handlers에는 적용되지 않습니다.
  • fetch를 사용하지 않고 데이터베이스 클라이언트, CMS 클라이언트, GraphQL 클라이언트에서는 메모이제이션되지 않습니다.

2. Data Cache (데이터 캐시)

Next.js는 서버에서 fetch 요청을 실행할 때 데이터를 서버 요청과 배포 간에도 지속적으로 유지하는 데이터 캐시를 제공합니다. next/fetch를 사용하는 GET 요청에만 해당합니다. 

데이터 캐시 동작 방식

How the Data cache Works

  1. fetch 요청이 실행되면, Next.js는 데이터 캐시에서 캐시된 응답이 있는지 확인합니다.
    • 캐시된 응답이 있으면 즉시 반환됩니다.
    • 캐시된 응답이 없으면 데이터를 가져오고, 결과를 데이터 캐시에 저장합니다.
  2. 설정된 재검증(revalidate) 시간이 지나면 새로운 요청 시 데이터를 갱신합니다.
  3. cache: 'no-store' 설정 시, 캐시를 사용하지 않고 항상 새로운 데이터를 가져옵니다.
// 1시간(3600초) 동안 캐싱된 데이터를 사용
fetch('https://...', { next: { revalidate: 3600 } });

데이터 캐시 vs 요청 메모이제이션

요청 메모이제이션데이터 캐시

캐시 지속 시간 렌더링 중에만 유지 서버 요청과 배포 간에도 지속
적용 범위 동일한 요청이라면 같은 렌더링 내에서 재사용 특정 시간 또는 온디맨드로 갱신 가능
무효화 시점 렌더링이 끝나면 초기화됨 revalidate 설정 또는 수동 재검증 필요

3. Full Route Cache (전체 경로 캐시)

Next.js는 기본적으로 경로를 빌드 시점에 캐싱하여 페이지 로딩 속도를 최적화합니다. 서버에서 매번 렌더링할 필요 없이 캐시된 HTML을 빠르게 제공합니다.

Full Route Cache

전체 경로 캐시 동작 방식

  1. Next.js는 React 서버 컴포넌트를 React Server Component Payload 형식으로 렌더링합니다.
  2. 해당 페이로드와 HTML을 서버에서 캐싱합니다.
  3. 클라이언트에서는 HTML을 먼저 표시한 후, JavaScript를 로드하여 상호작용을 활성화합니다.
  4. 사용자가 동일한 경로를 다시 방문하면, 서버 요청 없이 캐시된 데이터가 반환됩니다.

전체 경로 캐시 무효화 방법

  1. 데이터 재검증(Revalidate)
    • 데이터가 갱신되면 해당 페이지도 다시 렌더링되어 캐시가 업데이트됩니다.
  2. 배포 시 초기화
    • 새로운 배포가 이루어지면 모든 캐시가 초기화됩니다.
export const dynamic = 'force-dynamic'; // Full Route Cache에서 제외
fetch('https://...', { cache: 'no-store' }); // 특정 데이터 요청만 캐시하지 않음

4. Client-side Router Cache (클라이언트 측 라우터 캐시)

Next.js는 클라이언트 측 라우터 캐시를 활용하여 페이지 간 탐색 속도를 최적화합니다. 이 캐시는 메모리에 저장되며, 레이아웃, 로딩 상태, 페이지별로 분리된 경로 세그먼트의 React Server Component(RSC) 페이로드를 저장합니다.

사용자가 페이지를 이동할 때, Next.js는 이전 방문 경로를 캐시하고, 사용자가 이동할 가능성이 높은 경로를 미리 가져오기(prefetch)하여 즉각적인 탐색을 지원합니다.
이로 인해 뒤로/앞으로 탐색이 빠르게 이루어지며, 전체 페이지가 다시 로드되지 않고 React의 상태와 브라우저 상태가 유지됩니다.

라우터 캐시의 주요 기능

  • 레이아웃(Layouts) 캐싱부분 렌더링 가능
    • 한 페이지에서 다른 페이지로 이동할 때, 공통 레이아웃이 다시 로드되지 않고 유지됨
    • 예를 들어, layout.tsx를 활용하는 경우, 페이지가 변경되더라도 레이아웃을 다시 렌더링하지 않음
  • 로딩 상태 유지
    • 네트워크 요청이 오래 걸리더라도, 이전 데이터가 유지되면서 더 부드러운 사용자 경험 제공
  • 페이지 캐싱
    • 기본적으로 페이지는 캐시되지 않지만, 브라우저의 뒤로/앞으로 탐색 시 재사용됨
    • staleTimes라는 실험적 설정 옵션을 활성화하면, 특정 페이지 세그먼트의 캐싱을 유지 가능

클라이언트 측 라우터 캐시 지속 시간 (Duration)

클라이언트 측 라우터 캐시는 브라우저의 임시 메모리에 저장되며, 캐시 지속 시간은 다음 두 가지 요인에 의해 결정됩니다.

  1. Session(세션) 기반 유지
    • 사용자가 다른 페이지로 탐색할 때는 캐시가 유지되지만, 페이지를 새로고침하면 캐시가 초기화됨
  2. 자동 무효화 기간 (Automatic Invalidation Period)
    • 레이아웃과 로딩 상태의 캐시는 특정 시간이 지나면 자동으로 무효화됨
    • 리소스가 어떻게 미리 가져왔는지, 정적으로 생성되었는지에 따라 지속 시간이 다름

무효화 기준

미리 가져오기(prefetch) 옵션동적 페이지정적 페이지

prefetch={null} 또는 미지정 캐시되지 않음 5분간 캐시
prefetch={true} 또는 router.prefetch 사용 5분간 캐시 5분간 캐시

클라이언트 측 라우터 캐시 무효화 (Invalidation)

라우터 캐시를 무효화하는 방법은 두 가지가 있습니다.

1. 서버에서 캐시 무효화

  • revalidatePath: 특정 경로의 데이터를 요청 시 재검증
  • revalidateTag: 특정 캐시 태그를 기반으로 재검증
  • cookies.set() 또는 cookies.delete() → 쿠키를 사용하는 경로가 최신 상태를 유지하도록 캐시 무효화

2. 클라이언트에서 캐시 무효화

  • router.refresh() → 현재 경로의 라우터 캐시를 무효화하고 새로운 서버 요청을 수행
import { useRouter } from 'next/navigation';

const router = useRouter();
router.refresh(); // 라우터 캐시 무효화

 


5. 캐싱을 적용할 때 혼란스러운 점과 주의할 점

Next.js의 다양한 캐싱 전략을 이해하는 것이 중요하지만, 실제 프로젝트에서 적용할 때 예상치 못한 문제가 발생할 수 있습니다.
다음은 자주 혼란을 일으키는 몇 가지 사항입니다.

1. Request Memoization은 Next.js의 확장된 fetch에서만 적용됨

Next.js의 Request Memoization(요청 메모이제이션)은 Next.js가 확장한 fetch API에서만 동작합니다.
즉, react-query, GraphQL 클라이언트(gqlClient) 등을 사용하면 Next.js가 제공하는 fetch 기반 캐싱을 활용할 수 없습니다.

2. Full Route Cache와 Client-side Router Cache의 무효화 차이

Full Route Cache를 무효화할 때, Client-side Router Cache는 자동으로 갱신되지 않습니다.

  • 예를 들어, next/fetch를 사용하지 않는다면 데이터 캐시를 활용할 수 없고 재검증도 이루어지지 않습니다.
  • 즉, Full Route Cache가 업데이트되어도 클라이언트에서 즉시 반영되지 않을 수 있습니다.

🔍 해결 방법:

  • router.refresh()를 사용하여 클라이언트에서 강제 무효화 실행
  • fetch를 사용할 때 cache: 'no-store' 옵션을 적용하여 항상 최신 데이터를 가져오기

3. Next.js 14 vs Next.js 15의 클라이언트 측 라우터 캐시 기본 동작 차이

  • Next.js 14에서는 페이지 세그먼트가 기본적으로 캐싱됨 (prefetch: true)
  • Next.js 15에서는 페이지 세그먼트가 기본적으로 옵트아웃됨 (prefetch: false)

즉, Next.js 15에서는 추가적인 설정 없이도 클라이언트 캐싱이 비활성화된 상태입니다.
이전 버전에서 동작하던 코드가 Next.js 15로 업데이트하면서 예상과 다르게 동작할 가능성이 있습니다.


마치며

Next.js의 캐싱 전략을 제대로 이해하고 활용하면, 성능을 극대화하고 사용자 경험을 개선할 수 있습니다. 하지만 각 캐싱 메커니즘이 다르게 동작하므로, 프로젝트의 요구사항에 맞게 신중하게 설계하는 것이 중요합니다.

캐싱은 단순한 성능 최적화가 아닙니다. 적절한 전략을 적용하면 서버 부하를 줄이고, 페이지 로드 속도를 개선하며, 불필요한 API 호출을 방지할 수 있습니다. 그러나 기대한 대로 동작하는지 면밀히 검토해야 합니다.

특히, Next.js App Router는 강력한 캐싱 메커니즘을 제공하지만, 실제 서비스에 적용했을 때 기존 Page Router보다 TPS 성능이 낮게 나타날 수 있습니다. SSR 비중이 높아질수록 서버 부하가 증가하고, 오히려 페이지 로드 속도가 느려질 가능성이 있습니다.

또한, SSR을 클라이언트에서 서버로 옮긴다고 해서 성능이 무조건 개선되는 것은 아닙니다. SSR 로직이 복잡해질수록 초기 렌더링이 지연되면서 사용자 경험이 저하될 수 있습니다. 단순히 "SSR이 좋다"는 생각보다는 Next.js 에서 제공하는 다양한 캐싱 매커니즘과 SSR, CSR의 각 방식의 장단점을 고려한 렌더링 전략을 선택하는 것이 중요합니다.