Next.js App Router 캐싱 완벽 가이드: 4가지 캐시 계층부터 use cache까지

Next.js App Router의 4가지 캐시 계층(Request Memoization, Data Cache, Full Route Cache, Router Cache)과 재검증 전략, use cache 디렉티브, 실무에서 자주 하는 캐싱 실수까지 정리합니다.

Next.js 캐싱, 도대체 왜 이렇게 어려운 걸까

Next.js App Router를 쓰다 보면 한 번쯤은 이런 경험을 하게 됩니다. 분명 데이터를 업데이트했는데 화면엔 반영이 안 되거나, 반대로 캐시를 설정했는데 매번 새로 데이터를 불러오거나. 솔직히 말하면, 저도 처음 App Router 캐싱을 접했을 때 꽤 고생했습니다.

그 이유는 의외로 단순합니다. Next.js의 캐싱이 하나가 아니라 4가지 독립적인 계층으로 구성되어 있기 때문이에요. 각 계층이 서로 다른 시점에, 서로 다른 위치에서, 서로 다른 규칙으로 동작합니다. 하나만 이해하고 나머지를 무시하면? 예상치 못한 동작이 발생할 수밖에 없죠.

이 글에서는 Next.js App Router의 캐싱 시스템을 처음부터 끝까지 쭉 정리합니다. 4가지 캐시 계층의 동작 원리, 재검증 전략, 최신 use cache 디렉티브, 그리고 실무에서 자주 하는 실수 패턴까지 전부 다룰 거예요. 자, 바로 들어가 봅시다.

4가지 캐시 계층 전체 구조

Next.js App Router의 캐싱은 다음 4가지 계층으로 이루어져 있습니다. 요청이 처리되는 순서대로 보면 이렇습니다.

  • Request Memoization — 하나의 렌더링 과정에서 동일한 fetch 요청 중복 방지
  • Data Cache — 서버에서 fetch 결과를 지속적으로 저장
  • Full Route Cache — 정적 라우트의 HTML과 RSC Payload를 서버에 저장
  • Router Cache — 클라이언트 브라우저에서 방문한 페이지의 RSC Payload를 저장

각 계층의 핵심 특성을 간단히 정리하면요.

  • Request Memoization: 서버 측, 단일 요청 수명 동안만 유지, React 자체 기능
  • Data Cache: 서버 측, 배포 간에도 지속됨, 명시적 재검증 필요
  • Full Route Cache: 서버 측, 재배포 시 초기화, 정적 라우트 전용
  • Router Cache: 클라이언트 측, 세션 동안 유지, 브라우저 메모리에 저장

이 4가지가 어떻게 서로 맞물려 돌아가는지, 하나씩 파헤쳐 보겠습니다.

Request Memoization: 렌더링 중 중복 요청 제거

Request Memoization은 사실 React의 기능입니다. Next.js가 아니에요. 이 점을 모르면 나중에 혼란스러워질 수 있습니다.

하나의 렌더링 과정에서 동일한 URL과 옵션을 가진 fetch 요청이 여러 컴포넌트에서 호출되면, 실제로는 한 번만 실행되고 나머지는 메모리에서 결과를 가져옵니다. 같은 데이터를 layout과 page에서 둘 다 필요로 할 때 특히 유용하죠.

// app/layout.tsx
async function getUser(id: string) {
  // 이 함수가 layout과 page에서 모두 호출되어도
  // 실제 네트워크 요청은 1번만 발생합니다
  const res = await fetch(`https://api.example.com/users/${id}`);
  return res.json();
}

export default async function Layout({ children }: { children: React.ReactNode }) {
  const user = await getUser('1');  // 첫 번째 호출 → 실제 fetch 실행
  return (
    <div>
      <nav>{user.name}</nav>
      {children}
    </div>
  );
}

// app/page.tsx
export default async function Page() {
  const user = await getUser('1');  // 두 번째 호출 → 메모리에서 반환
  return <h1>{user.name}의 대시보드</h1>;
}

여기서 꼭 기억해야 할 점들이 있습니다.

  • GET 메서드에만 적용됩니다 — POST, DELETE 같은 건 메모이제이션 안 됩니다
  • 렌더링이 끝나면 메모리가 바로 초기화됩니다 — 지속적인 캐시가 아니에요
  • fetch를 안 쓰는 경우(ORM이나 DB 클라이언트 등) React의 cache 함수로 동일한 효과를 얻을 수 있습니다
import { cache } from 'react';

// fetch를 사용하지 않는 데이터 소스에 대한 메모이제이션
export const getUser = cache(async (id: string) => {
  const user = await db.user.findUnique({ where: { id } });
  return user;
});

Data Cache: 서버의 지속적 데이터 저장소

Data Cache는 fetch 요청의 결과를 서버에 지속적으로 저장하는 캐시입니다. Request Memoization과는 성격이 완전히 다릅니다. 요청이 끝나도 사라지지 않고, 심지어 배포 간에도 유지돼요.

Next.js 15에서 달라진 기본값

이 부분, 정말 중요합니다. 진지하게요.

Next.js 14까지는 fetch 요청이 기본적으로 캐시되었습니다. 하지만 Next.js 15부터는 기본적으로 캐시하지 않습니다. 14에서 15로 업그레이드했는데 갑자기 앱이 느려졌다면, 십중팔구 이게 원인입니다.

// Next.js 15 기본 동작: 캐시 안 함 (매번 새로운 데이터)
const res = await fetch('https://api.example.com/products');

// 명시적으로 캐시 활성화
const res = await fetch('https://api.example.com/products', {
  cache: 'force-cache',
});

// 시간 기반 재검증 (10분마다)
const res = await fetch('https://api.example.com/products', {
  next: { revalidate: 600 },
});

// 태그 기반 캐시 (온디맨드 무효화 가능)
const res = await fetch('https://api.example.com/products', {
  next: { tags: ['products'] },
});

캐시 동작 흐름

cache: 'force-cache'를 설정했을 때의 흐름을 단계별로 보면 이렇습니다.

  1. 첫 번째 요청: 외부 데이터 소스에서 데이터를 가져와 Data Cache에 저장
  2. 이후 요청: Data Cache에서 즉시 반환 (네트워크 요청 없음)
  3. 재검증 시점 도달: 캐시된 데이터를 먼저 반환하고, 백그라운드에서 새 데이터 생성
  4. 새 데이터 생성 완료: Data Cache 업데이트, 다음 요청부터 새 데이터 제공

이게 바로 stale-while-revalidate 패턴입니다. 사용자는 항상 즉시 응답을 받고, 데이터 갱신은 백그라운드에서 조용히 이루어지죠. 꽤 영리한 방식이에요.

Full Route Cache: 정적 라우트의 사전 렌더링 결과 저장

Full Route Cache는 정적으로 렌더링된 라우트의 HTML과 RSC Payload를 서버에 저장합니다. 빌드 시점이나 재검증 시점에 만들어진 결과를 캐시해서, 매 요청마다 새로 렌더링할 필요가 없게 해주는 거예요.

Data Cache와의 관계를 이해하는 게 핵심입니다.

  • Data Cache를 무효화하면 → Full Route Cache도 무효화됩니다 (렌더링 결과가 데이터에 의존하니까요)
  • Full Route Cache를 무효화해도 → Data Cache는 영향받지 않습니다

그리고 동적 함수(cookies(), headers(), searchParams 등)를 사용하는 라우트는 Full Route Cache에서 제외됩니다. 이런 라우트는 매 요청마다 서버에서 새로 렌더링돼요.

// 정적 라우트 → Full Route Cache에 저장됨
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    cache: 'force-cache',
  });
  return <ProductList products={products} />;
}

// 동적 라우트 → Full Route Cache에서 제외됨
import { cookies } from 'next/headers';

export default async function DashboardPage() {
  const cookieStore = await cookies();
  const token = cookieStore.get('session');
  // cookies()를 사용했으므로 매 요청마다 렌더링
  return <Dashboard />;
}

Router Cache: 브라우저의 클라이언트 측 캐시

Router Cache는 4가지 중 유일하게 클라이언트(브라우저)에서 동작합니다. 사용자가 페이지를 이동할 때 서버에서 받은 RSC Payload를 브라우저 메모리에 저장해두고, 같은 페이지를 다시 방문하면 서버 요청 없이 바로 화면을 보여주는 거죠.

Next.js 15에서의 변화

Next.js 15부터는 페이지 세그먼트가 기본적으로 Router Cache에서 제외됩니다. 레이아웃과 로딩 상태는 여전히 캐시되지만, 페이지 자체는 안 됩니다. 다만 브라우저의 앞으로/뒤로 가기 시에는 재사용됩니다.

staleTimes 설정으로 이 동작을 바꿀 수 있어요.

// next.config.ts
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,   // 동적 페이지의 stale 시간 (초)
      static: 180,   // 정적 페이지의 stale 시간 (초)
    },
  },
};

여기서 실무에서 많은 혼란을 일으키는 부분이 하나 있습니다. 서버에서 revalidatePathrevalidateTag를 호출하면 Data Cache와 Full Route Cache가 무효화되고, Router Cache도 함께 무효화됩니다. 하지만 Route Handler에서 호출하면? Router Cache는 자동으로 무효화되지 않습니다. 이 차이를 모르면 "서버에서 분명 무효화했는데 왜 화면이 안 바뀌지?" 하는 상황에 빠지게 됩니다.

재검증 전략: 시간 기반 vs 온디맨드

Data Cache에 저장된 데이터를 갱신하는 방법은 크게 두 가지입니다.

시간 기반 재검증 (Time-based Revalidation)

설정한 시간이 지나고 다음 요청이 들어오면 백그라운드에서 데이터를 새로 만듭니다.

// 방법 1: fetch 옵션에서 설정
const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 }, // 1시간
});

// 방법 2: 라우트 세그먼트 설정
export const revalidate = 3600; // 이 라우트의 모든 데이터를 1시간 간격으로 재검증

export default async function Page() {
  // ...
}

주의할 점이 하나 있는데요. 한 라우트에 여러 fetch 요청이 있고 각각 다른 revalidate 값을 갖고 있으면, 가장 낮은 값이 전체 라우트에 적용됩니다. 이걸 모르면 "왜 내가 설정한 시간보다 빨리 재검증되지?" 하고 혼란스러울 수 있어요.

온디맨드 재검증 (On-demand Revalidation)

이벤트(폼 제출, 웹훅 등)가 발생했을 때 즉시 캐시를 무효화하는 방식입니다. 경로 기반과 태그 기반, 두 가지가 있습니다.

revalidatePath — 경로 기반 무효화

'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  // 데이터 변경 로직
  await db.post.create({ data: { /* ... */ } });

  // 특정 경로의 캐시 무효화
  revalidatePath('/blog');

  // 레이아웃 포함 전체 무효화
  revalidatePath('/', 'layout');
}

revalidateTag — 태그 기반 무효화

// 데이터 페칭 시 태그 지정
const res = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});

// Server Action에서 태그 기반 무효화
'use server';

import { revalidateTag } from 'next/cache';

export async function publishPost() {
  await db.post.update({ /* ... */ });

  // profile="max"로 stale-while-revalidate 방식 사용 (권장)
  revalidateTag('posts', 'max');
}

updateTag — 즉시 만료 (Server Action 전용)

Next.js에 새로 도입된 updateTagrevalidateTag와는 좀 다른 목적을 가지고 있습니다.

'use server';

import { updateTag } from 'next/cache';

export async function toggleLike(postId: string) {
  await db.like.toggle({ postId });

  // 즉시 캐시를 만료시켜 사용자가 변경 사항을 바로 확인
  updateTag('likes');
}

둘의 핵심 차이를 정리하면요.

  • revalidateTag: 백그라운드에서 갱신, stale 데이터를 먼저 제공 (Route Handler와 Server Action 모두 사용 가능)
  • updateTag: 즉시 만료, 사용자가 변경 사항을 바로 확인 (Server Action에서만 사용 가능)

선택 기준은 간단합니다. 좋아요 버튼 누르기, 설정 변경처럼 즉각적인 피드백이 필요하면 updateTag. CMS 글 발행이나 재고 업데이트처럼 백그라운드 갱신이면 revalidateTag를 쓰면 됩니다.

use cache 디렉티브: Next.js 캐싱의 미래

지금까지 얘기한 캐싱 시스템은 주로 fetch API 중심이었습니다. 근데 실무에서는 데이터베이스 직접 쿼리, 파일 시스템 작업, 외부 SDK 호출 등 fetch를 안 쓰는 데이터 소스가 훨씬 많잖아요. 이전에는 이런 경우에 unstable_cache라는 (이름부터 불안한) 실험적 API를 써야 했습니다.

use cache 디렉티브는 이 문제를 근본적으로 해결합니다. 파일, 컴포넌트, 함수 수준에서 선언적으로 캐싱을 적용할 수 있어요.

설정 및 활성화

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

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;

기본 사용법

세 가지 레벨에서 사용할 수 있습니다. 함수, 컴포넌트, 그리고 파일 전체.

// 함수 수준 캐싱
import { cacheTag, cacheLife } from 'next/cache';

async function getProducts(categoryId: string) {
  'use cache';
  cacheTag('products', `category-${categoryId}`);
  cacheLife('hours');

  // DB 직접 쿼리도 캐시됩니다
  const products = await db.product.findMany({
    where: { categoryId },
  });
  return products;
}

// 컴포넌트 수준 캐싱
export async function ProductCard({ id }: { id: string }) {
  'use cache';
  cacheLife('days');
  cacheTag(`product-${id}`);

  const product = await db.product.findUnique({ where: { id } });
  return (
    <div>
      <h3>{product.name}</h3>
      <p>{product.price}원</p>
    </div>
  );
}

// 파일 수준 캐싱
'use cache';

import { cacheLife } from 'next/cache';

cacheLife('weeks');

export default async function AboutPage() {
  const content = await cms.getPage('about');
  return <article>{content.body}</article>;
}

cacheLife 프로필 상세

cacheLife는 세 가지 속성으로 캐시 수명을 조절합니다.

  • stale: 클라이언트 Router Cache에서의 유효 시간 (초)
  • revalidate: 서버에서 백그라운드 재생성 간격 (초)
  • expire: 캐시 엔트리의 최대 수명, 이후 트래픽이 없으면 삭제 (초)

기본 제공 프로필(minutes, hours, days, weeks)을 그냥 쓰거나, 프로젝트에 맞는 커스텀 프로필을 직접 정의할 수 있습니다.

// next.config.ts — 커스텀 프로필 정의
const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    // 에디토리얼 콘텐츠용 커스텀 프로필
    editorial: {
      stale: 600,       // 10분 (클라이언트)
      revalidate: 3600, // 1시간 (서버 백그라운드 재생성)
      expire: 86400,    // 24시간 (최대 수명)
    },
    // 상품 데이터용 커스텀 프로필
    product: {
      stale: 300,       // 5분
      revalidate: 900,  // 15분
      expire: 43200,    // 12시간
    },
  },
};

// 사용법
import { cacheLife } from 'next/cache';

export default async function EditorialPage() {
  'use cache';
  cacheLife('editorial');
  // ...
}

// 인라인 프로필도 가능 (일회성)
export default async function SpecialPage() {
  'use cache';
  cacheLife({ stale: 120, revalidate: 600, expire: 3600 });
  // ...
}

참고로 expire는 반드시 revalidate보다 커야 합니다. 안 그러면 설정 오류가 나요. 그리고 클라이언트 측 stale 값은 최소 30초가 강제됩니다.

use cache의 변형

use cache에는 두 가지 특수한 변형이 있는데, 용도가 명확합니다.

  • use cache: private — 사용자별 캐시. 로그인한 사용자의 개인화된 데이터를 캐시할 때 씁니다
  • use cache: remote — 원격 공유 캐시. 서버리스 환경에서 모든 인스턴스가 동일한 캐시를 공유해야 할 때 유용합니다

실무에서 자주 하는 7가지 캐싱 실수

여기서부터가 사실 이 글의 핵심입니다. 이론을 아무리 잘 알아도, 실무에서 이런 실수들을 반복하게 되거든요.

실수 1: Next.js 15의 기본값 변화를 모름

Next.js 14에서 15로 올리면서 fetch의 기본 캐시 동작이 바뀐 걸 모르는 분들이 정말 많습니다. 14에서는 force-cache가 기본이었고, 15부터는 no-store가 기본이에요. 업그레이드 후에 성능이 눈에 띄게 떨어졌다면, 이거부터 확인하세요.

실수 2: Request Memoization을 영속 캐시로 착각

Request Memoization은 하나의 렌더링 사이클 동안만 유지됩니다. 딱 거기까지입니다. "같은 데이터를 여러 컴포넌트에서 쓰니까 알아서 캐시될 거야"라고 생각하고 revalidate 설정을 빠뜨리면, 매 요청마다 외부 API를 호출하게 됩니다.

실수 3: 상충되는 캐시 옵션

// 이렇게 하면 안 됩니다!
const res = await fetch(url, {
  cache: 'no-store',
  next: { revalidate: 3600 },
});
// cache: 'no-store'와 revalidate를 동시에 설정하면
// 둘 다 무시됩니다. 개발 모드에서 경고가 표시됩니다.

의외로 이런 코드를 프로덕션에서 가끔 봅니다. 개발 모드에서 콘솔 경고가 뜨긴 하는데, 놓치기 쉬워요.

실수 4: revalidate의 동작을 오해

revalidate: 3600은 "매시간 자동 새로고침"이 아닙니다. "1시간이 지난 후 다음 요청이 오면 백그라운드에서 재생성"이에요. 1시간이 지나도 요청이 안 들어오면? 아무 일도 안 일어납니다. 그리고 재생성이 진행되는 동안 들어온 요청은 여전히 이전(stale) 데이터를 받습니다.

실수 5: Router Cache 무효화 누락

서버에서 revalidatePath를 호출해서 Data Cache를 깔끔하게 무효화했는데, 클라이언트에서는 여전히 이전 데이터가 보이는 경우. 특히 Route Handler에서 호출했을 때 이런 일이 발생합니다. Server Action에서 호출해야 Router Cache까지 같이 날아갑니다.

실수 6: Route Handler에서의 revalidate 설정

export const revalidate = 3600 같은 세그먼트 설정은 Route Handler에서 안 먹힙니다. Page, Layout, Server Component에서만 적용돼요. Route Handler에서 캐시를 제어하려면 fetch 옵션이나 use cache를 직접 써야 합니다.

실수 7: 혼합된 revalidate 값의 영향을 모름

한 라우트 안에서 여러 fetch 요청이 서로 다른 revalidate 값을 가지면, 가장 낮은 값이 전체 라우트에 적용됩니다. 특히 주의할 건, 하나의 fetchrevalidate: 0이나 cache: 'no-store'가 하나라도 있으면 전체 라우트가 동적 렌더링으로 전환된다는 점입니다.

실전 캐싱 패턴 모음

이론은 여기까지 하고, 실제 프로젝트에서 바로 참고할 수 있는 패턴들을 정리해 봤습니다.

패턴 1: 전자상거래 상품 페이지

상품 페이지는 데이터마다 변경 빈도가 다릅니다. 상품 기본 정보는 거의 안 변하고, 재고는 실시간에 가까워야 하고, 리뷰는 그 중간이죠. 이럴 때 데이터별로 캐시 전략을 다르게 가져가면 됩니다.

import { Suspense } from 'react';
import { cacheTag, cacheLife } from 'next/cache';

// 상품 기본 정보: 자주 안 변함 → 강한 캐시
async function getProductInfo(id: string) {
  'use cache';
  cacheTag(`product-${id}`);
  cacheLife('days');
  return await db.product.findUnique({ where: { id } });
}

// 재고 현황: 자주 변함 → 짧은 캐시
async function getStock(productId: string) {
  'use cache';
  cacheTag(`stock-${productId}`);
  cacheLife({ stale: 30, revalidate: 60, expire: 300 });
  return await db.inventory.findFirst({ where: { productId } });
}

// 리뷰: 중간 빈도 → 태그 기반 온디맨드 무효화
async function getReviews(productId: string) {
  'use cache';
  cacheTag(`reviews-${productId}`);
  cacheLife('hours');
  return await db.review.findMany({ where: { productId } });
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProductInfo(params.id);

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>

      <Suspense fallback={<p>재고 확인 중...</p>}>
        <StockStatus productId={params.id} />
      </Suspense>

      <Suspense fallback={<p>리뷰 로딩 중...</p>}>
        <ReviewList productId={params.id} />
      </Suspense>
    </main>
  );
}

패턴 2: 블로그 + CMS 웹훅 연동

CMS에서 글이 발행되면 웹훅으로 캐시를 자동 무효화하는 패턴입니다. 실무에서 아주 흔하게 쓰이는 조합이에요.

// app/blog/page.tsx
import { cacheTag, cacheLife } from 'next/cache';

async function getBlogPosts() {
  'use cache';
  cacheTag('blog-posts');
  cacheLife('days'); // 기본은 오래 캐시
  return await cms.getPosts();
}

// app/api/webhook/cms/route.ts — CMS가 글 발행 시 호출
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-webhook-secret');
  if (secret !== process.env.CMS_WEBHOOK_SECRET) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await request.json();

  // 글이 발행/수정되면 관련 캐시 무효화
  revalidateTag('blog-posts', 'max');

  if (body.slug) {
    revalidateTag(`post-${body.slug}`, 'max');
  }

  return NextResponse.json({ revalidated: true });
}

패턴 3: 사용자 대시보드 — 개인화 데이터 처리

대시보드는 공통 데이터와 개인 데이터가 섞여 있는 전형적인 케이스입니다. 공통 부분은 캐시하고, 개인 부분은 매번 새로 가져오는 게 핵심이에요.

import { cookies } from 'next/headers';

// 공통 데이터: 모든 사용자에게 동일 → 캐시 적극 활용
async function getDashboardConfig() {
  'use cache';
  cacheLife('days');
  cacheTag('dashboard-config');
  return await db.config.findFirst({ where: { type: 'dashboard' } });
}

// 개인 데이터: 사용자별 다름 → 캐시 사용하지 않음
async function getUserStats(userId: string) {
  // use cache 없음 — 매 요청마다 새로 가져옴
  return await db.stats.findUnique({ where: { userId } });
}

export default async function DashboardPage() {
  const cookieStore = await cookies();
  const userId = cookieStore.get('user-id')?.value;

  const config = await getDashboardConfig();     // 캐시됨
  const stats = await getUserStats(userId!);     // 매번 새로 가져옴

  return (
    <div>
      <h1>{config.title}</h1>
      <StatsPanel data={stats} />
    </div>
  );
}

자주 묻는 질문 (FAQ)

Next.js의 revalidate 설정이 작동하지 않는 이유는 무엇인가요?

가장 흔한 원인은 세 가지입니다. 첫째, Route Handler에서 export const revalidate를 쓴 경우 — 이 설정은 Page와 Layout에서만 작동합니다. 둘째, cache: 'no-store'revalidate를 동시에 설정한 경우에는 상충되어 둘 다 무시돼요. 셋째, 동적 함수(cookies(), headers())를 사용하는 라우트에서는 Full Route Cache가 적용되지 않아 매번 새로 렌더링됩니다.

revalidatePath와 revalidateTag 중 어떤 것을 사용해야 하나요?

revalidatePath는 특정 경로의 모든 캐시를 한꺼번에 날릴 때 사용합니다. 간단하지만 범위가 넓어요. revalidateTag는 특정 데이터만 정밀하게 무효화할 때 씁니다. 하나의 페이지에 여러 데이터 소스가 있을 때, 변경된 데이터만 골라서 갱신하고 싶다면 revalidateTag가 훨씬 효율적입니다. 개인적으로는 태그 기반을 기본으로 쓰는 걸 추천합니다.

Next.js 15에서 fetch 캐시 기본값이 왜 변경되었나요?

Next.js 팀이 "숨겨진 캐시는 없어야 한다"는 원칙을 채택했기 때문입니다. 이전 버전에서 fetch가 기본 캐시되면서 개발자들이 의도치 않게 오래된 데이터를 보여주는 문제가 너무 많았거든요. 이제는 캐싱을 원하면 명시적으로 cache: 'force-cache'next: { revalidate: N }을 설정해야 합니다.

use cache 디렉티브는 프로덕션에서 사용해도 되나요?

use cachecacheComponents: true 설정으로 활성화할 수 있고, Next.js 팀이 적극적으로 밀고 있는 기능입니다. fetch 기반 캐싱을 넘어서 DB 쿼리, 파일 시스템 작업 등 모든 비동기 작업에 선언적 캐싱을 적용할 수 있어서 앞으로 표준이 될 가능성이 높습니다. 다만 프로덕션에 도입하기 전에는 충분히 테스트해보시길 권장합니다.

Router Cache 때문에 최신 데이터가 안 보이는 문제는 어떻게 해결하나요?

가장 확실한 방법은 데이터 변경을 Server Action에서 처리하는 겁니다. Server Action 안에서 revalidatePathrevalidateTag를 호출하면 서버 측 캐시와 클라이언트 Router Cache가 모두 무효화됩니다. Route Handler에서 처리하는 경우에는 Router Cache가 자동 무효화되지 않으니, 클라이언트에서 router.refresh()를 추가로 호출해줘야 합니다.

저자 소개 Editorial Team

Our team of expert writers and editors.