시간 기반 및 주문형 재검증 전략
정밀하게 최신 상태를 제어하도록 재검증 간격을 revalidatePath 및 revalidateTag와 결합하는 방법을 배웁니다.
시간 기반 및 주문형 재검증 전략은(는) CoddyKit의 무료 Next.js 15 Fullstack (App Router + Server Actions) 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Next.js 15 Fullstack (App Router + Server Actions) 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Next.js 15 Fullstack (App Router + Server Actions) 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Two Axes of Freshness
In the App Router, a cached route or fetch can be refreshed along two independent axes:
- Time-based revalidation: data goes stale after a fixed number of seconds (
revalidate). - On-demand revalidation: you explicitly invalidate cache when something actually changes, using
revalidatePathorrevalidateTag.
Real apps combine both. Time-based gives a safety net so content never gets too old; on-demand gives instant updates when you control the write. This lesson shows how to wire them together for precise freshness control.
Time-Based with the Segment Config
The simplest time-based strategy is the route segment revalidate export. Next.js will serve the cached render until the interval elapses, then regenerate it in the background (stale-while-revalidate).
A value of 3600 means "at most one hour old". Setting 0 opts out of caching for the segment; false caches indefinitely.
// app/products/page.tsx
// Revalidate this Server Component page at most once per hour
export const revalidate = 3600;
export default async function ProductsPage() {
const res = await fetch('https://api.example.com/products');
const products: { id: string; name: string }[] = await res.json();
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}Per-Fetch revalidate
You do not have to revalidate a whole segment. Each fetch can carry its own time-based policy through the next.revalidate option.
This lets one page mix a fast-changing resource with a slow-changing one. The segment's effective revalidate becomes the lowest value among its fetches and its segment config.
// app/dashboard/page.tsx
export default async function Dashboard() {
// Prices change often: refresh every 60s
const prices = await fetch('https://api.example.com/prices', {
next: { revalidate: 60 },
}).then((r) => r.json());
// Categories rarely change: refresh once a day
const categories = await fetch('https://api.example.com/categories', {
next: { revalidate: 86400 },
}).then((r) => r.json());
return <Report prices={prices} categories={categories} />;
}Why Time Alone Is Not Enough
Time-based revalidation has a structural weakness: it cannot tell whether anything actually changed. With revalidate = 3600:
- A user who edits a product still sees the old version for up to an hour.
- If nothing changed, you regenerate anyway, wasting compute.
The fix is to pair a generous time interval (the safety net) with on-demand invalidation triggered by your own writes. Time keeps data from going truly stale; on-demand makes intentional edits feel instant.
revalidatePath: Invalidate by Route
revalidatePath purges the cache for a specific route so the next request re-renders it. Call it from a Server Action or Route Handler after a write.
- Pass an exact path like
'/products'. - Pass a dynamic template plus
'page'to target one dynamic route:revalidatePath('/products/[id]', 'page'). - Pass
'layout'to invalidate a layout and everything nested under it.
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export async function updateProduct(id: string, name: string) {
await db.product.update({ where: { id }, data: { name } });
// Refresh the list and this product's detail page
revalidatePath('/products');
revalidatePath('/products/[id]', 'page');
}revalidateTag: Invalidate by Data
revalidatePath is route-centric, but the same data can appear on many routes. revalidateTag is data-centric: you tag fetches, then invalidate every cached entry sharing that tag, no matter where it is rendered.
First, attach tags when fetching:
// lib/products.ts
export async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { tags: ['products'], revalidate: 3600 },
});
return res.json();
}
export async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { tags: ['products', `product:${id}`] },
});
return res.json();
}Triggering revalidateTag from a Write
With tags in place, one call invalidates everything tagged. A coarse tag like 'products' refreshes lists, grids, and detail pages at once; a granular tag like product:42 refreshes only that item's cached entries.
Use both: invalidate the granular tag for the edited row, plus the collection tag for any aggregate views.
'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
export async function editProduct(id: string, name: string) {
await db.product.update({ where: { id }, data: { name } });
revalidateTag(`product:${id}`); // this item's detail entries
revalidateTag('products'); // any list/grid that shows it
}Path vs Tag: Choosing
How to decide between the two on-demand tools:
- Use revalidatePath when you know exactly which route(s) changed and the mapping is simple, e.g. a CMS page slug.
- Use revalidateTag when one piece of data fans out across many routes, or when routes are hard to enumerate (search pages, related-items widgets, sitemaps).
They are not exclusive. A common pattern: tag the underlying fetches for data fan-out, and additionally call revalidatePath for a high-traffic landing page you want refreshed immediately.
Combining Time + On-Demand
The production recipe is layered:
- Set a generous time interval on tagged fetches (
revalidate: 3600) as a backstop for changes you do not control. - Call revalidateTag in every Server Action that writes that data for instant updates you do control.
Result: edits appear immediately, while external drift can never exceed one hour. This is the sweet spot for "precise freshness control".
'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
// Fetch elsewhere uses: next: { tags: ['orders'], revalidate: 3600 }
export async function createOrder(input: { sku: string; qty: number }) {
const order = await db.order.create({ data: input });
revalidateTag('orders'); // instant; time interval is the backstop
return order.id;
}On-Demand via a Webhook Route Handler
When data changes outside your app (a headless CMS publish, an upstream sync), trigger revalidation from a Route Handler that the external system calls. Protect it with a secret so it cannot be abused.
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
export async function POST(req: NextRequest) {
const secret = req.nextUrl.searchParams.get('secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
}
const { tag } = await req.json();
revalidateTag(tag);
return NextResponse.json({ revalidated: true, now: Date.now() });
}Gotchas to Remember
A few behaviors that trip people up:
revalidatePath/revalidateTagonly mark the cache as stale; regeneration happens on the next request, not instantly during your action.- They must run on the server (Server Action or Route Handler), never in a Client Component.
- A
fetchwithcache: 'no-store'orrevalidate: 0is never cached, so tags on it have nothing to invalidate. - Tags attach to
fetchdata caching; pick stable, predictable tag names so writers and readers agree.
Quick Check
Pick the most precise strategy for the scenario below.
Recap
You combined the two freshness axes for precise control:
- Time-based: segment
export const revalidateor per-fetchnext: { revalidate }as a backstop against uncontrolled drift. - On-demand by route:
revalidatePathwhen you know exactly which route changed. - On-demand by data:
revalidateTagwhen one resource fans out across many routes.
The production pattern is a generous time interval on tagged fetches plus revalidateTag in every write Server Action (and a secret-protected webhook for external changes). Edits feel instant; staleness is bounded.
AI 튜터와 함께 TypeScript을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 22
- 레슨
- 88
자주 묻는 질문
“시간 기반 및 주문형 재검증 전략” 강의는 무료인가요?
네 — “시간 기반 및 주문형 재검증 전략” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Next.js 15 Fullstack (App Router + Server Actions) 강의 전체를 잠금 해제할 수 있습니다. Next.js 15 Fullstack (App Router + Server Actions) 강의에는 총 4개의 강의가 포함되어 있습니다.
“시간 기반 및 주문형 재검증 전략”에서 뭘 배우나요?
정밀하게 최신 상태를 제어하도록 재검증 간격을 revalidatePath 및 revalidateTag와 결합하는 방법을 배웁니다. 브라우저에서 직접 실행하는 실습 코드로 Next.js 15 Fullstack (App Router + Server Actions)을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Next.js 15 Fullstack (App Router + Server Actions)을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Next.js 15 Fullstack (App Router + Server Actions)은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“시간 기반 및 주문형 재검증 전략” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Next.js 15 Fullstack (App Router + Server Actions) 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Next.js 15 Fullstack (App Router + Server Actions) 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.