콘텐츠로 이동

캐싱 전략 총정리

Next.js는 성능 최적화를 위해 다양한 캐싱 메커니즘을 제공합니다. 이 글에서는 4가지 캐시의 동작 원리와 제어 방법을 알아봅니다.

종류대상장소목적기간
Request Memoizationfetch 함수의 return 값서버React Component tree에서 데이터의 재사용request 생명주기
Data CacheData서버유저 요청이나 deployment에 의해 저장된 데이터영구적
Full Route CacheHTML, RSC Payload서버렌더링 비용 감소 및 성능 향상영구적
Router CacheRSC Payload클라이언트네비게이션에 의한 서버 요청 감소세션 또는 정해진 시간 동안

정확히 동일한 요청일 때 캐싱됩니다 (headers 구성이 다르면 캐싱 미발생).

// 가능한 한 캐싱
fetch('url', { cache: 'force-cache' })
// 이 요청은 캐싱되지 않음 → 항상 새로운 데이터
fetch('url', { cache: 'no-store' })
fetch("url", {
next: {
revalidate: 60, // 60초마다 재검증
},
});
export const revalidate = 5; // 5초마다 재검증
export default function SomeComponent() {
// ...
}
// 항상 재요청 (캐싱되지 않음)
export const dynamic = "force-dynamic";

페이지 내 일부 컴포넌트만 캐싱을 비활성화할 때 사용합니다.

import { unstable_noStore as noStore } from "next/cache";
export default async function Component() {
noStore();
const result = await db.query(/* ... */);
// ...
}

참고: Next.js 15에서 unstable_noStore는 deprecated되었습니다. 대신 import { connection } from "next/server"를 사용하고, 컴포넌트 내에서 await connection()을 호출합니다.

npm run build 시 사용됩니다. 동적 라우팅을 사용한 경우 동적 페이지로 빌드되나, 나머지는 가능한 한 정적 페이지로 빌드됩니다.

가능한 한 많은 부분을 캐싱하면서 필요할 때만 업데이트된 데이터를 얻을 수 있습니다.

import { revalidatePath } from "next/cache";
revalidatePath('/feed', 'page'); // 특정 페이지 재검증
revalidatePath('/', 'layout'); // 모든 페이지 재검증

fetch 요청에 태그를 추가하고, 태그 기반으로 캐시를 초기화합니다.

// 데이터 페칭 시 태그 추가
fetch("url", {
next: { tags: ["msg"] },
});
// 데이터 변경 시 태그로 캐시 초기화
async function updateHandler() {
// ...데이터 변경
revalidateTag("msg");
redirect("/");
}

직접 데이터베이스에 접근하는 경우 fetch를 사용하지 않으므로, React의 cache 함수를 이용합니다.

import { cache } from "react";
export const getMsg = cache(function getMsg() {
return db.collection.find("doc");
});

Object.is로 얕은 비교를 하므로, 인라인 객체를 인자로 전달하면 항상 cache miss가 발생합니다.

// Anti-pattern: 매번 새 객체 → cache miss
const getUser = cache(async (params) => {
return await db.user.findUnique({ where: { id: params.uid } });
});
getUser({ uid: 1 });
getUser({ uid: 1 }); // 다시 쿼리 실행
// 올바른 패턴: 원시값을 인자로 사용
const getUser = cache(async (uid) => {
return await db.user.findUnique({ where: { id: uid } });
});
getUser(1);
getUser(1); // cache hit!
  • Next.js의 fetch는 자동으로 요청 중복 제거가 적용되므로 React.cache() 래핑이 불필요
  • DB 쿼리, 인증 확인, 파일시스템 작업 등 fetch가 아닌 비동기 작업에는 React.cache()가 필수적
import { unstable_cache as nextCache } from "next/cache";
import { cache } from "react";
export const getMsg = nextCache(
cache(function getMsg() { // 요청을 cache
return db.collection.find("doc");
}),
["message"], // cache를 구분하기 위한 키
{ tags: ["msg"] } // revalidateTag용 태그
);
function someUpdateFunc() {
// ...데이터 변경
revalidateTag("msg"); // msg 태그의 cache 초기화
}

참고: unstable_cache는 Next.js 15에서 use cache 지시문으로 대체되었습니다.

React.cache()는 단일 요청 내에서만 작동합니다. 여러 요청에 걸쳐 데이터를 공유하려면 LRU 캐시를 사용합니다.

import { LRUCache } from "lru-cache";
const cache = new LRUCache({
max: 1000,
ttl: 5 * 60 * 1000, // 5분
});
export async function getUser(id) {
const cached = cache.get(id);
if (cached) return cached;
const user = await db.user.findUnique({ where: { id } });
cache.set(id, user);
return user;
}
  • Vercel Fluid Compute 환경에서는 동일 인스턴스를 여러 요청이 공유하므로 LRU 캐시가 효과적
  • 전통적인 serverless 환경에서는 Redis 같은 외부 캐시를 고려
캐시 종류제어 방법
Request Memoizationcache: 'no-store', noStore()
Data Cacherevalidate, revalidateTag()
Full Route CacherevalidatePath(), dynamic
Router Cache클라이언트 네비게이션 시 자동
Custom (DB 등)React.cache(), unstable_cache

다음 글에서는 데이터 페칭과 Server Action을 살펴봅니다.