Next.js 16 Cache Components - Vercel이 공개한 PPR·use cache 가이드 (전문 번역)
· nextjs, react, vercel, best-practices
목차
참고: 이 문서는 Vercel Labs의 next-cache-components 스킬 전문을 번역한 것입니다. 이전에 번역한 Next.js Best Practices가 Next.js 전반의 정확성 규칙을 다뤘다면, 이 문서는 Next.js 16에서 도입된 Cache Components 한 가지 주제 — 정적·캐시·동적 콘텐츠를 한 라우트에 섞는 Partial Prerendering(PPR)과
use cache지시어 — 를 집중적으로 다룹니다.
Cache Components는 Partial Prerendering(PPR)을 가능하게 합니다 — 정적, 캐시된, 동적 콘텐츠를 하나의 라우트에 섞을 수 있어요.
Cache Components 활성화 (Enable Cache Components)
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
이 설정이 기존의 experimental.ppr 플래그를 대체합니다.
세 가지 콘텐츠 유형 (Three Content Types)
Cache Components를 활성화하면 콘텐츠는 세 가지 범주로 나뉩니다:
1. 정적 (Static) - 자동 프리렌더링
동기 코드, import, 순수 연산 — 빌드 타임에 프리렌더링됩니다:
export default function Page() {
return (
<header>
<h1>Our Blog</h1> {/* 정적 - 즉시 표시 */}
<nav>...</nav>
</header>
)
}
2. 캐시됨 (use cache)
매 요청마다 새로 가져올 필요가 없는 비동기 데이터:
async function BlogPosts() {
'use cache'
cacheLife('hours')
const posts = await db.posts.findMany()
return <PostList posts={posts} />
}
3. 동적 (Suspense)
항상 최신이어야 하는 런타임 데이터 — Suspense로 감쌉니다:
import { Suspense } from 'react'
export default function Page() {
return (
<>
<BlogPosts /> {/* 캐시됨 */}
<Suspense fallback={<p>Loading...</p>}>
<UserPreferences /> {/* 동적 - 스트리밍으로 도착 */}
</Suspense>
</>
)
}
async function UserPreferences() {
const theme = (await cookies()).get('theme')?.value
return <p>Theme: {theme}</p>
}
use cache 지시어 (use cache Directive)
파일 레벨
'use cache'
export default async function Page() {
// 페이지 전체가 캐시됨
const data = await fetchData()
return <div>{data}</div>
}
컴포넌트 레벨
export async function CachedComponent() {
'use cache'
const data = await fetchData()
return <div>{data}</div>
}
함수 레벨
export async function getData() {
'use cache'
return db.query('SELECT * FROM posts')
}
캐시 프로필 (Cache Profiles)
내장 프로필
'use cache' // 기본값: stale 5분, revalidate 15분
'use cache: remote' // 플랫폼이 제공하는 캐시 (Redis, KV)
'use cache: private' // 컴플라이언스용, 런타임 API 허용
cacheLife() - 커스텀 수명
import { cacheLife } from 'next/cache'
async function getData() {
'use cache'
cacheLife('hours') // 내장 프로필
return fetch('/api/data')
}
내장 프로필: 'default', 'minutes', 'hours', 'days', 'weeks', 'max'
인라인 설정
async function getData() {
'use cache'
cacheLife({
stale: 3600, // 1시간 - 재검증하는 동안 stale 응답 제공
revalidate: 7200, // 2시간 - 백그라운드 재검증 주기
expire: 86400, // 1일 - 완전 만료
})
return fetch('/api/data')
}
캐시 무효화 (Cache Invalidation)
cacheTag() - 캐시된 콘텐츠에 태그 달기
import { cacheTag } from 'next/cache'
async function getProducts() {
'use cache'
cacheTag('products')
return db.products.findMany()
}
async function getProduct(id: string) {
'use cache'
cacheTag('products', `product-${id}`)
return db.products.findUnique({ where: { id } })
}
updateTag() - 즉시 무효화
같은 요청 안에서 캐시가 갱신되어야 할 때 사용합니다:
'use server'
import { updateTag } from 'next/cache'
export async function updateProduct(id: string, data: FormData) {
await db.products.update({ where: { id }, data })
updateTag(`product-${id}`) // 즉시 - 같은 요청에서 최신 데이터를 봄
}
revalidateTag() - 백그라운드 재검증
stale-while-revalidate 동작이 필요할 때 사용합니다:
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(data: FormData) {
await db.posts.create({ data })
revalidateTag('posts') // 백그라운드 - 다음 요청에서 최신 데이터를 봄
}
런타임 데이터 제약 (Runtime Data Constraint)
use cache 안에서는 cookies(), headers(), searchParams에 접근할 수 없습니다.
해결책: 인자로 전달하기
// 잘못된 예 - use cache 안에서 런타임 API 사용
async function CachedProfile() {
'use cache'
const session = (await cookies()).get('session')?.value // 에러!
return <div>{session}</div>
}
// 올바른 예 - 바깥에서 추출해 인자로 전달
async function ProfilePage() {
const session = (await cookies()).get('session')?.value
return <CachedProfile sessionId={session} />
}
async function CachedProfile({ sessionId }: { sessionId: string }) {
'use cache'
// sessionId는 자동으로 캐시 키의 일부가 됨
const data = await fetchUserData(sessionId)
return <div>{data.name}</div>
}
예외: use cache: private
리팩터링이 불가능한 컴플라이언스 요구사항이 있을 때:
async function getData() {
'use cache: private'
const session = (await cookies()).get('session')?.value // 허용됨
return fetchData(session)
}
캐시 키 생성 (Cache Key Generation)
캐시 키는 다음 요소를 기반으로 자동 생성됩니다:
- 빌드 ID - 배포할 때마다 모든 캐시를 무효화
- 함수 ID - 함수 위치의 해시
- 직렬화 가능한 인자 - props가 키의 일부가 됨
- 클로저 변수 - 바깥 스코프의 값도 포함됨
async function Component({ userId }: { userId: string }) {
const getData = async (filter: string) => {
'use cache'
// 캐시 키 = userId (클로저) + filter (인자)
return fetch(`/api/users/${userId}?filter=${filter}`)
}
return getData('active')
}
전체 예제 (Complete Example)
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'
export default function DashboardPage() {
return (
<>
{/* 정적 셸 - CDN에서 즉시 제공 */}
<header><h1>Dashboard</h1></header>
<nav>...</nav>
{/* 캐시됨 - 빠르고, 매시간 재검증 */}
<Stats />
{/* 동적 - 최신 데이터를 스트리밍으로 */}
<Suspense fallback={<NotificationsSkeleton />}>
<Notifications />
</Suspense>
</>
)
}
async function Stats() {
'use cache'
cacheLife('hours')
cacheTag('dashboard-stats')
const stats = await db.stats.aggregate()
return <StatsDisplay stats={stats} />
}
async function Notifications() {
const userId = (await cookies()).get('userId')?.value
const notifications = await db.notifications.findMany({
where: { userId, read: false }
})
return <NotificationList items={notifications} />
}
이전 버전에서 마이그레이션 (Migration from Previous Versions)
| 기존 설정 | 대체 |
|---|---|
experimental.ppr | cacheComponents: true |
dynamic = 'force-dynamic' | 제거 (기본 동작) |
dynamic = 'force-static' | 'use cache' + cacheLife('max') |
revalidate = N | cacheLife({ revalidate: N }) |
unstable_cache() | 'use cache' 지시어 |
unstable_cache를 use cache로 마이그레이션
Next.js 16에서 unstable_cache는 use cache 지시어로 대체되었습니다. cacheComponents가 활성화되어 있다면 unstable_cache 호출을 use cache 함수로 변환하세요:
변경 전 (unstable_cache):
import { unstable_cache } from 'next/cache'
const getCachedUser = unstable_cache(
async (id) => getUser(id),
['my-app-user'],
{
tags: ['users'],
revalidate: 60,
}
)
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const user = await getCachedUser(id)
return <div>{user.name}</div>
}
변경 후 (use cache):
import { cacheLife, cacheTag } from 'next/cache'
async function getCachedUser(id: string) {
'use cache'
cacheTag('users')
cacheLife({ revalidate: 60 })
return getUser(id)
}
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const user = await getCachedUser(id)
return <div>{user.name}</div>
}
핵심 차이점:
- 수동 캐시 키 불필요 -
use cache는 함수 인자와 클로저에서 키를 자동 생성합니다.unstable_cache의keyParts배열은 더 이상 필요 없습니다. - 태그 -
options.tags를 함수 안의cacheTag()호출로 대체하세요. - 재검증 -
options.revalidate를cacheLife({ revalidate: N })또는cacheLife('minutes')같은 내장 프로필로 대체하세요. - 동적 데이터 -
unstable_cache는 콜백 안에서cookies()나headers()를 지원하지 않았습니다. 같은 제약이use cache에도 적용되지만, 필요하다면'use cache: private'을 쓸 수 있습니다.
제한 사항 (Limitations)
- Edge 런타임 미지원 - Node.js가 필요합니다
- 정적 export 미지원 - 서버가 필요합니다
- 비결정적 값 (
Math.random(),Date.now())은use cache안에서 빌드 타임에 한 번만 실행됩니다
캐시 바깥에서 요청 시점의 랜덤 값이 필요하다면:
import { connection } from 'next/server'
async function DynamicContent() {
await connection() // 요청 시점으로 지연
const id = crypto.randomUUID() // 요청마다 다른 값
return <div>{id}</div>
}
참고 자료 (Sources)
관련 글
- Next.js Best Practices - Vercel이 공개한 Next.js 개발 가이드 (전문 번역)Vercel이 공개한 Next.js 베스트 프랙티스 스킬 전문 번역입니다. 파일 컨벤션, RSC 경계, 데이터 패턴, 에러 처리, 이미지·폰트 최적화 등 19개 문서를 한글로 번역했어요.
- React Native Skills - Vercel이 공개한 React Native·Expo 베스트 프랙티스 (전문 번역)Vercel Labs가 공개한 React Native Skills 전문 번역입니다. 리스트 가상화, Reanimated 애니메이션, 네이티브 내비게이션, 상태 관리 등 14개 카테고리 36개 규칙을 한글로 옮겼어요.
- React Best Practices - Vercel 엔지니어링 팀의 성능 최적화 가이드 (전문 번역)Vercel 엔지니어링 팀이 공개한 React/Next.js 성능 최적화 가이드 전문 번역입니다. 8개 카테고리, 45개 이상의 규칙을 영향도 기준으로 정리했습니다.