
Next.js는 App Router 기반으로 서버 컴포넌트와 데이터 페칭을 통합하면서
캐싱 체계를 전면 재설계하였습니다. 본 글에서는 실무에서 가장 자주
사용되는 네 가지 축, 즉 Request Memoization, Client Router
Cache, Data Cache, Full Route Cache를 중심으로 원리를
정리하고, 실행 가능한 예제와 함께 권장되는 베스트 프랙티스를 제시합니다.
cookies(), headers(), searchParams,dynamic/revalidate 설정revalidate, revalidatePath, revalidateTag,router.refresh()동일한 입력으로 fetch를 여러 컴포넌트에서 호출하더라도 서버 렌더링 한
사이클 내에서는 단 한 번만 네트워크 요청이 발생하며, 그 결과가
공유됩니다. 이는 네트워크 비용과 백엔드 부하를 크게 줄여줍니다.
fetch 중복 제거cache: 'no-store'일 경우에도 동일 렌더 내에서는 중복동일 데이터를 여러 컴포넌트에서 사용하더라도 네트워크는 1회만
발생합니다.
// app/posts/[id]/page.tsx
import { Suspense } from 'react';
async function getPost(id: string) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
cache: 'no-store'
});
return res.json() as Promise<{ id: number; title: string; body: string }>
}
async function PostTitle({ id }: { id: string }) {
const post = await getPost(id)
return <h2>{post.title}</h2>
}
async function PostBody({ id }: { id: string }) {
const post = await getPost(id)
return <p>{post.body}</p>
}
export default async function Page({ params }: { params: { id: string } }) {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<PostTitle id={params.id} />
<PostBody id={params.id} />
</Suspense>
</div>
)
}
브라우저 메모리에 RSC Payload와 레이아웃/로딩 상태 등을 캐싱하여 탐색
속도를 향상시킵니다. next/link의 사전 불러오기(prefetch) 기능과
연계되어 페이지 전환이 즉각적으로 이루어집니다.
router.prefetch()router.refresh() 호출목록 페이지에서 상세 링크를 사전 불러오고, 변경 시 캐시를 무효화합니다.
// app/articles/page.tsx
import Link from 'next/link'
export default function ArticlesPage() {
const items = Array.from({ length: 5 }).map((_, i) => ({ id: i + 1, title: `Post ${i + 1}` }))
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<Link href={`/articles/${item.id}`}>{item.title}</Link>
</li>
))}
</ul>
)
}
변경 후에는 서버 액션과 함께 revalidatePath 또는 클라이언트의
router.refresh()를 호출하여 동기화를 보장합니다.
서버에서 실행되는 fetch 응답을 캐싱합니다. 기본 동작은 렌더링 모드 및
옵션에 따라 달라집니다.
GET 요청이 기본적으로 캐싱force-cache 유사)no-storenext: { revalidate: number }를 지정하면 ISR처럼// app/products/page.tsx
export const revalidate = 60
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
cache: 'force-cache',
next: { revalidate: 120, tags: ['products'] }
})
if (!res.ok) throw new Error('Failed to load')
return res.json() as Promise<Array<{ id: string; name: string }>>
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
<h2>Products</h2>
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
)
}
태그 단위 무효화:
// app/admin/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function createProduct(name: string) {
await fetch('https://api.example.com/products', {
method: 'POST',
body: JSON.stringify({ name })
})
revalidateTag('products')
}
페이지 전체(HTML + RSC Payload)를 캐싱합니다. 이는 빌드 타임 또는 최초
요청 시 생성되며, revalidate에 따라 주기적으로 갱신됩니다.
export const revalidate = number 또는dynamic = 'force-static'dynamic = 'force-dynamic', cache: 'no-store',구분 범위 저장 위치 기본 수명 명시 제어 대표 사용처
Request 단일 서버 서버 메모리 요청 내장 기능 동일 데이터
Memoization 렌더 사이클 종료까지 중복 호출
제거
Client Router 클라이언트 브라우저 세션 유사 router.refresh() 빠른 페이지
Cache 탐색 메모리 전환
Data Cache 개별 fetch 서버 캐시 옵션에 따름 cache/no-store, API 응답
응답 revalidate, tags 캐싱
Full Route 페이지 전체 서버 캐시 revalidate revalidate, 마케팅,
Cache 주기 dynamic 블로그,
카탈로그
cache: 'no-store' 설정revalidate 또는 force-cache 전략으로 관리revalidateTag)revalidatePath 및 router.refresh()로 캐시prefetch={false}로 과도한 사전 요청 제어revalidate를 활용force-cache/no-store/next.revalidate/태그 전략을revalidatePath, revalidateTag, router.refresh()를 통해 무효화핵심은 "정적 가능 영역을 최대화하고, 동적 처리는 최소화"하는
것입니다. Next.js의 네 가지 캐시 축을 적절히 조합하면 성능과 일관성을
동시에 달성할 수 있습니다.