본문 바로가기
기술의기록

Next.js Cache Components 완벽 가이드: 'use cache' 디렉티브로 성능 혁신하기 (2025년 최신 기준)

by Jeremy Winchester 2025. 8. 20.
반응형

최근 프로젝트에서 페이지 로딩 속도가 느려서 고민이셨나요? 같은 데이터를 반복해서 불러오는 비효율적인 코드 때문에 서버 부하가 걱정되시나요?

Next.js 15에서 도입된 'use cache' 디렉티브는 이런 고민들을 한 번에 해결해줄 수 있는 혁신적인 기능입니다. 마치 마법처럼 간단한 한 줄의 코드로 컴포넌트와 함수의 캐싱을 자동화할 수 있어요. 오늘 이 글을 통해 Next.js의 Cache Components 기능을 완전히 마스터해보세요! ✨

🎯 Next.js Cache Components란 무엇인가?

Cache Components는 Next.js에서 제공하는 컴포넌트 단위 캐싱 시스템입니다. 기존의 복잡한 캐싱 로직을 대신해서, 단순한 디렉티브 하나로 강력한 캐싱 기능을 구현할 수 있어요.

'use cache' 디렉티브의 특징

'use cache'는 JavaScript 디렉티브로, 'use client'나 'use server'와 비슷한 방식으로 작동합니다. 하지만 중요한 차이점이 있어요:

  • 'use client', 'use server': React의 네이티브 기능
  • 'use cache': Next.js 전용 실험적 기능

이 디렉티브는 Next.js 컴파일러에게 "이 함수나 컴포넌트를 캐싱하라"고 지시하는 역할을 합니다.

⚙️ 설정 방법: 실전 적용하기

1. next.config.ts 설정

먼저 next.config.ts 파일에서 실험적 기능을 활성화해야 합니다:

 
 
typescript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    useCache: true,
    // 또는 cacheComponents: true 도 가능
  },
}

export default nextConfig

2. 세 가지 사용 방법

파일 레벨 캐싱

 
 
tsx
'use cache'

export default async function Page() {
  const data = await fetch('/api/data')
  return <div>{/* 페이지 전체가 캐싱됩니다 */}</div>
}

컴포넌트 레벨 캐싱

 
 
tsx
export async function ProductCard({ productId }: { productId: string }) {
  'use cache'
  
  const product = await fetch(`/api/products/${productId}`)
  return <div>{/* 이 컴포넌트만 캐싱됩니다 */}</div>
}

함수 레벨 캐싱

 
 
tsx
export async function getUserData(userId: string) {
  'use cache'
  
  const response = await fetch(`/api/users/${userId}`)
  return response.json()
}

🔧 실제 사용 예시: 쇼핑몰 프로젝트

실제 쇼핑몰 프로젝트에서 어떻게 활용할 수 있는지 살펴볼까요?

상품 목록 컴포넌트

 
 
tsx
// 상품 데이터를 캐싱하는 함수
async function getProducts(category: string) {
  'use cache'
  
  const response = await fetch(`/api/products?category=${category}`)
  return response.json()
}

// 상품 목록 컴포넌트
export default async function ProductList({ category }: { category: string }) {
  const products = await getProducts(category)
  
  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

사용자 프로필 페이지

 
 
tsx
'use cache'

export default async function ProfilePage({ params }: { params: { id: string } }) {
  const user = await getUserById(params.id)
  const orders = await getUserOrders(params.id)
  
  return (
    <div>
      <h1>{user.name}님의 프로필</h1>
      <UserInfo user={user} />
      <OrderHistory orders={orders} />
    </div>
  )
}

🎨 고급 기능: cacheLife와 cacheTag

cacheLife로 캐시 수명 관리

캐시의 유효 기간을 세밀하게 조절할 수 있습니다:

 
 
tsx
import { unstable_cacheLife as cacheLife } from 'next/cache'

export async function getHotDeals() {
  'use cache'
  cacheLife('minutes') // 분 단위로 캐시
  
  const deals = await fetch('/api/hot-deals')
  return deals.json()
}

export async function getStaticContent() {
  'use cache'
  cacheLife('days') // 일 단위로 캐시
  
  const content = await fetch('/api/static-content')
  return content.json()
}

사용 가능한 캐시 프로필

  • 'seconds': 초 단위 캐싱
  • 'minutes': 분 단위 캐싱
  • 'hours': 시간 단위 캐싱
  • 'days': 일 단위 캐싱
  • 'weeks': 주 단위 캐싱
  • 'max': 최대 캐싱 (거의 무제한)

cacheTag로 온디맨드 재검증

특정 태그를 기반으로 캐시를 무효화할 수 있습니다:

 
 
tsx
import { revalidateTag } from 'next/cache'

// 캐시에 태그 설정
export async function getProductsByCategory(category: string) {
  'use cache'
  
  const data = await fetch(`/api/products?category=${category}`, {
    next: { tags: ['products', `category-${category}`] }
  })
  return data.json()
}

// 특정 태그의 캐시 무효화
export async function updateProduct(productId: string) {
  await fetch(`/api/products/${productId}`, { method: 'PUT' })
  
  // 상품 관련 캐시 모두 무효화
  revalidateTag('products')
}

⚡ 성능 최적화 팁

1. 적절한 캐시 범위 설정

컴포넌트 레벨 캐싱이 더 효율적인 경우:

 
 
tsx
// ❌ 페이지 전체 캐싱 (비효율적)
'use cache'
export default function Page() {
  return (
    <div>
      <Header /> {/* 자주 변경되는 부분 */}
      <ProductList /> {/* 캐싱하고 싶은 부분 */}
    </div>
  )
}

// ✅ 필요한 부분만 캐싱 (효율적)
export default function Page() {
  return (
    <div>
      <Header />
      <CachedProductList /> {/* 이 컴포넌트만 캐싱 */}
    </div>
  )
}

function CachedProductList() {
  'use cache'
  // 상품 목록 로직
}

2. 클로저(Closure) 활용

'use cache'의 강력한 기능 중 하나는 자동 의존성 감지입니다:

 
 
tsx
function UserProfile({ userId }: { userId: string }) {
  async function getUserNotifications(page: number, limit: number) {
    'use cache'
    
    // userId는 자동으로 캐시 키에 포함됩니다
    return await db
      .select()
      .from(notifications)
      .where(eq(notifications.userId, userId))
      .limit(limit)
      .offset(page * limit)
  }
  
  return <NotificationList getData={getUserNotifications} />
}

3. 혼합 캐싱 전략

정적인 부분과 동적인 부분을 적절히 분리하세요:

 
 
tsx
// 레이아웃은 캐싱
'use cache'
export default function Layout({ children }: { children: ReactNode }) {
  return (
    <html>
      <body>
        <StaticHeader />
        <StaticNavigation />
        {children} {/* 동적 컨텐츠 */}
        <StaticFooter />
      </body>
    </html>
  )
}

// 각 페이지는 개별 캐싱 정책 적용
function ProductPage() {
  return (
    <Suspense fallback={<Loading />}>
      <DynamicProductData /> {/* 실시간 데이터 */}
    </Suspense>
  )
}

🤔 메모이제이션 vs 'use cache': 무엇이 다를까?

Next.js에는 여러 캐싱 메커니즘이 있어서 처음에는 혼란스러울 수 있어요. 특히 **메모이제이션(Request Memoization)**과 **'use cache'**의 차이점을 정확히 알아야 올바르게 사용할 수 있습니다.

메모이제이션(Request Memoization)이란?

메모이제이션은 React의 기본 기능으로, 같은 렌더링 사이클 내에서 동일한 fetch 요청을 자동으로 중복 제거합니다:

 
 
tsx
// 메모이제이션 예시
async function UserProfile({ userId }: { userId: string }) {
  const user = await fetch(`/api/users/${userId}`) // 첫 번째 호출
  return <UserCard user={user} />
}

async function UserPosts({ userId }: { userId: string }) {
  const user = await fetch(`/api/users/${userId}`) // 같은 요청이지만 중복 제거됨
  return <PostList user={user} />
}

export default function Page({ userId }: { userId: string }) {
  return (
    <div>
      <UserProfile userId={userId} />
      <UserPosts userId={userId} /> {/* 실제로는 API 호출 1번만 발생 */}
    </div>
  )
}

핵심 차이점 비교표

구분메모이제이션'use cache'

범위 단일 렌더링 사이클 내 여러 요청에 걸쳐 지속
지속시간 렌더링 완료시 소멸 설정한 캐시 수명까지
설정 필요 자동 (fetch만 사용) 명시적 디렉티브 필요
적용 대상 fetch 요청만 모든 함수/컴포넌트
저장 위치 메모리 (임시) 서버 캐시
제어 방법 자동 관리 cacheLife, cacheTag 등

실제 동작 비교

메모이제이션 동작:

 
 
tsx
// 첫 번째 요청
function ComponentA() {
  const data = fetch('/api/data') // ✅ 실제 API 호출
  return <div>{data}</div>
}

// 같은 렌더링 사이클 내의 두 번째 요청
function ComponentB() {
  const data = fetch('/api/data') // ✅ 메모이제이션으로 즉시 반환
  return <div>{data}</div>
}

// 새로운 페이지 요청 (새로운 렌더링 사이클)
function ComponentC() {
  const data = fetch('/api/data') // ✅ 다시 실제 API 호출
  return <div>{data}</div>
}

'use cache' 동작:

 
 
tsx
async function getCachedData() {
  'use cache'
  cacheLife('minutes') // 5분간 캐싱
  
  return fetch('/api/data')
}

// 첫 번째 요청
function ComponentA() {
  const data = getCachedData() // ✅ 실제 API 호출 + 캐시 저장
  return <div>{data}</div>
}

// 5분 내 다른 페이지에서의 요청
function ComponentB() {
  const data = getCachedData() // ✅ 캐시에서 즉시 반환 (API 호출 없음)
  return <div>{data}</div>
}

언제 무엇을 사용할까?

메모이제이션 사용 시나리오:

 
 
tsx
// ✅ 같은 페이지 내 여러 컴포넌트에서 동일한 사용자 데이터 필요
function ProfilePage({ userId }: { userId: string }) {
  return (
    <div>
      <UserHeader userId={userId} />   {/* fetch('/api/users/123') */}
      <UserDetails userId={userId} />  {/* 메모이제이션으로 중복 제거 */}
      <UserActivity userId={userId} /> {/* 메모이제이션으로 중복 제거 */}
    </div>
  )
}

'use cache' 사용 시나리오:

 
 
tsx
// ✅ 자주 접근하지만 변경이 적은 데이터
async function getProductCategories() {
  'use cache'
  cacheLife('hours') // 1시간 캐싱
  
  return fetch('/api/categories') // 카테고리는 자주 변하지 않음
}

// ✅ 비용이 큰 계산 작업
async function calculateComplexReport(params: ReportParams) {
  'use cache'
  cacheLife('minutes')
  
  // 복잡한 데이터 처리 로직
  return processHeavyData(params)
}

조합 사용: 더 강력한 성능 최적화

두 기능을 함께 사용하면 이중 최적화 효과를 얻을 수 있습니다:

 
 
tsx
// 1차: 'use cache'로 장기 캐싱
async function getExpensiveData(id: string) {
  'use cache'
  cacheLife('hours')
  
  return fetch(`/api/expensive-operation/${id}`)
}

// 2차: 메모이제이션으로 렌더링 사이클 내 중복 제거
function OptimizedPage({ dataId }: { dataId: string }) {
  return (
    <div>
      <HeaderComponent dataId={dataId} />  {/* getExpensiveData 호출 */}
      <ContentComponent dataId={dataId} /> {/* 메모이제이션으로 중복 제거 */}
      <FooterComponent dataId={dataId} />  {/* 메모이제이션으로 중복 제거 */}
    </div>
  )
}

데이터베이스 쿼리에서의 차이

메모이제이션 (React cache 함수 사용):

 
 
tsx
import { cache } from 'react'

// 렌더링 사이클 내에서만 중복 제거
const getUserById = cache(async (id: string) => {
  return db.select().from(users).where(eq(users.id, id))
})

'use cache' (Next.js 디렉티브):

 
 
tsx
// 설정한 시간 동안 지속적으로 캐싱
async function getUserById(id: string) {
  'use cache'
  cacheLife('minutes')
  
  return db.select().from(users).where(eq(users.id, id))
}

주의사항: 함께 사용할 때

 
 
tsx
// ❌ 잘못된 예: 이중 캐싱으로 예측하기 어려운 동작
const memoizedFetch = cache(async (url: string) => {
  'use cache' // 이렇게 중첩하면 안됨
  return fetch(url)
})

// ✅ 올바른 예: 역할 분리
async function getCachedData(id: string) {
  'use cache' // 장기 캐싱
  cacheLife('hours')
  
  return fetch(`/api/data/${id}`)
}

const memoizedGetData = cache(getCachedData) // 렌더링 내 중복 제거

🔍 Next.js 15의 캐싱 변화

기존 방식 vs 새로운 방식

Next.js 14 이전:

 
 
tsx
// 복잡한 캐시 설정
export default async function Page() {
  const data = await fetch('/api/data', {
    cache: 'force-cache',
    next: { revalidate: 3600 }
  })
  
  return <div>{/* ... */}</div>
}

Next.js 15 이후:

 
 
tsx
// 간단한 디렉티브
export default async function Page() {
  'use cache'
  
  const data = await fetch('/api/data') // 자동으로 캐싱됨
  return <div>{/* ... */}</div>
}

Suspense와의 관계

Next.js 15에서는 명확한 캐싱 정책이 필요합니다:

 
 
tsx
// 캐싱하지 않는 경우 - Suspense 필수
export default function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <FreshDataComponent />
    </Suspense>
  )
}

// 캐싱하는 경우 - Suspense 불필요
'use cache'
export default function Page() {
  return <CachedDataComponent />
}

💡 실무 활용 시나리오

1. 전자상거래 사이트

상품 카탈로그:

 
 
tsx
async function getProductCatalog(category: string, page: number) {
  'use cache'
  cacheLife('hours') // 1시간 캐싱
  
  return await fetch(`/api/products?category=${category}&page=${page}`)
}

사용자 장바구니 (캐싱 안 함):

 
 
tsx
function ShoppingCart() {
  return (
    <Suspense fallback={<CartSkeleton />}>
      <DynamicCartContent />
    </Suspense>
  )
}

2. 블로그/뉴스 사이트

블로그 포스트:

 
 
tsx
'use cache'
export default async function BlogPost({ slug }: { slug: string }) {
  cacheLife('days') // 하루 캐싱
  
  const post = await getBlogPost(slug)
  return <Article post={post} />
}

최신 뉴스 (실시간):

 
 
tsx
function LatestNews() {
  return (
    <Suspense fallback={<NewsSkeleton />}>
      <RealtimeNews />
    </Suspense>
  )
}

3. 대시보드 애플리케이션

통계 데이터:

 
 
tsx
async function getDashboardStats(userId: string, dateRange: string) {
  'use cache'
  cacheLife('minutes') // 5분 캐싱
  
  return await analytics.getStats(userId, dateRange)
}

🚨 주의사항과 베스트 프랙티스

1. 실험적 기능임을 인지

'use cache'는 아직 실험적 기능입니다. 프로덕션 환경에서 사용할 때는:

  • 충분한 테스트 수행
  • 모니터링 시스템 구축
  • 롤백 계획 준비

2. 적절한 캐시 수명 설정

 
 
tsx
// ❌ 잘못된 예: 실시간 데이터를 너무 오래 캐싱
async function getRealTimeStock() {
  'use cache'
  cacheLife('hours') // 주식 데이터를 1시간 캐싱? 위험!
}

// ✅ 올바른 예: 실시간 데이터는 짧게 캐싱
async function getRealTimeStock() {
  'use cache'
  cacheLife('seconds') // 실시간 데이터는 초 단위로
}

3. 메모리 사용량 고려

대량의 데이터를 캐싱할 때는 메모리 사용량을 주의깊게 모니터링하세요:

 
 
tsx
// 큰 데이터셋은 적절한 캐시 정책 설정
async function getBigDataset() {
  'use cache'
  cacheLife('hours') // 너무 오래 캐싱하지 않기
  
  const data = await fetch('/api/big-dataset')
  return data.json()
}

4. 시리얼라이제이션 이해

캐시할 수 있는 값과 없는 값을 구분하세요:

 
 
tsx
function MyComponent({ children }: { children: ReactNode }) {
  'use cache'
  
  // children은 시리얼라이제이션되지 않으므로 캐시 키에 포함되지 않음
  return <div>{children}</div>
}

🎉 마무리: 다음 단계로 나아가기

Next.js의 'use cache' 디렉티브는 정말 혁신적인 기능이에요! 복잡했던 캐싱 로직을 한 줄로 해결할 수 있다니, 정말 놀랍지 않나요?

이 글에서 배운 핵심 내용들:

  • 'use cache' 디렉티브로 간단한 캐싱 구현
  • cacheLife로 세밀한 캐시 수명 관리
  • cacheTag로 스마트한 캐시 무효화
  • 실무 시나리오별 최적 활용법

이제 여러분의 프로젝트에도 적용해보세요! 처음에는 작은 컴포넌트부터 시작해서 점진적으로 확장해나가는 것을 추천드려요.

혹시 구현 과정에서 궁금한 점이 있으시거나, 실제 적용 후기가 있으시다면 댓글로 공유해주세요! 함께 나누면 더 좋은 개발 경험을 만들어갈 수 있을 거예요.

반응형