Next.js 15 cacheLife 커스텀 프로파일과 Vercel 엣지 캐시 계층 전략으로 서비스 성격별 캐시 설계하기
뉴스 피드, 마케팅 랜딩페이지, 실시간 재고 화면 — 이 세 가지를 한 서비스 안에서 어떻게 캐시하고 있나요? 배포 직후 수정된 상품 가격이 30분 넘게 낡은 값으로 노출되거나, 반대로 거의 바뀌지 않는 카테고리 목록이 요청마다 DB를 조회하는 상황을 경험해보셨을 것입니다. fetch() 옵션이나 revalidate 숫자를 파일마다 흩뿌려 관리하면 "어디서 얼마나 캐시하는지" 파악조차 어려워집니다.
Next.js 15가 도입한 use cache 디렉티브와 cacheLife() API는 이 문제를 근본적으로 다르게 접근합니다. next.config.ts에 서비스 성격별 캐시 프로파일을 한 번 정의해두면, 컴포넌트마다 이름 하나로 일관된 캐시 정책을 선언할 수 있습니다. 여기에 Vercel의 계층적 캐싱 헤더를 더하면 브라우저, 전 세계 엣지 CDN, 서버 캐시 각 레이어를 독립적으로 튜닝할 수 있습니다.
이 글에서는 Next.js App Router 경험이 있는 프론트엔드 개발자를 대상으로, 서비스 데이터 성격에 맞는 커스텀 cacheLife 프로파일을 직접 설계하고, Vercel 엣지 캐시와 서버 캐시를 계층적으로 조합해 사용자에게 항상 빠른 응답을 제공하는 전략을 단계별로 살펴봅니다.
실험적 기능 안내:
use cache디렉티브는 Next.js 15.x 기준 실험적(experimental) 기능입니다. 프로덕션 적용 전 최신 Next.js 릴리스 노트와 공식 문서를 반드시 확인하는 것을 권장합니다.
핵심 개념
cacheLife 프로파일의 세 가지 시간 축
cacheLife()에 전달하는 프로파일은 세 개의 숫자 속성으로 구성됩니다. 각각이 캐시 생애주기의 서로 다른 단계를 담당합니다.
| 속성 | 역할 | 비유 |
|---|---|---|
stale |
클라이언트가 서버 확인 없이 캐시를 그대로 사용하는 시간 | 냉장고에서 바로 꺼내 먹기 |
revalidate |
백그라운드에서 캐시를 조용히 갱신하는 주기 | 자는 동안 장 보러 가기 |
expire |
이 시간 이후 요청이 없으면 캐시를 완전히 버리고 동적 패치로 전환 | 유통기한 |
주의:
expire는 반드시revalidate보다 큰 값이어야 합니다. 이 조건을 위반하면 Next.js 빌드 시점에 에러가 발생합니다.expire = revalidate × N공식을 기준으로 값을 정하면 안전합니다.
stale-while-revalidate: 캐시가 만료된 이후에도 이전 응답을 즉시 반환하면서, 동시에 백그라운드에서 새 데이터를 가져오는 HTTP 캐시 전략입니다. 사용자는 항상 빠른 응답을 받고, 다음 요청부터 새 데이터가 적용됩니다.revalidate와expire사이의 시간이 바로 이 "낡지만 빠른" 응답 구간입니다.
Vercel 계층적 캐시 헤더
Vercel 환경에서는 세 가지 Cache-Control 계열 헤더를 통해 각 캐시 레이어를 독립적으로 제어할 수 있습니다.
Cache-Control → 브라우저 캐시 (모든 HTTP 캐시의 기본값)
CDN-Cache-Control → Vercel을 포함한 모든 중간 CDN
Vercel-CDN-Cache-Control → Vercel 엣지 네트워크 전용 (최우선 적용)실제 요청 흐름은 다음과 같습니다. Vercel 환경에서는 별도의 외부 CDN 없이 Vercel Edge CDN이 직접 오리진 서버 앞단을 담당합니다.
사용자 브라우저 (Cache-Control 기준)
↓ 캐시 미스 시
Vercel Edge CDN (Vercel-CDN-Cache-Control 최우선, CDN-Cache-Control 보조 적용)
↓ 캐시 미스 시
Next.js 서버 캐시 (use cache + cacheLife 프로파일 적용)
↓ 캐시 미스 시
데이터베이스 / 외부 APIISR(Incremental Static Regeneration): Next.js가 서버 측에서 유지하는 캐시 레이어입니다. 페이지나 데이터를 첫 요청 시 생성·저장해두고, 이후 요청에서 캐시된 결과를 반환하면서 백그라운드에서 재검증합니다.
use cache와cacheLife프로파일은 이 ISR 서버 캐시의 동작 방식을 세밀하게 제어합니다.
Vercel-CDN-Cache-Control이 존재하면 Vercel 엣지는 이 헤더만 읽고 CDN-Cache-Control과 Cache-Control은 엣지에서 무시합니다. 덕분에 브라우저와 엣지를 완전히 다른 TTL로 운영할 수 있습니다.
실전 적용
예시 1: 서비스 성격별 커스텀 프로파일 정의
next.config.ts의 cacheLife 객체에 서비스 도메인을 반영한 프로파일을 선언합니다. cacheComponents: true 옵션을 함께 활성화하면 함수뿐 아니라 Server Component 파일 단위까지 use cache 디렉티브를 적용할 수 있습니다.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// Server Component 파일·함수 단위 use cache 적용을 활성화
cacheComponents: true,
cacheLife: {
// 뉴스·블로그: 콘텐츠가 자주 바뀌어 빠른 갱신이 필요
editorial: {
stale: 600, // 10분 — 클라이언트 캐시
revalidate: 3600, // 1시간 — 백그라운드 갱신
expire: 86400, // 24시간 — 최대 수명
},
// 마케팅 랜딩페이지: 변경 빈도가 낮아 긴 TTL이 유리
marketing: {
stale: 3600, // 1시간
revalidate: 86400, // 24시간
expire: 604800, // 7일
},
// 실시간 재고·가격: 오래된 정보가 비즈니스 손실로 직결
inventory: {
stale: 60, // 1분
revalidate: 300, // 5분
expire: 600, // 10분
},
// 카테고리 목록 등 사용자 비의존적 공통 데이터
reference: {
stale: 86400, // 24시간
revalidate: 604800, // 7일
expire: 2592000, // 30일
},
},
}
export default nextConfig각 프로파일이 어떤 데이터에 적합한지 정리하면 다음과 같습니다.
| 프로파일 | 대상 데이터 | 핵심 설계 의도 |
|---|---|---|
editorial |
블로그 포스트, 뉴스 피드 | 1시간 주기 갱신으로 최신성 유지 |
marketing |
히어로 배너, 프로모션 문구 | 7일 TTL로 CDN 히트율 극대화 |
inventory |
재고 수량, 가격 | 5분 갱신으로 오정보 노출 최소화 |
reference |
카테고리, 국가 코드 | 30일 TTL로 DB 부하 제거 |
예시 2: 컴포넌트에서 프로파일 선언
앞서 정의한 프로파일을 실제 Server Component에 적용하면 어떻게 되는지 살펴봅니다. 각 컴포넌트 최상단에 'use cache'를 선언하고 cacheLife()로 프로파일 이름을 지정합니다.
import { cacheLife, cacheTag } from 'next/cache'
// 블로그 포스트 목록 — editorial 프로파일: 1시간마다 백그라운드 갱신
export async function BlogList() {
'use cache'
cacheLife('editorial')
cacheTag('posts') // 'posts' 태그로 그룹화 → revalidateTag('posts')로 즉시 무효화 가능
// fetchPosts()는 DB에서 포스트 목록을 가져오는 일반 async 함수
// 함수 자체가 아닌 BlogList 컴포넌트의 반환값(JSX)이 캐시됨
const posts = await fetchPosts()
return <PostGrid posts={posts} />
}
// 마케팅 히어로 섹션 — marketing 프로파일: 7일 유지
export async function HeroSection() {
'use cache'
cacheLife('marketing')
cacheTag('cms', 'hero')
// fetchCMSContent()는 외부 CMS API를 호출하는 일반 async 함수
const content = await fetchCMSContent('hero')
return <Hero content={content} />
}
use cache디렉티브: 파일 최상단에 선언하면 해당 파일 전체에, 함수 내부에 선언하면 그 함수의 반환값만 캐시됩니다. React의'use client'와 동일한 문법 구조입니다.cacheComponents: true가 활성화되어 있어야 컴포넌트 단위 적용이 가능합니다.
cacheTag()로 태그를 지정해두면, CMS 웹훅이나 관리자 API에서 revalidateTag('posts')를 호출하는 것만으로 해당 그룹의 캐시를 즉시 무효화할 수 있습니다. 다음은 웹훅 수신 엔드포인트의 간단한 예시입니다.
// app/api/revalidate/route.ts — CMS 웹훅 수신 엔드포인트
import { revalidateTag } from 'next/cache'
export async function POST(req: Request) {
const { tag } = await req.json() // 예: { tag: 'posts' }
revalidateTag(tag)
return Response.json({ revalidated: true })
}예시 3: Vercel 엣지 캐시와 서버 캐시 계층 조합
이번에는 Route Handler에서 Vercel-CDN-Cache-Control 헤더를 함께 설정해 Vercel 엣지 CDN 캐시와 Next.js 서버 캐시를 동시에 활용하는 방법을 살펴봅니다. Route Handler는 Server Component가 아니라 외부 클라이언트(모바일 앱, 다른 서비스)가 데이터를 직접 소비하거나, 응답 헤더를 직접 제어해야 할 때 적합합니다.
// app/api/products/route.ts
import { cacheLife, cacheTag } from 'next/cache'
async function getProducts() {
'use cache'
cacheLife('inventory') // 서버 캐시: 5분 주기로 백그라운드 갱신
cacheTag('products')
// DB 또는 외부 API에서 상품 데이터를 가져오는 실제 로직
return await db.products.findMany({ where: { active: true } })
}
export async function GET() {
const data = await getProducts()
return new Response(JSON.stringify(data), {
headers: {
// 브라우저: 1분 캐시
'Cache-Control': 'public, max-age=60',
// 모든 CDN: 5분 캐시, 이후 10분간 stale 허용
'CDN-Cache-Control': 's-maxage=300, stale-while-revalidate=600',
// Vercel 엣지 전용: 서버 캐시 revalidate(300)와 값을 맞춰 불일치 방지
'Vercel-CDN-Cache-Control': 's-maxage=300, stale-while-revalidate=600',
},
})
}각 헤더가 어느 레이어에 영향을 미치는지 정리하면 다음과 같습니다.
| 헤더 | 적용 레이어 | 우선순위 |
|---|---|---|
Cache-Control |
브라우저 | 낮음 |
CDN-Cache-Control |
모든 CDN (Vercel 포함) | 중간 |
Vercel-CDN-Cache-Control |
Vercel 엣지 전용 | 최우선 |
설계 시 주의:
Vercel-CDN-Cache-Control의s-maxage는 서버 캐시의revalidate값과 일치시키거나 더 작게 설정하는 것을 권장합니다. 예를 들어 엣지를 1시간(s-maxage=3600)으로 설정하고 서버 캐시는 5분(revalidate: 300)으로 설정하면, 서버 데이터는 5분마다 갱신되지만 엣지는 1시간 동안 이전 응답을 그대로 반환합니다. 재고·가격처럼 신선도가 중요한 데이터에서는 이 불일치가 비즈니스 손실로 이어질 수 있습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 세밀한 제어 | 컴포넌트·함수 단위로 캐시 정책을 분리할 수 있어 과도한 캐시와 캐시 부재를 동시에 방지 |
| DX 향상 | 설정이 next.config.ts에 집중되어 있어 프로파일 이름만으로 의도를 명확히 표현 가능 |
| 계층 분리 | 브라우저 / Vercel Edge / ISR 서버를 독립적으로 제어해 각 레이어 최적화 |
| stale-while-revalidate | 갱신 중에도 이전 캐시를 반환해 사용자가 항상 빠른 응답을 받을 수 있음 |
| 즉시 무효화 | cacheTag() + revalidateTag() 조합으로 시간 만료를 기다리지 않고 캐시를 즉시 갱신 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
expire > revalidate 강제 |
순서가 잘못되면 빌드 에러 발생 | expire = revalidate × N 공식 적용 |
| 최소 stale 30초 강제 | stale: 0으로 설정해도 클라이언트 라우터가 30초 미만으로 내려가지 않음 |
즉각 반영이 필요한 데이터는 캐시 대상에서 제외 |
| 부작용 주의 | use cache는 반환값만 캐시하며 함수 실행 자체는 캐시하지 않음 |
로그 기록, 이벤트 발행 등 side effect는 캐시 바깥에서 처리 |
| 개인화 데이터 금지 | 엣지 캐시에 사용자 데이터가 혼입될 경우 정보 유출 위험 | 세션·사용자 ID에 의존하는 컴포넌트는 use cache 적용 제외 |
| Vercel 전용 헤더 | Vercel-CDN-Cache-Control은 Vercel 플랫폼에서만 동작 |
멀티 플랫폼 배포 시 CDN-Cache-Control로 대체 또는 조건부 적용 |
| 실험적 기능 | use cache는 Next.js 15.x 기준 아직 실험적 단계 |
프로덕션 적용 전 릴리스 노트 확인 필수 |
실무에서 가장 흔한 실수
- 개인화 데이터에
use cache를 적용하는 경우 — 로그인한 사용자의 장바구니, 프로필 정보 등은 반드시 캐시 대상에서 제외해야 합니다. 엣지 캐시가 A 사용자의 데이터를 B 사용자에게 반환하는 심각한 정보 유출 사고로 이어질 수 있습니다. - 엣지 캐시 TTL을 서버 캐시
revalidate보다 훨씬 길게 설정하는 경우 — 서버 측에서 데이터를 갱신해도 엣지가 오래된 응답을 계속 반환합니다. 두 값을 맞추거나 엣지 TTL을 더 짧게 설정하는 것을 권장합니다. revalidateTag()없이 시간 만료에만 의존하는 경우 — 콘텐츠 편집 후 즉각적인 반영이 필요한 서비스에서는 CMS 웹훅과revalidateTag()를 연동해야 합니다. 그렇지 않으면 편집한 내용이revalidate주기가 돌아올 때까지 반영되지 않습니다.
마치며
이 글을 시작할 때는 "재고 데이터를 얼마나 캐시해야 하는가"가 질문이었지만, 끝에서 보면 사실 더 근본적인 질문이었습니다. 캐시 정책은 기술 설정이 아니라 데이터의 비즈니스 성격을 코드로 표현하는 방식이며, cacheLife 프로파일과 Vercel 계층 캐싱 헤더는 그 표현을 체계적으로 가능하게 해주는 도구입니다.
지금 바로 시작해볼 수 있는 3단계를 안내합니다.
- 현재 서비스의 데이터를 성격별로 분류해보시면 좋습니다. 공지사항처럼 가끔 바뀌는 데이터는
editorial, 상품 가격·재고처럼 자주 바뀌는 데이터는inventory, 카테고리·국가 코드처럼 거의 바뀌지 않는 데이터는reference프로파일에 대응시켜next.config.ts에 정의해보는 것을 권장합니다. cacheComponents: true를 활성화한 뒤 가장 비용이 큰 Server Component 하나에'use cache'와cacheLife(프로파일명)을 선언해보시면 좋습니다. 어떤 컴포넌트가 얼마나 오래 캐시되는지 명확하게 파악할 수 있습니다.cacheTag()로 관련 컴포넌트를 그룹화하고, CMS 저장 훅이나 관리자 API에서revalidateTag()를 호출하도록 연결하는 것을 권장합니다. 시간 만료를 기다리지 않고도 즉각적인 콘텐츠 업데이트가 가능해집니다.
다음 글:
cacheTag()+revalidateTag()로 CMS 웹훅과 연동하는 온디맨드 캐시 무효화 전략 — 시간 기반 만료의 한계를 넘어 편집 즉시 반영하는 방법
참고 자료
개념 이해용
- use cache 디렉티브 | Next.js 공식 문서
- CDN Caching 가이드 | Next.js 공식 문서
- Cache-Control Headers | Vercel 공식 문서
- Cache Components for Instant and Fresh Pages | Vercel Academy
설정 레퍼런스
- cacheLife 함수 | Next.js 공식 문서
- next.config.js cacheLife 설정 | Next.js 공식 문서
- cacheComponents 설정 | Next.js 공식 문서
- cacheHandlers 설정 | Next.js 공식 문서
- CDN Cache | Vercel 공식 문서
심화 학습