Cache Components
Cache Components는 Next.js의 새로운 렌더링 및 캐싱 접근 방식으로, Partial Prerendering (PPR)을 통해 우수한 사용자 경험을 보장하면서도 무엇을 언제 캐시할지에 대한 세밀한 제어를 제공합니다.
Cache Components란?
동적 애플리케이션을 개발할 때, 두 가지 주요 접근 방식 사이에서 균형을 맞춰야 합니다:
- 완전히 정적인 페이지는 빠르게 로드되지만 개인화되거나 실시간 데이터를 표시할 수 없습니다
- 완전히 동적인 페이지는 최신 데이터를 표시할 수 있지만 매 요청마다 모든 것을 렌더링해야 하므로 초기 로드가 느립니다
Cache Components가 활성화되면, Next.js는 모든 라우트를 기본적으로 동적으로 처리합니다. 모든 요청은 사용 가능한 최신 데이터로 렌더링됩니다. 하지만 대부분의 페이지는 정적 부분과 동적 부분으로 구성되어 있으며, 모든 동적 데이터가 매 요청마다 소스에서 가져와질 필요는 없습니다.
Cache Components를 사용하면 데이터와 UI의 일부를 캐시 가능하도록 표시할 수 있으며, 이는 페이지의 정적 부분과 함께 사전 렌더링 단계에 포함됩니다.
Cache Components는 Partial Prerendering (PPR)과 use cache를 구현하여 두 가지 장점을 모두 제공합니다.
사용자가 라우트를 방문하면:
- 서버는 캐시된 콘텐츠를 포함하는 정적 셸을 전송하여 빠른 초기 로드를 보장합니다
- Suspense 경계로 감싸진 동적 섹션은 셸에 fallback UI를 표시합니다
- 동적 부분만 렌더링되어 fallback을 대체하며, 준비되는 대로 병렬로 스트리밍됩니다
- use cache로 캐싱하여 원래 동적이었던 데이터를 초기 셸에 포함할 수 있습니다
작동 방식
Cache Components는 렌더링을 제어하기 위한 세 가지 주요 도구를 제공합니다:
1. 런타임 데이터를 위한 Suspense
일부 데이터는 실제 사용자가 요청할 때만 런타임에 사용 가능합니다. cookies, headers, searchParams와 같은 API는 요청별 정보에 액세스합니다. 이러한 API를 사용하는 컴포넌트를 Suspense 경계로 감싸면 페이지의 나머지 부분을 정적 셸로 사전 렌더링할 수 있습니다.
런타임 API에는 다음이 포함됩니다:
- cookies
- headers
- searchParams prop
- params prop - generateStaticParams를 통해 최소한 하나의 예시 값을 제공하지 않는 한 이는 런타임 데이터입니다. 제공되면 해당 특정 param 값은 사전 렌더링된 경로에 대해 정적으로 처리되고, 다른 값은 런타임으로 유지됩니다
2. 동적 데이터를 위한 Suspense
fetch 호출이나 데이터베이스 쿼리(db.query(...))와 같은 동적 데이터는 요청 간에 변경될 수 있지만 사용자별로 다르지 않습니다. connection API는 메타-동적입니다—실제로 반환할 데이터가 없더라도 사용자 네비게이션을 기다리는 것을 나타냅니다. 이러한 것들을 사용하는 컴포넌트를 Suspense 경계로 감싸서 스트리밍을 활성화하세요.
동적 데이터 패턴에는 다음이 포함됩니다:
- fetch 요청
- 데이터베이스 쿼리
- connection
3. use cache로 캐시된 데이터
Server Component에 use cache를 추가하여 캐시되도록 만들고 사전 렌더링된 셸에 포함시킬 수 있습니다. 캐시된 컴포넌트 내부에서는 런타임 API를 사용할 수 없습니다. 유틸리티 함수를 use cache로 표시하고 Server Component에서 호출할 수도 있습니다.
export async function getProducts() {
'use cache'
const data = await db.query('SELECT * FROM products')
return data
}
Suspense 경계 사용하기
React Suspense 경계를 사용하면 동적 또는 런타임 데이터를 감쌀 때 사용할 fallback UI를 정의할 수 있습니다.
fallback UI를 포함한 경계 외부의 콘텐츠는 정적 셸로 사전 렌더링되며, 경계 내부의 콘텐츠는 준비되면 스트리밍됩니다.
다음은 Cache Components에서 Suspense를 사용하는 방법입니다:
import { Suspense } from 'react'
export default function Page() {
return (
<>
<h1>This will be pre-rendered</h1>
<Suspense fallback={<Skeleton />}>
<DynamicContent />
</Suspense>
</>
)
}
async function DynamicContent() {
const res = await fetch('http://api.cms.com/posts')
const { posts } = await res.json()
return <div>{/* ... */}</div>
}
빌드 시점에 Next.js는 정적 콘텐츠와 fallback UI를 사전 렌더링하고, 동적 콘텐츠는 사용자가 라우트를 요청할 때까지 연기됩니다.
누락된 Suspense 경계
Cache Components는 동적 코드가 Suspense 경계로 감싸져야 함을 강제합니다. 잊어버리면 다음 오류가 표시됩니다:
이로 인해 전체 페이지 렌더링이 지연되어 느린 사용자 경험이 발생합니다. Next.js는 이 오류를 사용하여 앱이 모든 네비게이션에서 즉시 로드되도록 보장합니다.
이를 해결하려면 다음 중 하나를 수행할 수 있습니다:
- 컴포넌트를 <Suspense>로 감싸기 - 이를 통해 Next.js가 나머지 앱을 차단하지 않고 준비되는 즉시 콘텐츠를 사용자에게 스트리밍할 수 있습니다.
- 비동기 await을 Cache Component("use cache")로 이동 - 이를 통해 Next.js가 컴포넌트를 HTML 문서의 일부로 정적으로 사전 렌더링하여 사용자에게 즉시 표시되도록 합니다.
params, cookies, headers와 같은 요청별 정보는 정적 사전 렌더링 중에 사용할 수 없으므로 <Suspense>로 감싸야 합니다.
이 오류는 정적 셸을 즉시 얻는 대신 사용자가 아무것도 표시되지 않는 차단된 런타임 렌더링에 도달하는 상황을 방지하는 데 도움이 됩니다. 이를 해결하려면 Suspense 경계를 추가하거나 use cache를 사용하여 작업을 대신 캐시하세요.
스트리밍 작동 방식
스트리밍은 라우트를 청크로 분할하고 준비되는 대로 클라이언트에 점진적으로 스트리밍합니다. 이를 통해 사용자는 전체 콘텐츠의 렌더링이 완료되기 전에 페이지의 일부를 즉시 볼 수 있습니다.
부분 사전 렌더링을 사용하면 초기 UI를 브라우저에 즉시 전송할 수 있으며 동적 부분이 렌더링됩니다. 이는 UI까지의 시간을 줄이고 UI의 사전 렌더링 비율에 따라 전체 요청 시간을 줄일 수 있습니다.
네트워크 오버헤드를 줄이기 위해 정적 HTML과 스트리밍된 동적 부분을 포함한 전체 응답이 단일 HTTP 요청으로 전송됩니다. 이는 추가 왕복을 방지하고 초기 로드와 전체 성능을 모두 향상시킵니다.
use cache 사용하기
Suspense 경계가 동적 콘텐츠를 관리하는 반면, use cache 지시어는 자주 변경되지 않는 데이터나 계산을 캐싱하는 데 사용할 수 있습니다.
기본 사용법
페이지, 컴포넌트 또는 비동기 함수를 캐시하려면 use cache를 추가하고 cacheLife로 수명을 정의하세요:
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife('hours')
// fetch 또는 계산
return <div>...</div>
}
주의사항
use cache를 사용할 때 다음 제약 사항을 염두에 두세요:
인수는 직렬화 가능해야 합니다
Server Action과 마찬가지로, 캐시된 함수의 인수는 직렬화 가능해야 합니다. 이는 원시 타입, 일반 객체, 배열을 전달할 수 있지만 클래스 인스턴스, 함수 또는 기타 복잡한 타입은 전달할 수 없음을 의미합니다.
내부 검사 없이 직렬화 불가능한 값 허용
내부 검사를 하지 않는 한 직렬화 불가능한 값을 인수로 허용할 수 있습니다. 그러나 반환할 수는 있습니다. 이를 통해 Server 또는 Client Component를 children으로 허용하는 캐시된 컴포넌트와 같은 패턴이 가능합니다:
import type { ReactNode } from 'react'
import { setTimeout } from 'node:timers/promises'
async function getSiteTitle() {
// 느린 데이터베이스 또는 API 호출 시뮬레이션
await setTimeout(1000) // 'node:timers/promises'에서
return 'My Website'
}
export async function CachedWrapper({ children }: { children: ReactNode }) {
'use cache'
const title = await getSiteTitle()
// children을 내부 검사하지 말고 그냥 전달
return (
<div className="wrapper">
<h1>{title}</h1>
{children}
</div>
)
}
태깅 및 재검증
cacheTag로 캐시된 데이터에 태그를 지정하고, Server Action에서 updateTag를 사용하여 즉각적인 업데이트를 위해 변경 후 재검증하거나, 업데이트 지연이 허용되는 경우 revalidateTag를 사용하세요.
updateTag 사용
동일한 요청 내에서 캐시된 데이터를 만료하고 즉시 새로 고쳐야 하는 경우 updateTag를 사용하세요:
import { cacheTag, updateTag } from 'next/cache'
export async function getCart() {
'use cache'
cacheTag('cart')
// 데이터 가져오기
}
export async function updateCart(itemId: string) {
'use server'
// itemId를 사용하여 데이터 쓰기
// 사용자 장바구니 업데이트
updateTag('cart')
}
revalidateTag 사용
적절하게 태그가 지정된 캐시 항목만 stale-while-revalidate 동작으로 무효화하려는 경우 revalidateTag를 사용하세요. 이는 최종 일관성을 허용할 수 있는 정적 콘텐츠에 이상적입니다.
import { cacheTag, revalidateTag } from 'next/cache'
export async function getPosts() {
'use cache'
cacheTag('posts')
// 데이터 가져오기
}
export async function createPost(post: FormData) {
'use server'
// FormData를 사용하여 데이터 쓰기
revalidateTag('posts', 'max')
}
updateTag vs revalidateTag 선택 기준:
- updateTag: 사용자 장바구니, 좋아요 카운트 등 즉각적인 반영이 필요한 경우
- revalidateTag: 블로그 포스트, 제품 목록 등 약간의 지연이 허용되는 경우
일반적으로 사용자 경험에 직접적인 영향을 미치는 데이터는 updateTag를, 정적 콘텐츠는 revalidateTag를 사용하는 것이 좋습니다.
Cache Components 활성화하기
Next config 파일에 cacheComponents 옵션을 추가하여 Cache Components(PPR 포함)를 활성화할 수 있습니다:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
Cache Components를 사용한 네비게이션
cacheComponents 플래그가 활성화되면, Next.js는 React의 <Activity> 컴포넌트를 사용하여 클라이언트 측 네비게이션 중 컴포넌트 상태를 보존합니다.
이전 라우트를 탐색할 때 언마운트하는 대신, Next.js는 Activity 모드를 "hidden"으로 설정합니다. 이는 다음을 의미합니다:
- 라우트 간 탐색 시 컴포넌트 상태가 보존됩니다
- 뒤로 탐색하면 이전 라우트가 상태를 유지한 채 다시 나타납니다
- 라우트가 숨겨지면 효과가 정리되고 다시 표시되면 재생성됩니다
라우트 세그먼트 설정에 미치는 영향
Cache Components가 활성화되면 여러 라우트 세그먼트 설정 옵션이 더 이상 필요하지 않거나 지원되지 않습니다. 변경 사항과 마이그레이션 방법은 다음과 같습니다:
dynamic = "force-dynamic"
필요 없음. Cache Components가 활성화되면 모든 페이지가 기본적으로 동적이므로 이 설정은 불필요합니다.
// 이전 - 더 이상 필요 없음
export const dynamic = 'force-dynamic'
export default function Page() {
return <div>...</div>
}
// 이후 - 그냥 제거하면 됨, 페이지는 기본적으로 동적
export default function Page() {
return <div>...</div>
}
dynamic = "force-static"
use cache로 대체. 대신 연결된 라우트의 각 Layout 및 Page에 use cache를 추가해야 합니다.
참고: force-static은 이전에 cookies()와 같은 런타임 API의 사용을 허용했지만 이는 더 이상 지원되지 않습니다. use cache를 추가하고 런타임 데이터와 관련된 오류가 표시되면 런타임 API 사용을 제거해야 합니다.
// 이전
export const dynamic = 'force-static'
export default async function Page() {
const data = await fetch('https://api.example.com/data')
return <div>...</div>
}
// 이후 - 'use cache' 사용
export default async function Page() {
'use cache'
const data = await fetch('https://api.example.com/data')
return <div>...</div>
}
revalidate
cacheLife로 대체. 라우트 세그먼트 설정 대신 cacheLife 함수를 사용하여 캐시 기간을 정의하세요.
// 이전
export const revalidate = 3600 // 1시간
export default async function Page() {
return <div>...</div>
}
// 이후 - cacheLife 사용
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife('hours')
return <div>...</div>
}
fetchCache
필요 없음. use cache를 사용하면 캐시된 범위 내의 모든 데이터 가져오기가 자동으로 캐시되므로 fetchCache가 불필요합니다.
// 이전
export const fetchCache = 'force-cache'
// 이후 - 'use cache'를 사용하여 캐싱 동작 제어
export default async function Page() {
'use cache'
// 여기의 모든 fetch는 캐시됩니다
return <div>...</div>
}
runtime = 'edge'
지원되지 않음. Cache Components는 Node.js 런타임이 필요하며 Edge Runtime에서 오류가 발생합니다.
Cache Components로 마이그레이션할 때 다음을 확인하세요:
- dynamic = "force-dynamic" 제거
- dynamic = "force-static"을 use cache로 변경
- revalidate를 cacheLife로 변경
- fetchCache 제거
- Edge Runtime 사용 중이라면 Node.js로 전환 고려
- 런타임 API 사용 위치에 Suspense 경계 추가
Cache Components 이전 vs 이후
Cache Components가 사고 모델을 어떻게 변경하는지 이해하기:
Cache Components 이전
- 기본적으로 정적: Next.js는 opt-out하지 않는 한 가능한 많은 것을 사전 렌더링하고 캐시하려고 시도했습니다
- 라우트 수준 제어: dynamic, revalidate, fetchCache와 같은 스위치가 전체 페이지의 캐싱을 제어했습니다
- fetch의 한계: fetch만 사용하는 것은 직접 데이터베이스 클라이언트나 다른 서버 측 IO를 다루지 못했기 때문에 불완전했습니다. 중첩된 fetch가 동적으로 전환되면(예: { cache: 'no-store' }) 의도치 않게 전체 라우트 동작을 변경할 수 있었습니다
Cache Components 사용
- 기본적으로 동적: 모든 것이 기본적으로 동적입니다. use cache를 추가하여 캐시할 부분을 직접 결정합니다
- 세밀한 제어: 파일/컴포넌트/함수 수준의 use cache와 cacheLife가 필요한 곳에서 정확하게 캐싱을 제어합니다
- 스트리밍 유지: <Suspense> 또는 loading.(js|tsx) 파일을 사용하여 셸이 즉시 표시되는 동안 동적 부분을 스트리밍합니다
- fetch를 넘어서: use cache 지시어를 사용하면 fetch뿐만 아니라 모든 서버 IO(데이터베이스 호출, API, 계산)에 캐싱을 적용할 수 있습니다. 중첩된 fetch 호출은 명시적 캐시 경계와 Suspense에 의해 동작이 제어되므로 전체 라우트를 자동으로 전환하지 않습니다
예시
동적 API
cookies()와 같은 런타임 API에 액세스할 때, Next.js는 이 컴포넌트 위의 fallback UI만 사전 렌더링합니다.
이 예시에서는 fallback이 정의되지 않았으므로 Next.js는 fallback을 제공하도록 지시하는 오류를 표시합니다. <User /> 컴포넌트는 cookies API를 사용하므로 Suspense로 감싸야 합니다:
import { cookies } from 'next/headers'
export async function User() {
const session = (await cookies()).get('session')?.value
return '...'
}
이제 User 컴포넌트 주위에 Suspense 경계가 있으므로 Skeleton UI로 Page를 사전 렌더링하고 특정 사용자가 요청할 때 <User /> UI를 스트리밍할 수 있습니다:
import { Suspense } from 'react'
import { User, AvatarSkeleton } from './user'
export default function Page() {
return (
<section>
<h1>This will be pre-rendered</h1>
<Suspense fallback={<AvatarSkeleton />}>
<User />
</Suspense>
</section>
)
}
동적 props 전달하기
cookies나 searchParams와 같은 런타임 값에 액세스하는 컴포넌트는 사전 렌더링될 수 없습니다. 페이지 콘텐츠의 더 많은 부분을 사전 렌더링하려면 이러한 props를 전달하고 트리에서 더 낮은 위치에서 값에 액세스할 수 있습니다. 예를 들어, <Page /> 컴포넌트에서 searchParams를 읽는 경우 이 값을 다른 컴포넌트에 prop으로 전달할 수 있습니다:
import { Table, TableSkeleton } from './table'
import { Suspense } from 'react'
export default function Page({
searchParams,
}: {
searchParams: Promise<{ sort: string }>
}) {
return (
<section>
<h1>This will be pre-rendered</h1>
<Suspense fallback={<TableSkeleton />}>
<Table searchParams={searchParams.then((search) => search.sort)} />
</Suspense>
</section>
)
}
table 컴포넌트 내부에서 searchParams의 값에 액세스하면 컴포넌트가 동적이 되지만 페이지의 나머지 부분은 사전 렌더링됩니다.
export async function Table({ sortPromise }: { sortPromise: Promise<string> }) {
const sort = (await sortPromise) === 'true'
return '...'
}
동적 props 전달 패턴의 장점:
- 페이지의 더 많은 부분을 사전 렌더링하여 초기 로드 속도 향상
- 필요한 곳에만 동적 렌더링을 격리하여 성능 최적화
- 컴포넌트 재사용성 향상 - 동적 데이터를 prop으로 받아 더 유연하게 사용
이 패턴은 특히 검색, 필터링, 정렬 기능이 있는 테이블이나 리스트에 효과적입니다.
Cache Components를 사용한 Route Handler
GET Route Handler는 애플리케이션의 일반 UI 라우트와 동일한 모델을 따릅니다. 기본적으로 동적이며, 결정적일 때 사전 렌더링될 수 있고, use cache를 사용하여 캐시된 응답에 더 많은 동적 데이터를 포함할 수 있습니다.
동적 예시, 매 요청마다 다른 숫자를 반환합니다:
export async function GET() {
return Response.json({
randomNumber: Math.random(),
})
}
정적 데이터만 반환하는 핸들러는 빌드 시점에 사전 렌더링됩니다:
export async function GET() {
return Response.json({
projectName: 'Next.js',
})
}
매 요청마다 데이터베이스에서 최신 동적 데이터를 반환하는 라우트가 있다면:
export async function GET() {
const products = await db.query('SELECT * FROM products')
return Response.json(products)
}
이를 캐시하고 매 요청마다 데이터베이스에 접근하는 것을 피하려면, 동적 작업을 use cache 함수로 추출하고 cacheLife('hours')를 설정하여 데이터베이스가 시간당 최대 한 번만 쿼리되도록 하세요:
import { cacheLife } from 'next/cache'
export async function GET() {
const products = await getProducts()
return Response.json(products)
}
async function getProducts() {
'use cache'
cacheLife('hours')
return await db.query('SELECT * FROM products')
}
- use cache는 Route Handler 본문 내부에서 직접 사용할 수 없습니다. 헬퍼로 추출하세요.
- 캐시된 응답은 새 요청이 도착하면 cacheLife에 따라 재검증됩니다.
- cookies()나 headers()와 같은 런타임 API를 사용하거나 connection()을 호출하면 항상 요청 시점으로 연기됩니다(사전 렌더링 없음).
상황별 권장 전략:
- 자주 변경되지 않는 데이터 (제품 목록, 카테고리): cacheLife('hours') 또는 cacheLife('days')
- 중간 빈도 업데이트 (블로그 포스트, 뉴스): cacheLife('minutes') + revalidateTag
- 실시간 데이터 (주식 가격, 채팅): 캐싱하지 않음
- 사용자별 데이터 (장바구니, 프로필): 캐싱하지 않거나 사용자 ID 기반 세밀한 캐싱
자주 묻는 질문
Partial Prerendering (PPR)을 대체하나요?
아니요. Cache Components는 PPR을 기능으로 구현합니다. 이전의 실험적 PPR 플래그는 제거되었지만 PPR은 계속 유지됩니다.
PPR은 정적 셸과 스트리밍 인프라를 제공합니다. use cache를 사용하면 유익할 때 해당 셸에 최적화된 동적 출력을 포함할 수 있습니다.
무엇을 먼저 캐시해야 하나요?
캐시할 내용은 원하는 UI 로딩 상태의 함수여야 합니다. 데이터가 런타임 데이터에 의존하지 않고 일정 기간 동안 여러 요청에 대해 캐시된 값이 제공되어도 괜찮다면, use cache와 cacheLife를 사용하여 해당 동작을 설명하세요.
업데이트 메커니즘이 있는 콘텐츠 관리 시스템의 경우, 더 긴 캐시 기간으로 태그를 사용하고 revalidateTag에 의존하여 정적 초기 UI를 재검증 준비가 된 것으로 표시하는 것을 고려하세요. 이 패턴을 사용하면 캐시를 선제적으로 만료시키는 대신 콘텐츠가 실제로 변경될 때 업데이트하면서 빠른 캐시된 응답을 제공할 수 있습니다.
캐시된 콘텐츠를 빠르게 업데이트하려면 어떻게 해야 하나요?
cacheTag를 사용하여 캐시된 데이터에 태그를 지정한 다음 updateTag 또는 revalidateTag를 트리거하세요.
다음 단계
Cache Components의 설정 옵션에 대해 자세히 알아보세요:
- cacheComponents - Next.js에서 cacheComponents 플래그를 활성화하는 방법을 알아보세요.
- use cache - Next.js 애플리케이션에서 데이터를 캐시하기 위해 use cache 지시어를 사용하는 방법을 알아보세요.
- cacheLife - 캐시된 함수 또는 컴포넌트의 캐시 만료 시간을 설정하기 위해 cacheLife 함수를 사용하는 방법을 알아보세요.
- cacheTag - Next.js 애플리케이션에서 캐시 무효화를 관리하기 위해 cacheTag 함수를 사용하는 방법을 알아보세요.
- revalidateTag - revalidateTag 함수에 대한 API 레퍼런스.
- updateTag - updateTag 함수에 대한 API 레퍼런스.
원문: https://nextjs.org/docs/app/building-your-application/rendering/cache-components
'je개발 복습 > Next.js' 카테고리의 다른 글
| Next.js 서버 컴포넌트 가이드 (0) | 2025.11.12 |
|---|