Next.js 16 캐싱 설명: 재검증, 태그, 드래프트 모드 및 실제 프로덕션 패턴
Source: Dev.to
Next.js 16 캐싱 설명: 재검증, 태그, Draft Mode, 실제 프로덕션 패턴
Next.js 16에서는 데이터 캐싱과 재검증을 다루는 새로운 API와 개념이 도입되었습니다. 이 글에서는 revalidation, tags, draft mode 그리고 실제 프로젝트에서 사용할 수 있는 패턴들을 자세히 살펴보겠습니다.
목차
왜 캐싱이 중요한가?
- 성능: 서버에서 데이터를 매번 가져오는 대신, 이미 캐시된 데이터를 재사용함으로써 응답 시간을 크게 단축할 수 있습니다.
- 비용 절감: 외부 API 호출을 최소화하면 비용을 절감하고, 레이트 제한(rate limits) 문제도 피할 수 있습니다.
- UX: 사용자는 더 빠른 페이지 로딩을 경험하게 되며, 이는 전환율과 SEO에 긍정적인 영향을 줍니다.
Revalidation (재검증)
기본 개념
revalidate 옵션은 ISR (Incremental Static Regeneration) 과 비슷하지만, Next.js 16에서는 fetch와 cache API를 통해 더 직관적으로 사용할 수 있습니다.
// 예시: 페이지 컴포넌트에서 fetch 사용
export default async function Page() {
const data = await fetch('/api/posts', {
// 60초마다 백그라운드에서 재검증
next: { revalidate: 60 }
}).then(res => res.json());
return <PostsList posts={data} />;
}
revalidate: 60→ 60초가 지나면 백그라운드에서 새 데이터를 가져와 캐시를 업데이트합니다.- 사용자는 stale(구버전) 데이터를 즉시 보게 되며, 새 데이터는 다음 요청에 반영됩니다.
강제 재검증
특정 상황(예: CMS에서 콘텐츠가 업데이트된 경우)에는 강제 재검증이 필요합니다.
// 서버 액션 또는 API 라우트에서
export async function POST(request: Request) {
// 데이터베이스 업데이트 로직 ...
// 캐시 무효화
await revalidatePath('/posts');
return new Response('OK');
}
revalidatePath는 지정된 경로의 캐시를 즉시 무효화합니다.revalidateTag와 조합하면 더 세밀한 제어가 가능합니다.
Tags (태그)
태그란?
태그는 데이터 의존성을 선언하는 메커니즘입니다. 여러 페이지가 같은 데이터를 공유할 때, 해당 데이터가 변경되면 연관된 모든 페이지를 동시에 무효화할 수 있습니다.
사용 예시
// posts/[id]/page.tsx
export default async function Post({ params }) {
const post = await fetch(`/api/posts/${params.id}`, {
next: { tags: ['post'] } // 이 페이지는 'post' 태그에 의존
}).then(res => res.json());
return <PostDetail post={post} />;
}
// comments/[postId]/page.tsx
export default async function Comments({ params }) {
const comments = await fetch(`/api/comments?postId=${params.postId}`, {
next: { tags: ['comment'] } // 이 페이지는 'comment' 태그에 의존
}).then(res => res.json());
return <CommentsList comments={comments} />;
}
태그 무효화
// API 라우트: 댓글이 추가될 때
export async function POST(request: Request) {
// 댓글 저장 로직 ...
// 'comment' 태그에 연결된 모든 캐시 무효화
await revalidateTag('comment');
return new Response('Comment added');
}
revalidateTag('comment')를 호출하면 댓글과 연관된 모든 페이지가 재검증됩니다.- 이렇게 하면 데이터 일관성을 손쉽게 유지할 수 있습니다.
Draft Mode (드래프트 모드)
개념
Draft Mode는 프리뷰(preview) 환경을 제공하여, 아직 퍼블리시되지 않은 콘텐츠를 실시간으로 확인할 수 있게 해줍니다. 기존 next/preview와 달리, 서버 액션과 fetch를 그대로 사용할 수 있습니다.
활성화 / 비활성화
// app/api/draft/activate/route.ts
import { draftMode } from 'next/headers';
export async function GET() {
draftMode().enable(); // Draft Mode 켜기
return new Response('Draft mode enabled');
}
// app/api/draft/deactivate/route.ts
import { draftMode } from 'next/headers';
export async function GET() {
draftMode().disable(); // Draft Mode 끄기
return new Response('Draft mode disabled');
}
Draft Mode와 캐시
Draft Mode가 활성화된 요청은 캐시를 건너뛰고 최신 데이터를 직접 조회합니다.
export default async function Page() {
const data = await fetch('/api/posts', {
// Draft Mode가 켜져 있으면 캐시 무시
next: { revalidate: 0 }
}).then(res => res.json());
return <PostsList posts={data} />;
}
revalidate: 0은 no‑cache를 의미합니다.- 일반 사용자에게는 캐시된 버전이 제공되고, 프리뷰 사용자에게는 최신(드래프트) 버전이 제공됩니다.
실제 프로덕션 패턴
1️⃣ 데이터 레이어와 캐시 전략 분리
- API 라우트: 모든 데이터 로직을 여기서 처리하고,
revalidateTag/revalidatePath를 사용해 캐시를 관리합니다. - 페이지/컴포넌트:
fetch(..., { next: { tags, revalidate } })만 선언해 의존성을 명시합니다.
2️⃣ 태그 기반 무효화 + 재검증 조합
// 게시글 업데이트 API
export async function PUT(request: Request) {
// DB 업데이트 ...
// 게시글과 댓글 모두 무효화
await Promise.all([
revalidateTag('post'), // 게시글 페이지
revalidateTag('comment') // 댓글 리스트
]);
return new Response('Post updated');
}
- 핵심: 하나의 업데이트가 여러 캐시 엔트리를 동시에 무효화하도록 설계합니다.
3️⃣ ISR + Draft Mode 혼합
// app/posts/[slug]/page.tsx
export default async function Post({ params }) {
const isDraft = draftMode().isEnabled;
const data = await fetch(`/api/posts/${params.slug}`, {
next: {
// Draft Mode이면 캐시 안 함, 아니면 30초마다 재검증
revalidate: isDraft ? 0 : 30,
tags: ['post']
}
}).then(res => res.json());
return <PostDetail post={data} />;
}
- 일반 방문자는 ISR(30초)으로 빠른 응답을 받고, 에디터는 실시간으로 최신 초안을 확인합니다.
4️⃣ Edge Middleware와 캐시 헤더
Edge Middleware에서 Cache-Control 헤더를 직접 조작해 CDN 레벨 캐시 정책을 세밀하게 제어할 수 있습니다.
// middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request) {
const response = NextResponse.next();
// API 경로는 5초 동안 CDN에 캐시
if (request.nextUrl.pathname.startsWith('/api/')) {
response.headers.set('Cache-Control', 'public, max-age=5, stale-while-revalidate=30');
}
return response;
}
- 이렇게 하면 서버와 CDN 양쪽에서 캐시를 최적화할 수 있습니다.
마무리
Next.js 16의 캐싱 시스템은 재검증, 태그, Draft Mode라는 세 가지 핵심 요소를 중심으로 설계되었습니다.
| 기능 | 언제 사용하나요? | 주요 API |
|---|---|---|
| Revalidation | 일정 주기로 데이터를 최신 상태로 유지하고 싶을 때 | fetch(..., { next: { revalidate } }), revalidatePath |
| Tags | 여러 페이지가 동일 데이터를 공유하고, 해당 데이터가 바뀔 때 전체를 무효화하고 싶을 때 | next: { tags }, revalidateTag |
| Draft Mode | 아직 퍼블리시되지 않은 콘텐츠를 프리뷰하고 싶을 때 | draftMode().enable()/disable(), draftMode().isEnabled |
이 세 가지를 조합하면 성능과 데이터 일관성을 동시에 만족하는 강력한 프로덕션 패턴을 만들 수 있습니다. 실제 프로젝트에 적용해 보면서, 캐시 무효화 전략을 점진적으로 다듬어 가길 권장합니다.
Tip: 캐시 전략을 설계할 때는 데이터 변경 빈도, 사용자 트래픽 패턴, 외부 API 비용을 모두 고려하세요. 적절한
revalidate간격과 태그 설계가 장기적인 유지보수 비용을 크게 낮춰줍니다.
🎯 우리가 만들고 있는 것
- 정적 + 동적 제어를
fetch로 사용 - 태그 기반 무효화
- 요청 시 재검증
- 미리보기 워크플로우를 위한 초안 모드
- 제가 실제로 사용하는 실전 패턴
추측은 없습니다. 실수로 오래된 페이지가 없습니다.
🧠 첫 번째: 새로운 사고 모델
Next.js 16에서는 캐싱이 더 이상 “페이지 기반”이 아닙니다.
이제는 데이터 기반입니다. 캐싱 단위는 이제 fetch() 호출이 됩니다.
- 각
fetch는 캐시되거나 동적으로 동작할 수 있습니다. - 각
fetch는 재검증 규칙을 정의할 수 있습니다. - 각
fetch는 태그를 통해 무효화될 수 있습니다.
이렇게 하면 더 깔끔하고 확장성이 높아집니다.
🔥 1. fetch 로 캐시 제어하기
기본 동작
const res = await fetch("https://api.example.com/posts");
기본적으로 이 응답은 프로덕션 환경에서 캐시됩니다.
재검증이 있는 정적 (ISR‑스타일)
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 }
});
- 이 응답을 캐시합니다.
- 매 60 초마다 재검증합니다.
이전 ISR 패턴을 보다 세밀한 단위로 대체합니다.
완전 동적 (캐시 없음)
const res = await fetch("https://api.example.com/posts", {
cache: "no-store"
});
동적 렌더링을 강제합니다. 다음과 같은 경우에 사용합니다:
- 사용자별 데이터
- 인증된 대시보드
- 빠르게 변하는 메트릭
🏷️ 2. 실제 업그레이드: 캐시 태그
Next.js 16에서는 캐시된 fetch에 태그를 지정할 수 있습니다:
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] }
});
이제 캐시 엔트리는 "posts" 태그와 연결되어 수동 무효화를 가능하게 합니다.
🚀 3. 태그를 이용한 온‑디맨드 재검증
태그를 재검증하는 라우트 핸들러를 생성합니다:
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
export async function POST() {
revalidateTag("posts");
return Response.json({ revalidated: true });
}
이 엔드포인트를 호출하면 "posts" 태그가 붙은 모든 캐시된 fetch가 즉시 무효화됩니다—정밀하게, 페이지 수준이 아니라, 전역이 아닙니다.
🧪 4. 재검증 + 태그 결합 (최고 패턴)
const res = await fetch("https://api.example.com/posts", {
next: {
revalidate: 3600,
tags: ["posts"]
}
});
- 자동 시간당 새로 고침
- 필요 시 수동 무효화
- 불필요한 재빌드 없음
📝 5. 미리보기 워크플로우를 위한 Draft Mode
Draft Mode 활성화
import { draftMode } from "next/headers";
export async function GET() {
draftMode().enable();
return Response.redirect("/admin");
}
페이지에서 Draft Mode 사용
import { draftMode } from "next/headers";
export default async function Page() {
const { isEnabled } = draftMode();
const res = await fetch("https://api.example.com/posts", {
cache: isEnabled ? "no-store" : "force-cache"
});
const data = await res.json();
return { data.title };
}
Draft Mode가 활성화되면:
- 캐시가 우회됩니다
- 미발행된 변경 사항이 표시됩니다
비활성화되면 일반 캐시가 재개됩니다.
⚙️ 6. 실제로 사용하는 프로덕션 패턴
| 사용 사례 | 캐시 구성 |
|---|---|
| 공개 콘텐츠 | next: { revalidate: 600, tags: ["posts"] } |
| 관리자 업데이트 | revalidateTag("posts") |
| 사용자 대시보드 | cache: "no-store" |
| 프리뷰 라우트 | draftMode + no-store |
Result: 성능, 신선도, 정밀도, 확장성.
⚠️ 내가 저지른 흔한 실수
cache: "no-store"와revalidate를 혼용하기- 태그를 빼먹고 전체 경로를 재검증하려 시도하기
- 개발 모드가 프로덕션 캐싱을 반영한다고 가정하기
- 과도하게 무효화하기
Tip: 개발 모드는 다르게 동작합니다. 항상 프로덕션 빌드에서 캐싱을 테스트하세요:
next build
next start
🧩 이것이 모든 것을 바꾸는 방법
Next.js 16 이전에는 캐싱이 페이지 기반이고 간접적으로 느껴졌습니다.
이제는:
- 선언적
- 세분화된
- 완전 제어 가능
페이지 ISR에서 fetch‑level 캐싱으로의 전환은 주요 아키텍처 개선입니다.
🏁 최종 생각
Next.js 16은 단순히 캐싱을 개선하는 것이 아니라 예측 가능하게 만듭니다.
다음 항목을 이해한다면:
fetch캐시 제어revalidatetagsrevalidateTag()draftMode()
성능을 추측하는 것이 아니라 직접 제어할 수 있습니다.
이 내용이 도움이 되었다면, 오래된 데이터와 싸우는 다른 프론트엔드 엔지니어와 공유해 주세요.
Next.js 16에서 만든 흥미로운 캐싱 패턴이 있다면 보고 싶습니다.
추가 심층 분석이 곧 찾아옵니다.
Check me out at .