본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 6장: Cache Components와 'use cache' 디렉티브
2026년 1월 30일·웹 개발·

6장: Cache Components와 'use cache' 디렉티브

Next.js 16의 Cache Components 시스템을 다룹니다. 'use cache' 디렉티브, cacheLife 프로필, cacheTag 무효화, 세 가지 캐시 변형을 살펴봅니다.

17분861자8개 섹션
nextjsreacttypescriptfrontend
공유
nextjs-app-router6 / 13
12345678910111213
이전5장: 데이터 페칭과 캐싱 전략의 대전환다음7장: 스트리밍 SSR과 로딩 UI 전략

5장에서 Next.js의 캐싱 레이어와 unstable_cache를 다루었습니다. 이번 장에서는 Next.js 16이 도입한 Cache Components 시스템과 "use cache" 디렉티브를 살펴봅니다. 기존 캐싱 API의 한계를 넘어서는, 선언적이고 직관적인 새로운 캐싱 패러다임입니다.

기존 캐싱의 한계

5장에서 살펴본 캐싱 시스템에는 몇 가지 한계가 있었습니다.

첫째, fetch() 기반 캐싱은 fetch() API를 사용하는 경우에만 적용됩니다. ORM이나 직접 데이터베이스 호출에는 unstable_cache라는 별도 API가 필요했습니다.

둘째, 캐싱의 단위가 데이터 요청(fetch) 또는 라우트 전체로 제한되었습니다. 컴포넌트 단위의 세밀한 캐싱은 불가능했습니다.

셋째, unstable_cache는 이름 그대로 불안정한 API였으며, 캐시 키를 수동으로 관리해야 하는 부담이 있었습니다.

Next.js 16의 "use cache" 디렉티브는 이 모든 한계를 해결합니다.

"use cache" 디렉티브

"use cache"는 "use client"나 "use server"처럼 파일 또는 함수의 최상단에 선언하는 디렉티브입니다. 선언된 범위의 실행 결과를 자동으로 캐시합니다.

세 가지 적용 레벨

"use cache"는 페이지, 컴포넌트, 함수의 세 가지 레벨에서 적용할 수 있습니다.

페이지 레벨 캐싱
typescript
// app/products/page.tsx
"use cache";
 
import { db } from '@/lib/db';
 
export default async function ProductsPage() {
  const products = await db.products.findMany();
 
  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}
컴포넌트 레벨 캐싱
typescript
// components/Sidebar.tsx
async function Sidebar() {
  "use cache";
 
  const categories = await db.categories.findMany();
  const popularTags = await db.tags.findMany({
    orderBy: { count: 'desc' },
    take: 20,
  });
 
  return (
    <aside>
      <CategoryList categories={categories} />
      <TagCloud tags={popularTags} />
    </aside>
  );
}
함수 레벨 캐싱
typescript
// lib/data.ts
async function getProductById(id: string) {
  "use cache";
 
  return await db.products.findUnique({
    where: { id },
    include: { reviews: true, category: true },
  });
}
Info

"use cache"를 파일 최상단에 선언하면 해당 파일의 모든 export가 캐시됩니다. 함수 본문 최상단에 선언하면 해당 함수만 캐시됩니다.

기존 방식과의 비교

unstable_cache (기존)
typescript
import { unstable_cache } from 'next/cache';
 
const getCachedProducts = unstable_cache(
  async () => {
    return await db.products.findMany();
  },
  ['products'],           // 캐시 키 수동 관리
  { revalidate: 3600, tags: ['products'] }
);
use cache (Next.js 16)
typescript
async function getProducts() {
  "use cache";
  cacheLife('hours');
  cacheTag('products');
 
  return await db.products.findMany();
}

"use cache"는 캐시 키를 자동으로 생성하며, 함수의 인자가 캐시 키의 일부로 포함됩니다. 개발자가 수동으로 캐시 키를 관리할 필요가 없습니다.

cacheLife: 캐시 수명 프로필

cacheLife()는 캐시된 데이터의 수명을 제어하는 함수입니다. 미리 정의된 프로필을 사용하거나 커스텀 프로필을 생성할 수 있습니다.

내장 프로필

프로필stalerevalidateexpire
"seconds"즉시1초60초
"minutes"5분1분1시간
"hours"5분1시간1일
"days"5분1일1주
"weeks"5분1주1달
"max"5분1달무기한

각 프로필의 세 가지 값은 다음을 의미합니다.

  • stale: 클라이언트 캐시에서 stale 상태로 유지되는 시간
  • revalidate: 서버에서 백그라운드 재검증이 시작되는 간격
  • expire: 데이터가 완전히 만료되어 더 이상 사용할 수 없는 시간
내장 프로필 사용
typescript
import { cacheLife } from 'next/cache';
 
async function getCategories() {
  "use cache";
  cacheLife('days');  // 카테고리는 자주 변하지 않으므로 days 프로필
 
  return await db.categories.findMany();
}
 
async function getTrendingPosts() {
  "use cache";
  cacheLife('minutes');  // 트렌딩은 자주 변하므로 minutes 프로필
 
  return await db.posts.findMany({
    orderBy: { viewCount: 'desc' },
    take: 10,
  });
}

커스텀 프로필 정의

내장 프로필이 요구사항에 맞지 않으면, next.config.ts에서 커스텀 프로필을 정의할 수 있습니다.

next.config.ts
typescript
import type { NextConfig } from 'next';
 
const nextConfig: NextConfig = {
  experimental: {
    cacheLife: {
      'product-detail': {
        stale: 300,       // 5분간 stale 허용
        revalidate: 900,  // 15분마다 재검증
        expire: 86400,    // 24시간 후 만료
      },
      'user-feed': {
        stale: 0,         // stale 허용 안 함
        revalidate: 30,   // 30초마다 재검증
        expire: 300,      // 5분 후 만료
      },
      'static-content': {
        stale: 600,       // 10분 stale 허용
        revalidate: 604800, // 1주마다 재검증
        expire: false,    // 만료 없음
      },
    },
  },
};
 
export default nextConfig;
커스텀 프로필 사용
typescript
import { cacheLife } from 'next/cache';
 
async function getProduct(id: string) {
  "use cache";
  cacheLife('product-detail');
 
  return await db.products.findUnique({ where: { id } });
}
 
async function getUserFeed(userId: string) {
  "use cache";
  cacheLife('user-feed');
 
  return await db.feeds.findMany({
    where: { userId },
    orderBy: { createdAt: 'desc' },
  });
}
Tip

cacheLife()를 호출하지 않으면 기본 프로필이 적용됩니다. 기본 프로필은 Next.js 설정에서 "default" 키로 커스터마이징할 수 있습니다.

인라인 수명 지정

프로필 이름 대신 직접 값을 전달할 수도 있습니다.

인라인 수명 지정
typescript
import { cacheLife } from 'next/cache';
 
async function getRealtimeStats() {
  "use cache";
  cacheLife({ stale: 0, revalidate: 10, expire: 60 });
 
  return await analytics.getStats();
}

cacheTag: 세밀한 캐시 무효화

cacheTag()는 캐시된 항목에 태그를 부여하여 revalidateTag()로 선택적 무효화를 가능하게 합니다.

cacheTag 사용
typescript
import { cacheTag, cacheLife } from 'next/cache';
 
async function getProduct(id: string) {
  "use cache";
  cacheLife('hours');
  cacheTag('products', `product-${id}`);
 
  return await db.products.findUnique({
    where: { id },
    include: { reviews: true },
  });
}
 
async function getProductReviews(productId: string) {
  "use cache";
  cacheLife('minutes');
  cacheTag(`reviews-${productId}`);
 
  return await db.reviews.findMany({
    where: { productId },
    orderBy: { createdAt: 'desc' },
  });
}
Server Action에서 무효화
typescript
'use server';
 
import { revalidateTag } from 'next/cache';
 
export async function updateProduct(id: string, data: ProductUpdateData) {
  await db.products.update({ where: { id }, data });
 
  revalidateTag(`product-${id}`);  // 해당 상품만 무효화
}
 
export async function addReview(productId: string, review: ReviewData) {
  await db.reviews.create({
    data: { ...review, productId },
  });
 
  revalidateTag(`reviews-${productId}`);  // 해당 상품의 리뷰만 무효화
  revalidateTag(`product-${productId}`);  // 상품 정보도 무효화 (리뷰 수 등)
}

세 가지 캐시 변형

"use cache" 디렉티브는 세 가지 변형을 제공하며, 캐시 저장소의 위치와 공유 범위가 다릅니다.

"use cache" (기본)

가장 일반적인 형태로, 서버의 공유 캐시에 저장됩니다. 모든 사용자와 모든 요청에서 같은 캐시를 공유합니다.

기본 캐시
typescript
async function getGlobalSettings() {
  "use cache";
  cacheLife('days');
 
  return await db.settings.findFirst();
}

"use cache: remote"

원격 캐시 저장소(CDN 엣지 등)에 저장됩니다. 여러 서버 인스턴스 간에 캐시를 공유해야 하는 분산 환경에 적합합니다.

원격 캐시
typescript
async function getPopularProducts() {
  "use cache: remote";
  cacheLife('hours');
  cacheTag('popular-products');
 
  return await db.products.findMany({
    orderBy: { salesCount: 'desc' },
    take: 50,
  });
}
Info

"use cache: remote"는 배포 환경의 인프라에 따라 동작이 달라집니다. AWS, Vercel 등의 플랫폼에서 제공하는 분산 캐시 레이어와 통합됩니다.

"use cache: private"

요청별 또는 사용자별 캐시로, 다른 사용자와 공유되지 않습니다. 사용자 맞춤 데이터를 캐싱할 때 사용합니다.

프라이빗 캐시
typescript
import { cookies } from 'next/headers';
 
async function getUserDashboard() {
  "use cache: private";
  cacheLife('minutes');
 
  const cookieStore = await cookies();
  const userId = cookieStore.get('userId')?.value;
 
  if (!userId) return null;
 
  return await db.dashboards.findUnique({
    where: { userId },
    include: { widgets: true, recentActivity: true },
  });
}
Warning

"use cache: private"는 사용자별로 별도의 캐시 엔트리를 생성하므로, 메모리 사용량이 사용자 수에 비례하여 증가할 수 있습니다. 캐시 수명을 적절히 짧게 설정하는 것이 중요합니다.

세 변형 비교

변형저장소공유 범위적합한 데이터
"use cache"서버 로컬전체 사용자공개 데이터
"use cache: remote"원격(CDN)전체 사용자, 분산 서버인기 콘텐츠
"use cache: private"서버 로컬개별 사용자맞춤 데이터

실전: 전자상거래 페이지 캐싱 설계

5장의 전자상거래 예제를 "use cache"로 재구현해 봅니다.

lib/products.ts
typescript
import { cacheLife, cacheTag } from 'next/cache';
import { db } from '@/lib/db';
 
export async function getProduct(id: string) {
  "use cache";
  cacheLife('hours');
  cacheTag('products', `product-${id}`);
 
  return await db.products.findUnique({
    where: { id },
    include: { category: true },
  });
}
 
export async function getProductReviews(productId: string) {
  "use cache";
  cacheLife('minutes');
  cacheTag(`reviews-${productId}`);
 
  return await db.reviews.findMany({
    where: { productId },
    orderBy: { createdAt: 'desc' },
  });
}
components/ProductStock.tsx
typescript
// 재고는 실시간이므로 캐싱하지 않음
async function ProductStock({ productId }: { productId: string }) {
  const stock = await db.inventory.findUnique({
    where: { productId },
  });
 
  return (
    <div className={stock && stock.quantity > 0 ? 'text-green-600' : 'text-red-600'}>
      {stock && stock.quantity > 0 ? `${stock.quantity}개 남음` : '품절'}
    </div>
  );
}
components/UserRecommendations.tsx
typescript
import { cacheLife, cacheTag } from 'next/cache';
 
// 사용자별 추천은 private 캐시 사용
async function UserRecommendations({ userId }: { userId: string }) {
  "use cache: private";
  cacheLife('minutes');
  cacheTag(`recommendations-${userId}`);
 
  const recommendations = await db.recommendations.findMany({
    where: { userId },
    take: 8,
  });
 
  return (
    <section>
      <h3 className="text-lg font-semibold">맞춤 추천</h3>
      <div className="grid grid-cols-4 gap-4">
        {recommendations.map(item => (
          <ProductCard key={item.id} product={item.product} />
        ))}
      </div>
    </section>
  );
}
app/products/[id]/page.tsx
typescript
import { Suspense } from 'react';
import { getProduct, getProductReviews } from '@/lib/products';
 
interface PageProps {
  params: Promise<{ id: string }>;
}
 
export default async function ProductPage({ params }: PageProps) {
  const { id } = await params;
  const product = await getProduct(id);
 
  if (!product) return <div>상품을 찾을 수 없습니다.</div>;
 
  return (
    <div className="mx-auto max-w-6xl">
      <ProductInfo product={product} />
 
      <Suspense fallback={<div>재고 확인 중...</div>}>
        <ProductStock productId={id} />
      </Suspense>
 
      <Suspense fallback={<div>리뷰 로딩 중...</div>}>
        <ReviewSection productId={id} />
      </Suspense>
 
      <Suspense fallback={<div>추천 상품 로딩 중...</div>}>
        <UserRecommendations userId="current-user" />
      </Suspense>
    </div>
  );
}

이 구조에서 각 섹션은 독립적인 캐싱 전략을 가집니다. 상품 정보는 시간 단위로, 리뷰는 분 단위로, 재고는 매 요청마다, 추천은 사용자별 프라이빗 캐시로 관리됩니다. 7장에서 다루는 Suspense와 결합하면 각 섹션이 독립적으로 스트리밍되어 최적의 UX를 제공합니다.

unstable_cache에서 마이그레이션

기존 unstable_cache 코드를 "use cache"로 마이그레이션하는 방법입니다.

Before: unstable_cache
typescript
import { unstable_cache } from 'next/cache';
 
const getCachedUser = unstable_cache(
  async (id: string) => {
    return await db.users.findUnique({ where: { id } });
  },
  ['user'],
  { revalidate: 900, tags: ['users'] }
);
 
// 사용
const user = await getCachedUser('user-1');
After: use cache
typescript
import { cacheLife, cacheTag } from 'next/cache';
 
async function getUser(id: string) {
  "use cache";
  cacheLife({ stale: 300, revalidate: 900, expire: 86400 });
  cacheTag('users', `user-${id}`);
 
  return await db.users.findUnique({ where: { id } });
}
 
// 사용
const user = await getUser('user-1');

주요 차이점은 다음과 같습니다.

  • 캐시 키를 수동으로 관리할 필요가 없습니다. 함수 인자(id)가 자동으로 캐시 키에 포함됩니다.
  • 래퍼 함수가 아닌 디렉티브이므로 코드가 더 자연스럽습니다.
  • stale, revalidate, expire의 세 값을 독립적으로 제어할 수 있습니다.

핵심 요약

  • "use cache" 디렉티브는 페이지, 컴포넌트, 함수 레벨에서 선언적 캐싱을 제공합니다.
  • cacheLife()로 캐시 수명을 제어하며, 내장 프로필(seconds, minutes, hours 등)과 커스텀 프로필을 사용할 수 있습니다.
  • cacheTag()로 캐시에 태그를 부여하고, revalidateTag()로 세밀하게 무효화합니다.
  • 세 가지 변형("use cache", "use cache: remote", "use cache: private")으로 캐시 저장소와 공유 범위를 제어합니다.
  • unstable_cache를 대체하는 더 선언적이고 자연스러운 캐싱 방법이며, 캐시 키가 자동 관리됩니다.

다음 장에서는 스트리밍 SSR과 로딩 UI 전략을 다룹니다. "use cache"와 Suspense를 결합하여 각 컴포넌트가 독립적으로 스트리밍되는 방법을 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#nextjs#react#typescript#frontend

관련 글

웹 개발

7장: 스트리밍 SSR과 로딩 UI 전략

Next.js의 스트리밍 SSR 동작 원리를 살펴봅니다. loading.tsx, Suspense 경계, 스켈레톤 설계, 프로그레시브 렌더링 전략과 성능 지표 영향을 다룹니다.

2026년 2월 1일·17분
웹 개발

5장: 데이터 페칭과 캐싱 전략의 대전환

Next.js 14에서 16까지 캐싱 전략이 어떻게 변화했는지 살펴봅니다. 네 가지 캐싱 레이어, fetch() 기본값 변경, 재검증 전략을 다룹니다.

2026년 1월 28일·20분
웹 개발

8장: Server Actions와 폼 처리 고급 패턴

Next.js의 Server Actions와 폼 처리를 다룹니다. next/form, useActionState, 보안, revalidation 통합, after() API, 고급 뮤테이션 패턴을 살펴봅니다.

2026년 2월 3일·21분
이전 글5장: 데이터 페칭과 캐싱 전략의 대전환
다음 글7장: 스트리밍 SSR과 로딩 UI 전략

댓글

목차

약 17분 남음
  • 기존 캐싱의 한계
  • "use cache" 디렉티브
    • 세 가지 적용 레벨
    • 기존 방식과의 비교
  • cacheLife: 캐시 수명 프로필
    • 내장 프로필
    • 커스텀 프로필 정의
    • 인라인 수명 지정
  • cacheTag: 세밀한 캐시 무효화
  • 세 가지 캐시 변형
    • "use cache" (기본)
    • "use cache: remote"
    • "use cache: private"
    • 세 변형 비교
  • 실전: 전자상거래 페이지 캐싱 설계
  • unstable_cache에서 마이그레이션
  • 핵심 요약