최근 프로젝트에서 페이지 로딩 속도가 느려서 고민이셨나요? 같은 데이터를 반복해서 불러오는 비효율적인 코드 때문에 서버 부하가 걱정되시나요?
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 파일에서 실험적 기능을 활성화해야 합니다:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
useCache: true,
// 또는 cacheComponents: true 도 가능
},
}
export default nextConfig
2. 세 가지 사용 방법
파일 레벨 캐싱
'use cache'
export default async function Page() {
const data = await fetch('/api/data')
return <div>{/* 페이지 전체가 캐싱됩니다 */}</div>
}
컴포넌트 레벨 캐싱
export async function ProductCard({ productId }: { productId: string }) {
'use cache'
const product = await fetch(`/api/products/${productId}`)
return <div>{/* 이 컴포넌트만 캐싱됩니다 */}</div>
}
함수 레벨 캐싱
export async function getUserData(userId: string) {
'use cache'
const response = await fetch(`/api/users/${userId}`)
return response.json()
}
🔧 실제 사용 예시: 쇼핑몰 프로젝트
실제 쇼핑몰 프로젝트에서 어떻게 활용할 수 있는지 살펴볼까요?
상품 목록 컴포넌트
// 상품 데이터를 캐싱하는 함수
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>
)
}
사용자 프로필 페이지
'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로 캐시 수명 관리
캐시의 유효 기간을 세밀하게 조절할 수 있습니다:
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로 온디맨드 재검증
특정 태그를 기반으로 캐시를 무효화할 수 있습니다:
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. 적절한 캐시 범위 설정
컴포넌트 레벨 캐싱이 더 효율적인 경우:
// ❌ 페이지 전체 캐싱 (비효율적)
'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'의 강력한 기능 중 하나는 자동 의존성 감지입니다:
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. 혼합 캐싱 전략
정적인 부분과 동적인 부분을 적절히 분리하세요:
// 레이아웃은 캐싱
'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 요청을 자동으로 중복 제거합니다:
// 메모이제이션 예시
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 등 |
실제 동작 비교
메모이제이션 동작:
// 첫 번째 요청
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' 동작:
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>
}
언제 무엇을 사용할까?
메모이제이션 사용 시나리오:
// ✅ 같은 페이지 내 여러 컴포넌트에서 동일한 사용자 데이터 필요
function ProfilePage({ userId }: { userId: string }) {
return (
<div>
<UserHeader userId={userId} /> {/* fetch('/api/users/123') */}
<UserDetails userId={userId} /> {/* 메모이제이션으로 중복 제거 */}
<UserActivity userId={userId} /> {/* 메모이제이션으로 중복 제거 */}
</div>
)
}
'use cache' 사용 시나리오:
// ✅ 자주 접근하지만 변경이 적은 데이터
async function getProductCategories() {
'use cache'
cacheLife('hours') // 1시간 캐싱
return fetch('/api/categories') // 카테고리는 자주 변하지 않음
}
// ✅ 비용이 큰 계산 작업
async function calculateComplexReport(params: ReportParams) {
'use cache'
cacheLife('minutes')
// 복잡한 데이터 처리 로직
return processHeavyData(params)
}
조합 사용: 더 강력한 성능 최적화
두 기능을 함께 사용하면 이중 최적화 효과를 얻을 수 있습니다:
// 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 함수 사용):
import { cache } from 'react'
// 렌더링 사이클 내에서만 중복 제거
const getUserById = cache(async (id: string) => {
return db.select().from(users).where(eq(users.id, id))
})
'use cache' (Next.js 디렉티브):
// 설정한 시간 동안 지속적으로 캐싱
async function getUserById(id: string) {
'use cache'
cacheLife('minutes')
return db.select().from(users).where(eq(users.id, id))
}
주의사항: 함께 사용할 때
// ❌ 잘못된 예: 이중 캐싱으로 예측하기 어려운 동작
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 이전:
// 복잡한 캐시 설정
export default async function Page() {
const data = await fetch('/api/data', {
cache: 'force-cache',
next: { revalidate: 3600 }
})
return <div>{/* ... */}</div>
}
Next.js 15 이후:
// 간단한 디렉티브
export default async function Page() {
'use cache'
const data = await fetch('/api/data') // 자동으로 캐싱됨
return <div>{/* ... */}</div>
}
Suspense와의 관계
Next.js 15에서는 명확한 캐싱 정책이 필요합니다:
// 캐싱하지 않는 경우 - Suspense 필수
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<FreshDataComponent />
</Suspense>
)
}
// 캐싱하는 경우 - Suspense 불필요
'use cache'
export default function Page() {
return <CachedDataComponent />
}
💡 실무 활용 시나리오
1. 전자상거래 사이트
상품 카탈로그:
async function getProductCatalog(category: string, page: number) {
'use cache'
cacheLife('hours') // 1시간 캐싱
return await fetch(`/api/products?category=${category}&page=${page}`)
}
사용자 장바구니 (캐싱 안 함):
function ShoppingCart() {
return (
<Suspense fallback={<CartSkeleton />}>
<DynamicCartContent />
</Suspense>
)
}
2. 블로그/뉴스 사이트
블로그 포스트:
'use cache'
export default async function BlogPost({ slug }: { slug: string }) {
cacheLife('days') // 하루 캐싱
const post = await getBlogPost(slug)
return <Article post={post} />
}
최신 뉴스 (실시간):
function LatestNews() {
return (
<Suspense fallback={<NewsSkeleton />}>
<RealtimeNews />
</Suspense>
)
}
3. 대시보드 애플리케이션
통계 데이터:
async function getDashboardStats(userId: string, dateRange: string) {
'use cache'
cacheLife('minutes') // 5분 캐싱
return await analytics.getStats(userId, dateRange)
}
🚨 주의사항과 베스트 프랙티스
1. 실험적 기능임을 인지
'use cache'는 아직 실험적 기능입니다. 프로덕션 환경에서 사용할 때는:
- 충분한 테스트 수행
- 모니터링 시스템 구축
- 롤백 계획 준비
2. 적절한 캐시 수명 설정
// ❌ 잘못된 예: 실시간 데이터를 너무 오래 캐싱
async function getRealTimeStock() {
'use cache'
cacheLife('hours') // 주식 데이터를 1시간 캐싱? 위험!
}
// ✅ 올바른 예: 실시간 데이터는 짧게 캐싱
async function getRealTimeStock() {
'use cache'
cacheLife('seconds') // 실시간 데이터는 초 단위로
}
3. 메모리 사용량 고려
대량의 데이터를 캐싱할 때는 메모리 사용량을 주의깊게 모니터링하세요:
// 큰 데이터셋은 적절한 캐시 정책 설정
async function getBigDataset() {
'use cache'
cacheLife('hours') // 너무 오래 캐싱하지 않기
const data = await fetch('/api/big-dataset')
return data.json()
}
4. 시리얼라이제이션 이해
캐시할 수 있는 값과 없는 값을 구분하세요:
function MyComponent({ children }: { children: ReactNode }) {
'use cache'
// children은 시리얼라이제이션되지 않으므로 캐시 키에 포함되지 않음
return <div>{children}</div>
}
🎉 마무리: 다음 단계로 나아가기
Next.js의 'use cache' 디렉티브는 정말 혁신적인 기능이에요! 복잡했던 캐싱 로직을 한 줄로 해결할 수 있다니, 정말 놀랍지 않나요?
이 글에서 배운 핵심 내용들:
- 'use cache' 디렉티브로 간단한 캐싱 구현
- cacheLife로 세밀한 캐시 수명 관리
- cacheTag로 스마트한 캐시 무효화
- 실무 시나리오별 최적 활용법
이제 여러분의 프로젝트에도 적용해보세요! 처음에는 작은 컴포넌트부터 시작해서 점진적으로 확장해나가는 것을 추천드려요.
혹시 구현 과정에서 궁금한 점이 있으시거나, 실제 적용 후기가 있으시다면 댓글로 공유해주세요! 함께 나누면 더 좋은 개발 경험을 만들어갈 수 있을 거예요.
'기술의기록' 카테고리의 다른 글
개발자를 위한 AI 코딩 파트너, Serena MCP 완벽 가이드 (2025년 기준) (3) | 2025.08.22 |
---|---|
Next.js 15.5.0 드디어 공개! 🚀 개발자들이 기다린 핵심 업데이트 완벽 분석 (2) | 2025.08.20 |
프론트엔드 개발자라면 꼭 알아야 할 기능 설계 완벽 가이드 (2) | 2025.08.19 |
개발자가 꼭 알아야 할 Unicode와 텍스트 인코딩의 모든 것 (6) | 2025.08.18 |
React의 Fine Grained Reactivity가 가져올 혁신 - 더 빠르고 효율적인 상태 관리의 미래 (1) | 2025.08.18 |