Next.js는 App Router 기반으로 서버 컴포넌트와 데이터 페칭을 통합하면서
캐싱 체계를 전면 재설계하였습니다. 본 글에서는 실무에서 가장 자주
사용되는 네 가지 축, 즉 Request Memoization, Client Router
Cache, Data Cache, Full Route Cache를 중심으로 원리를
정리하고, 실행 가능한 예제와 함께 권장되는 베스트 프랙티스를 제시합니다.
목차
- Request Memoization: 동일 요청 중복 제거
- Client Router Cache: 탐색 성능 최적화
- Data Cache: fetch 응답의 서버 캐싱 및 검증
- Full Route Cache: 페이지 단위 정적 캐싱(ISR 포함)
- 실무 팁/주의사항, 비교 표, 참고 자료
캐싱 전반 개념 맵
- 서버 단계: Request Memoization, Data Cache, Full Route Cache
- 클라이언트 단계: Client Router Cache(RSC Payload/라우팅)
- 동적 여부 판별 기준:
cookies()
,headers()
,searchParams
,dynamic
/revalidate
설정 - 무효화 수단:
revalidate
,revalidatePath
,revalidateTag
,router.refresh()
1) Request Memoization (요청 중복 제거)
동일한 입력으로 fetch
를 여러 컴포넌트에서 호출하더라도 서버 렌더링 한
사이클 내에서는 단 한 번만 네트워크 요청이 발생하며, 그 결과가
공유됩니다. 이는 네트워크 비용과 백엔드 부하를 크게 줄여줍니다.
- 범위: 단일 서버 렌더(요청) 사이클
- 효과: 동일 URL+옵션의
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>
)
}
2) Client Router Cache (클라이언트 라우터 캐시)
브라우저 메모리에 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()
를 호출하여 동기화를 보장합니다.
3) Data Cache (데이터 응답 캐시)
서버에서 실행되는 fetch
응답을 캐싱합니다. 기본 동작은 렌더링 모드 및
옵션에 따라 달라집니다.
- 기본 규칙 요약:
- 정적 렌더링 경로에서는
GET
요청이 기본적으로 캐싱
대상(force-cache
유사) - 동적 렌더링을 유발하는 경우에는 기본값이
no-store
- 명시적으로
next: { 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')
}
4) Full Route Cache (페이지 단위 캐싱, ISR 포함)
페이지 전체(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
) - 동적 함수 사용 여부를 명확히 구분하여 Full Route Cache가 불필요하게
깨지지 않도록 관리 - 서버 액션 이후에는
revalidatePath
및router.refresh()
로 캐시
동기화 보장 - 트래픽 상황에 따라
prefetch={false}
로 과도한 사전 요청 제어
결론: 적용 가이드
- 페이지 특성을 파악하여 개인화 여부, 변경 주기, 트래픽 패턴을 분석
- 가능한 경우 Full Route Cache와
revalidate
를 활용 - 데이터 단위로
force-cache
/no-store
/next.revalidate
/태그 전략을
수립 revalidatePath
,revalidateTag
,router.refresh()
를 통해 무효화
경로를 설계- 실제 트래픽에서 TTFB, 탐색 속도, 백엔드 QPS를 모니터링하여 검증
핵심은 "정적 가능 영역을 최대화하고, 동적 처리는 최소화"하는
것입니다. Next.js의 네 가지 캐시 축을 적절히 조합하면 성능과 일관성을
동시에 달성할 수 있습니다.
'Frontend Development' 카테고리의 다른 글
React Error Boundary: 왜 아직도 클래스일까? (0) | 2025.09.07 |
---|---|
setTimeout vs Promise.then vs queueMicrotask (0) | 2025.09.05 |
면접에서 묻는 "의존성 주입 경험이 있나요?"의 의미 (1) | 2025.08.26 |
useState vs useRef vs let: 언제 무엇을 써야 할까? (0) | 2025.08.21 |
Core Web Vitals: LCP, INP, CLS 개념과 개선 방법 (3) | 2025.08.20 |