Next.js Cache Components 정리
Next.js Docs – Getting Started: Cache Components 내용을 바탕으로, App Router 기준의 Cache Components 개념을 정리한 자료입니다.
1. 개요
- Cache Components는 옵트인 기능입니다.
next.config.(js|ts)의cacheComponents옵션을true로 설정해야 동작합니다.
- 하나의 라우트에서 정적(static)·캐시된(cached)·동적(dynamic) 콘텐츠를 섞어서 사용할 수 있도록 하는 기능입니다.
- 전통적인 서버 렌더링 앱의 문제:
- 정적 페이지: 빠르지만, 데이터가 금방 낡음(stale).
- 동적 페이지: 항상 최신이지만, 느리고 서버 부담 큼.
- 이 일을 클라이언트로 넘기면, 번들이 커지고 최초 렌더가 느려짐.
- Cache Components는 라우트를 “정적 HTML 쉘(static HTML shell)”로 미리 렌더링한 후, 동적 부분은 준비되는 대로 UI를 업데이트하는 방식으로 이 문제를 해결합니다.
이 렌더링 방식은 **Partial Prerendering (PPR)**이라고 부르며, Cache Components를 활성화하면 기본 동작이 됩니다.
2. Cache Components에서 렌더링이 동작하는 방식
2.1 Prerendering과 Static Shell
- 빌드 시점에 Next.js가 라우트의 컴포넌트 트리를 렌더링합니다.
- 다음과 같은 작업만 사용하는 컴포넌트는 자동으로 static shell에 포함됩니다.
- 동기 I/O (예: 동기 파일 읽기)
- 모듈 import
- 순수 계산 (pure computations)
- 반대로, 다음과 같은 경우는 자동으로 처리되지 않으므로 직접 처리 방법을 선택해야 합니다.
- 네트워크 요청 (
fetch) - 일부 시스템 API 사용
- 요청(request) 객체가 필요한 경우 등
- 네트워크 요청 (
이때 선택지는 두 가지입니다.
<Suspense>로 감싸서 요청 시점(request time)에 렌더링을 미루기use cache지시문을 사용해 결과를 캐시하고 static shell에 포함시키기 (요청 데이터가 필요 없는 경우)
-
이 작업은 요청이 오기 전에 미리 수행되므로 prerendering이라고 부릅니다.
-
결과로 만들어지는 것은:
- 최초 진입용 HTML static shell
- 클라이언트 측 내비게이션에 사용되는 RSC Payload → 사용자가 URL로 직접 들어오든, 페이지 간 이동을 하든 곧바로 렌더된 콘텐츠를 받도록 보장합니다.
-
만약 prerendering 과정에서 처리할 수 없는 작업을
<Suspense>로 감싸지 않거나use cache로 표시하지 않으면
개발·빌드 시점에Uncached data was accessed outside of <Suspense>에러가 발생합니다.
3. 자동으로 프리렌더되는 콘텐츠 (Automatically prerendered content)
다음처럼 동기 I/O + 모듈 import + 순수 계산만 사용하는 컴포넌트는 자동으로 static shell에 포함됩니다.
// page.tsx
import fs from 'node:fs'
export default async function Page() {
// 동기 파일 읽기
const content = fs.readFileSync('./config.json', 'utf-8')
// 모듈 import
const constants = await import('./constants.json')
// 순수 계산
const processed = JSON.parse(content).items.map((item) => item.value * 2)
return (
<div>
<h1>{constants.appName}</h1>
<ul>
{processed.map((value, i) => (
<li key={i}>{value}</li>
))}
</ul>
</div>
)
}
- 레이아웃과 페이지 둘 다 정상적으로 prerender되면, 해당 라우트 전체가 static shell이 됩니다.
- 빌드 로그 요약이나 브라우저의 페이지 소스보기로, 어떤 내용이 static shell에 포함됐는지 확인할 수 있습니다.
4. 요청 시점으로 렌더링 미루기 (Defer rendering to request time)
빌드 시점에 완료할 수 없는 작업(네트워크 요청, 런타임 데이터 등)은 명시적으로 처리해야 합니다.
- 부모 컴포넌트가
<Suspense>boundary를 제공해야 합니다.- fallback UI는 static shell에 포함되고,
- 실제 콘텐츠는 요청 시점에 스트리밍됩니다.
- Suspense boundary는 가능한 해당 컴포넌트에 가깝게 배치하는 것이 좋습니다.
→ static shell에 최대한 많은 내용을 포함시키기 위해서입니다.
<Suspense>를 통해 여러 동적 영역을 병렬로 렌더링할 수 있어 전체 로딩 시간이 줄어듭니다.
4.1 Dynamic content
- 외부 시스템에서 가져오는 데이터는
- 응답 시간이 예측 불가능하고,
- 실패할 수도 있습니다.
- 따라서 prerendering에서는 자동으로 실행되지 않습니다.
최신 데이터(실시간 피드, 개인화된 콘텐츠 등)가 필요하다면, <Suspense>를 사용하여 fallback UI를 static shell에 포함시키고, 실제 콘텐츠는 요청 시점에 스트리밍하는 방식으로 처리합니다.
// DynamicContent: 자동 프리렌더 대상이 아닌 작업들
async function DynamicContent() {
// 네트워크 요청
const data = await fetch('https://api.example.com/data')
// DB 쿼리
const users = await db.query('SELECT * FROM users')
// 비동기 파일 읽기
const file = await fs.readFile('..', 'utf-8')
// 외부 시스템 지연 시뮬레이션
await setTimeout(100)
return <div>Not in the static shell</div>
}
// Page: Suspense로 감싸기
export default async function Page() {
return (
<>
<h1>Part of the static shell</h1>
{/* 아래 fallback은 static shell에 포함 */}
<Suspense fallback={<p>Loading..</p>}>
<DynamicContent />
<div>Sibling excluded from static shell</div>
</Suspense>
</>
)
}
- prerendering은
fetch에서 멈추며, 요청도 시작되지 않습니다. <p>Loading..</p>는 static shell에 포함되고,DynamicContent결과는 요청 시점에 스트리밍됩니다.
자주 변하지 않는 동적 데이터라면,
<Suspense>대신use cache를 사용해 static shell에 포함할 수도 있습니다.
4.2 Runtime data
런타임 데이터는 요청 컨텍스트가 있어야만 접근 가능한 데이터입니다.
예:
cookies()– 사용자의 쿠키headers()– 요청 헤더searchParams– URL 쿼리 파라미터params– 동적 라우트 파라미터
(단,generateStaticParams로 샘플을 제공한 경우 일부는 정적으로 가능)
이런 데이터를 사용하는 컴포넌트는 반드시 <Suspense>로 감싸야 합니다.
중요: Runtime data는
use cache와 같은 스코프에서 함께 사용할 수 없으며,
무조건<Suspense>로 감싼 동적 영역에서만 사용해야 합니다.
4.3 비결정적 연산 (Non-deterministic operations)
Math.random(),Date.now(),crypto.randomUUID(),crypto.getRandomValues()등은 실행할 때마다 값이 달라집니다.- Cache Components에서는 이러한 연산이 요청 시점에 실행되도록 명시적으로 표현해야 합니다.
- 이를 위해
connection()또는 runtime data를 먼저 호출하여, “지금은 request time 컨텍스트다”라고 표시합니다.
- 이를 위해
import { connection } from 'next/server'
async function UniqueContent() {
// 요청 시점으로 명시적 defer
await connection()
const random = Math.random()
const now = Date.now()
const date = new Date()
const uuid = crypto.randomUUID()
const bytes = crypto.getRandomValues(new Uint8Array(16))
return (
<div>
<p>{random}</p>
<p>{now}</p>
<p>{date.getTime()}</p>
<p>{uuid}</p>
<p>{bytes}</p>
</div>
)
}
- 이 컴포넌트 역시
<Suspense>로 감싸야 하며,
각 요청마다 다른 난수·시간·UUID를 보게 됩니다.
5. use cache 지시문 사용 (Using use cache)
use cache 지시문은 비동기 함수와 컴포넌트의 반환값을 캐시합니다.
- 적용 위치:
- 함수 스코프
- 컴포넌트 스코프
- 파일 전체 스코프
- 캐시 키:
- **함수 인자(arguments)**와
- 상위 스코프에서 클로저로 잡힌 값들이 자동으로 cache key에 포함됩니다.
→ 입력 값에 따라 서로 다른 캐시 엔트리가 생성되므로, 개인화 또는 파라미터 기반 캐시가 가능합니다.
동적 데이터가 매 요청마다 최신일 필요가 없다면:
use cache를 사용하여- prerendering 시 static shell에 포함하거나
- 런타임 여러 요청에서 같은 결과를 재사용할 수 있습니다.
캐시된 내용은 두 가지 방식으로 리밸리데이트할 수 있습니다.
cacheLife로 정의한 **수명(lifetime)**에 따라 자동으로cacheTag+revalidateTag/updateTag를 사용한 태그 기반 온디맨드 리밸리데이트
자세한 제약 사항 및 직렬화 제한은
use cacheAPI 문서를 참고해야 합니다.
5.1 Prerendering 시 use cache 사용 (During prerendering)
- 상품 목록, 블로그 본문, 과거 날짜의 분석 리포트 등은
자주 변하지 않는 경우가 많습니다. - 런타임 데이터에 의존하지 않는다면,
use cache로 감싸서 static shell에 포함할 수 있습니다.
// app/page.tsx
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife('hours') // 기본 프로필 사용
const users = await db.query('SELECT * FROM users')
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
cacheLife는'hours','days','weeks'같은 프로필 이름 또는
stale,revalidate,expire값을 가진 설정 객체를 받을 수 있습니다.
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife({
stale: 3600, // 1시간 후 stale
revalidate: 7200, // 2시간 후 백그라운드 리밸리데이트
expire: 86400, // 1일 후 캐시 만료
})
const users = await db.query('SELECT * FROM users')
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
- 리밸리데이트 시 static shell이 새로운 데이터로 갱신됩니다.
5.2 Runtime data와 함께 사용 (With runtime data)
- Runtime data와
use cache는 같은 스코프에서 함께 사용할 수 없습니다. - 대신, 다음 패턴을 사용합니다.
- runtime API를 읽는 컴포넌트는
use cache없이 실행 - 읽어온 값을 인자로 받아
use cache가 선언된 함수/컴포넌트에 전달
- runtime API를 읽는 컴포넌트는
// app/profile/page.tsx
import { cookies } from 'next/headers'
import { Suspense } from 'react'
export default function Page() {
// Page가 동적 boundary 역할
return (
<Suspense fallback={<div>Loading...</div>}>
<ProfileContent />
</Suspense>
)
}
// runtime data 읽기 (캐시 X)
async function ProfileContent() {
const session = (await cookies()).get('session')?.value
return <CachedContent sessionId={session} />
}
// 캐시되는 컴포넌트/함수
async function CachedContent({ sessionId }: { sessionId: string }) {
'use cache'
// sessionId는 cache key의 일부가 됨
const data = await fetchUserData(sessionId)
return <div>{data}</div>
}
- 요청 시점에 적절한 캐시 엔트리가 없으면
CachedContent가 실행되고, 결과가 향후 요청을 위해 저장됩니다.
5.3 비결정적 연산과 함께 사용 (With non-deterministic operations)
use cache스코프 내부에서 비결정적 연산을 실행하면,
prerendering 시 한 번만 실행되어 그 결과가 캐시됩니다.
export default async function Page() {
'use cache'
// 한 번만 실행되고, 이후 요청에는 동일한 결과 사용
const random = Math.random()
const random2 = Math.random()
const now = Date.now()
const date = new Date()
const uuid = crypto.randomUUID()
const bytes = crypto.getRandomValues(new Uint8Array(16))
return (
<div>
<p>
{random} and {random2}
</p>
<p>{now}</p>
<p>{date.getTime()}</p>
<p>{uuid}</p>
<p>{bytes}</p>
</div>
)
}
- 캐시가 리밸리데이트되기 전까지 모든 요청은 같은 랜덤 값과 시간, UUID를 보게 됩니다.
6. 태깅과 리밸리데이트 (Tagging and revalidating)
cacheTag로 캐시된 데이터를 태그하고, 이후 Server Action에서 updateTag 또는 revalidateTag로 리밸리데이트할 수 있습니다.
6.1 updateTag 사용 (즉시 갱신)
- 같은 요청 안에서 캐시를 만료하고 즉시 새 데이터로 갱신해야 할 때 사용합니다.
// app/actions.ts
import { cacheTag, updateTag } from 'next/cache'
export async function getCart() {
'use cache'
cacheTag('cart')
// 장바구니 데이터 fetch
}
export async function updateCart(itemId: string) {
'use server'
// itemId를 사용하여 데이터 갱신
updateTag('cart') // cart 태그가 달린 캐시를 즉시 갱신
}
6.2 revalidateTag 사용 (stale-while-revalidate)
- 태그가 달린 캐시 엔트리만 골라서 stale-while-revalidate 방식으로 무효화할 때 사용합니다.
- 일정 정도의 지연(최종 일관성)을 허용하는 정적 콘텐츠에 적합합니다.
// app/actions.ts
import { cacheTag, revalidateTag } from 'next/cache'
export async function getPosts() {
'use cache'
cacheTag('posts')
// 게시물 목록 fetch
}
export async function createPost(post: FormData) {
'use server'
// 새 게시글 작성
revalidateTag('posts', 'max')
}
7. 무엇을 캐시할 것인가? (What should I cache?)
- 캐시 전략은 원하는 UI 로딩 상태와 직결됩니다.
- 다음 조건을 만족한다면
use cache+cacheLife를 사용하는 것이 좋습니다.- 런타임 데이터에 의존하지 않고,
- 일정 기간 동안 여러 요청에 같은 데이터를 제공해도 괜찮을 때
- CMS 같은 시스템에서는:
- 캐시 기간을 넉넉하게 두고,
- 콘텐츠가 실제로 변경될 때
revalidateTag를 사용해 리밸리데이트하는 패턴이 좋습니다.
→ 미리 캐시를 자주 버리는 대신, 변경 시점에만 갱신하여 빠른 응답 + 신선한 데이터를 동시에 만족시킬 수 있습니다.
8. 전체 예시 (Putting it all together)
정적 콘텐츠 + 캐시된 동적 콘텐츠 + 스트리밍 동적 콘텐츠를 하나의 페이지에 함께 사용하는 예시 구조입니다.
// app/blog/page.tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'
import Link from 'next/link'
export default function BlogPage() {
return (
<>
{/* 정적 콘텐츠 - 자동 프리렌더 */}
<header>
<h1>Our Blog</h1>
<nav>
<Link href="/">Home</Link> | <Link href="/about">About</Link>
</nav>
</header>
{/* 캐시된 동적 콘텐츠 - static shell에 포함 */}
<BlogPosts />
{/* 런타임 동적 콘텐츠 - 요청 시 스트리밍 */}
<Suspense fallback={<p>Loading your preferences...</p>}>
<UserPreferences />
</Suspense>
</>
)
}
// 모든 사용자가 공유하는 블로그 글 목록
async function BlogPosts() {
'use cache'
cacheLife('hours')
const res = await fetch('https://api.vercel.app/blog')
const posts = await res.json()
return (
<section>
<h2>Latest Posts</h2>
<ul>
{posts.slice(0, 5).map((post: any) => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>
By {post.author} on {post.date}
</p>
</li>
))}
</ul>
</section>
)
}
// 사용자별 개인화 (쿠키 기반)
async function UserPreferences() {
const theme = (await cookies()).get('theme')?.value || 'light'
const favoriteCategory = (await cookies()).get('category')?.value
return (
<aside>
<p>Your theme: {theme}</p>
{favoriteCategory && <p>Favorite category: {favoriteCategory}</p>}
</aside>
)
}
렌더링 흐름 요약:
- prerendering 시:
- 헤더(정적),
BlogPosts(캐시된 동적 데이터),UserPreferences의 fallback이 static shell에 포함됩니다.
- 헤더(정적),
- 요청 시:
- 사용자는 즉시 헤더 + 블로그 글 목록을 확인할 수 있습니다.
- 쿠키에 의존하는 개인화된
UserPreferences는 요청 시점에 스트리밍됩니다.
9. Cache Components 활성화 (Enabling Cache Components)
next.config.js 또는 next.config.ts에서 cacheComponents 옵션을 켜면 됩니다.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
- Cache Components를 활성화하면, GET Route Handlers도 페이지와 동일한 prerendering 모델을 따릅니다.
10. Navigation과 Activity
cacheComponents를 켜면, Next.js는 클라이언트 내비게이션 시 React의 <Activity> 컴포넌트를 사용하여 상태를 보존합니다.
- 새 라우트로 이동할 때 이전 라우트를 바로 언마운트하지 않고, Activity 모드를
"hidden"으로 설정합니다.- 라우트 상태(폼 입력값, 펼쳐진 아코디언 등)가 유지됩니다.
- 뒤로 가기를 하면 이전 상태 그대로 다시 나타납니다.
- 숨겨져 있는 동안 효과(effects)는 정리되고, 다시 표시되면 재생성됩니다.
- Next.js는 최근 방문한 몇 개의 라우트만
"hidden"상태로 유지하고,
오래된 라우트는 DOM에서 제거하여 메모리 사용이 과도하게 증가하는 것을 방지합니다.
11. 기존 Route Segment 설정 마이그레이션
Cache Components를 활성화하면, 기존 App Router의 일부 route segment 설정이 더 이상 필요 없거나 지원되지 않습니다.
11.1 dynamic = "force-dynamic"
- 더 이상 필요 없습니다. 모든 페이지는 기본적으로 **동적(dynamic)**입니다.
// Before
export const dynamic = 'force-dynamic'
export default function Page() {
return <div>...</div>
}
// After
export default function Page() {
return <div>...</div>
}
11.2 dynamic = "force-static"
- 우선 제거하는 것이 권장됩니다.
- 개발/빌드 시점에 동적 데이터 또는 runtime data 접근이 감지되면 에러가 발생하며,
이 에러를 바탕으로 다음처럼 조정합니다.
- 동적 데이터(fetch 등)
- 데이터 접근 가까이에
use cache를 추가하고,
cacheLife('max')같은 긴 캐시 기간을 부여해 기존의 static behavior를 유지합니다.
- 데이터 접근 가까이에
- Runtime data (
cookies(),headers()등)- Suspense로 감싸라는 에러 안내에 따라
<Suspense>를 추가해야 합니다. - 원래
force-static을 사용하고 있었다면, 요청 시점 작업을 제거해야 완전히 정적이 됩니다.
- Suspense로 감싸라는 에러 안내에 따라
11.3 revalidate
cacheLife로 교체합니다.
기존 route segment 설정 대신, 컴포넌트 내부에서 캐시 수명을 정의해야 합니다.
// Before
export const revalidate = 3600 // 1 hour
export default async function Page() {
return <div>...</div>
}
// After
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife('hours')
return <div>...</div>
}
11.4 fetchCache
- 필요 없습니다.
use cache스코프 안에 있는 모든fetch는 자동으로 캐시됩니다.
// Before
export const fetchCache = 'force-cache'
// After
export default async function Page() {
'use cache'
// 여기서 하는 fetch는 자동 캐시
return <div>...</div>
}
11.5 runtime = 'edge'
- 지원되지 않습니다.
Cache Components는 Node.js 런타임이 필요하며, Edge Runtime과 함께 사용할 수 없습니다.
12. Next Steps
더 깊이 학습하려면 다음 API 문서를 함께 보는 것이 좋습니다.
cacheComponents– Next.js에서 Cache Components 플래그 활성화 방법use cache– 데이터/컴포넌트를 캐시하는 방법cacheLife– 캐시 수명 설정cacheTag– 캐시 무효화(tagging) 관리revalidateTag/updateTag– 태그 기반 온디맨드 리밸리데이트
이 정리본을 기준으로 스터디에서:
cacheComponents: true를 켜고- 간단한 페이지에
- 정적 섹션
use cache+cacheLife섹션<Suspense>+ runtime data 섹션
을 하나씩 추가해 보면, static shell에 들어가는 것 / 요청 시점에 스트리밍되는 것이 어떻게 나뉘는지 감각을 빠르게 익힐 수 있습니다.