App Router의 캐시/재검증은 어떻게 이해해야 하는가
Next.js App Router를 처음 쓰면 데이터 패칭 코드는 오히려 더 단순해 보입니다.
export default async function Page() {
const data = await fetch('https://api.example.com/posts').then((res) => res.json());
return <div>{data.length}</div>;
}문제는 그 다음입니다.
- 이 데이터는 캐시되는가
- 언제 다시 가져오는가
- 왜 수정했는데 바로 안 바뀌는가
revalidate와no-store는 무엇이 다른가revalidatePath,revalidateTag는 언제 써야 하는가
실무에서 App Router가 어렵게 느껴지는 이유는 라우팅보다 오히려 이 캐시 모델 때문인 경우가 많습니다.
특히 Pages Router의:
getServerSidePropsgetStaticProps- ISR
감각으로 접근하면 "지금 이 페이지가 정확히 어떤 상태인 거지?"라는 혼란이 쉽게 생깁니다.
이 글에서는 App Router의 캐시/재검증을 옵션 암기식으로 설명하지 않고, 실무에서 어떤 화면에 어떤 전략을 쓰는가 기준으로 정리해보겠습니다.
한눈에 보면
먼저 아주 짧게 정리하면 이렇습니다.
- 기본적으로 App Router에서는
fetch가 캐시와 연결됩니다 revalidate는 "이 데이터는 일정 시간 동안 재사용해도 된다"는 뜻에 가깝습니다cache: 'no-store'는 "매 요청마다 새로 가져와라"에 가깝습니다revalidatePath는 특정 경로를 다시 보게 만들 때 씁니다revalidateTag는 특정 데이터 그룹을 다시 보게 만들 때 씁니다
실무 감각으로 더 줄이면 이렇게 볼 수 있습니다.
| 상황 | 보통 선택 |
|---|---|
| 블로그 목록, 문서, 공개 페이지 | revalidate |
| 관리자 대시보드, 실시간성 높은 데이터 | cache: 'no-store' |
| 특정 페이지 하나만 새로고침하면 되는 경우 | revalidatePath |
| 여러 페이지에서 공유하는 같은 데이터 묶음 갱신 | revalidateTag |
먼저 무엇이 캐시되는지부터 이해해야 한다
App Router를 처음 보면 보통 "페이지가 캐시된다"라고 이해하기 쉽습니다. 틀린 말은 아니지만, 실무에서는 데이터 요청을 먼저 보는 편이 더 낫습니다.
예를 들어:
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 600 },
}).then((res) => res.json());
return <PostList posts={posts} />;
}이 코드는 단순히 페이지를 그리는 코드가 아니라, 아래 운영 정책을 같이 담고 있습니다.
- 이 목록 데이터는 10분 정도 재사용해도 된다
- 10분이 지나면 다시 검증할 수 있다
즉, App Router에서는 "렌더링 코드"와 "캐시 정책"이 가까이 붙습니다.
이 점이 장점이기도 하고, 처음엔 헷갈리는 이유이기도 합니다.
Pages Router 감각과 무엇이 다를까?
Pages Router에서는 보통 이렇게 생각했습니다.
getServerSideProps: 요청마다 새로 실행getStaticProps: 빌드 시 생성revalidate: 일정 주기 ISR
즉, 페이지 전체 렌더링 전략을 먼저 골랐습니다.
반면 App Router에서는 이 질문이 조금 바뀝니다.
- 이 데이터는 매번 최신이어야 하는가
- 일정 시간 재사용해도 되는가
- 어떤 단위로 다시 무효화할 것인가
즉, App Router는 페이지 전체보다 데이터 요청 단위 정책이 더 전면으로 올라옵니다.
가장 많이 쓰는 옵션 1: revalidate
어떤 의미인가?
revalidate는 이 데이터를 일정 시간 동안 재사용할 수 있다는 뜻에 가깝습니다.
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 600 },
}).then((res) => res.json());
return <PostList posts={posts} />;
}이 코드는 보통 이렇게 읽으면 됩니다.
- 블로그 목록은 10분 정도 stale해도 괜찮다
- 대신 매 요청마다 API를 다시 치고 싶지는 않다
즉, revalidate는 "실시간은 아니지만 너무 오래된 데이터도 싫다"는 상황에 잘 맞습니다.
어떤 화면에 잘 맞을까?
- 블로그 목록
- 문서 페이지
- 상품 목록
- 공개 마케팅 페이지
이런 화면은 보통 초 단위 최신성이 중요하지 않습니다. 그래서 revalidate가 가장 자연스럽습니다.
실무 예시
// app/articles/page.tsx
export default async function ArticlesPage() {
const articles = await fetch('https://api.example.com/articles', {
next: { revalidate: 300 },
}).then((res) => res.json());
return (
<ul>
{articles.map((article: { id: string; title: string }) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
);
}이 화면은 5분 정도 캐시해도 문제가 크지 않다면 꽤 좋은 선택입니다.
가장 많이 쓰는 옵션 2: cache: 'no-store'
어떤 의미인가?
no-store는 매 요청마다 새 데이터를 가져오겠다는 의미에 가깝습니다.
export default async function DashboardPage() {
const summary = await fetch('https://api.example.com/dashboard', {
cache: 'no-store',
}).then((res) => res.json());
return <DashboardSummary summary={summary} />;
}즉, "캐시보다 최신성이 중요하다"는 선언입니다.
어떤 화면에 잘 맞을까?
- 관리자 대시보드
- 주문 현황
- 재고 현황
- 사용자별 개인화 데이터
이런 화면은 예전 데이터가 보이면 오히려 문제가 됩니다. 그래서 no-store가 더 낫습니다.
실무에서 자주 하는 오해
no-store를 남발하면 가장 단순해 보입니다. 왜냐하면 "캐시 때문에 안 바뀌는 문제"가 줄기 때문입니다.
하지만 모든 요청에 no-store를 쓰면:
- 서버 부하가 커지고
- 응답 속도 이점이 줄고
- App Router의 캐시 장점을 거의 못 쓰게 됩니다
즉, no-store는 안전한 기본값이 아니라 최신성이 정말 중요한 경우의 선택지에 가깝습니다.
revalidate와 no-store를 어떻게 고를까?
이 둘 사이에서 가장 많이 고민합니다.
아래처럼 생각하면 비교적 단순해집니다.
revalidate가 맞는 경우
- 공개 데이터
- 조금 stale해도 큰 문제 없음
- API 비용이나 응답 속도 최적화가 중요함
no-store가 맞는 경우
- 사용자별 데이터
- 관리 화면
- 매 요청 최신성이 중요함
- stale 데이터가 실제 장애처럼 느껴짐
예를 들어 같은 서비스 안에서도 화면마다 다를 수 있습니다.
/blog -> revalidate: 600
/products -> revalidate: 300
/dashboard -> no-store
/admin/orders -> no-store즉, App Router에서는 "서비스 전체 전략 하나"보다 화면과 데이터 성격에 맞는 전략이 더 중요합니다.
revalidatePath는 언제 쓸까?
여기서부터는 "캐시된 데이터를 다시 보게 만드는 방법"입니다.
예를 들어 관리자 화면에서 게시글을 수정했다고 해보겠습니다.
- 게시글 상세 페이지도 갱신돼야 하고
- 게시글 목록 페이지도 갱신돼야 할 수 있습니다
특정 경로 하나를 다시 보게 하고 싶다면 revalidatePath를 씁니다.
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function updatePost(postId: string, formData: FormData) {
await fetch(`https://api.example.com/posts/${postId}`, {
method: 'PATCH',
body: JSON.stringify({
title: formData.get('title'),
}),
});
revalidatePath('/posts');
revalidatePath(`/posts/${postId}`);
}이 코드는 이렇게 읽으면 됩니다.
- 데이터 수정은 했고
/posts목록과/posts/[id]상세를 다시 보게 만든다
즉, revalidatePath는 경로 기준 무효화입니다.
어떤 상황에 잘 맞을까?
- 목록 페이지 하나
- 상세 페이지 하나
- 수정 후 돌아갈 특정 경로가 명확한 경우
즉, 영향 범위가 경로로 설명될 때 가장 직관적입니다.
revalidateTag는 언제 쓸까?
revalidatePath가 경로 기준이라면, revalidateTag는 데이터 그룹 기준입니다.
예를 들어 같은 상품 데이터가 여러 화면에서 쓰인다고 해보겠습니다.
- 메인 추천 상품
- 카테고리 목록
- 상품 상세
이런 경우 경로를 하나씩 다 적는 것은 불편합니다. 대신 태그를 붙여서 관리할 수 있습니다.
// app/products/page.tsx
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products', {
next: { tags: ['products'], revalidate: 300 },
}).then((res) => res.json());
return <ProductList products={products} />;
}// app/page.tsx
export default async function HomePage() {
const featuredProducts = await fetch('https://api.example.com/products/featured', {
next: { tags: ['products'], revalidate: 300 },
}).then((res) => res.json());
return <FeaturedProducts items={featuredProducts} />;
}이제 상품 수정 이후에는:
'use server';
import { revalidateTag } from 'next/cache';
export async function updateProduct(productId: string, formData: FormData) {
await fetch(`https://api.example.com/products/${productId}`, {
method: 'PATCH',
body: JSON.stringify({
name: formData.get('name'),
}),
});
revalidateTag('products');
}이렇게 할 수 있습니다.
즉, revalidateTag는 "이 데이터 그룹을 쓰는 화면 전체를 다시 보게 하자"는 발상입니다.
어떤 상황에 잘 맞을까?
- 같은 데이터가 여러 페이지에서 재사용될 때
- 영향 범위를 경로보다 데이터 단위로 설명하는 편이 자연스러울 때
- 상품, 게시글, 카테고리처럼 공통 데이터 그룹이 있을 때
revalidatePath와 revalidateTag는 무엇이 다를까?
이 둘은 자주 헷갈립니다.
revalidatePath
- 경로 기준
- 특정 페이지나 레이아웃 갱신에 직관적
- 수정 이후 돌아갈 화면이 명확할 때 좋음
revalidateTag
- 데이터 기준
- 여러 페이지에서 공유하는 데이터를 한 번에 갱신하기 좋음
- 재사용 범위가 넓을수록 유리함
아래처럼 보면 간단합니다.
게시글 수정 후 /posts, /posts/123만 다시 보면 됨
-> revalidatePath
상품 데이터가 홈, 목록, 상세, 추천 영역에 모두 퍼져 있음
-> revalidateTag즉, 경로로 설명되면 revalidatePath, 데이터 묶음으로 설명되면 revalidateTag가 더 자연스럽습니다.
실무 예제로 한 번에 묶어보자
예시 1. 블로그
블로그 목록은 약간 stale해도 괜찮습니다.
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 600, tags: ['posts'] },
}).then((res) => res.json());
return <PostList posts={posts} />;
}게시글 발행 후에는:
'use server';
import { revalidateTag } from 'next/cache';
export async function publishPost(postId: string) {
await fetch(`https://api.example.com/posts/${postId}/publish`, {
method: 'POST',
});
revalidateTag('posts');
}즉, 블로그처럼 여러 화면이 같은 데이터를 공유하는 경우 태그가 잘 맞습니다.
예시 2. 관리자 주문 대시보드
주문 현황은 stale하면 곤란합니다.
export default async function AdminOrdersPage() {
const orders = await fetch('https://api.example.com/admin/orders', {
cache: 'no-store',
}).then((res) => res.json());
return <OrderTable orders={orders} />;
}이 화면은 굳이 재검증보다 최신 데이터를 바로 보는 편이 낫습니다.
예시 3. 상품 관리
상품은 목록, 홈 추천, 상세에서 동시에 쓰입니다.
export async function getProducts() {
return fetch('https://api.example.com/products', {
next: { tags: ['products'], revalidate: 300 },
}).then((res) => res.json());
}수정 후에는:
'use server';
import { revalidateTag } from 'next/cache';
export async function saveProduct(productId: string, payload: { name: string }) {
await fetch(`https://api.example.com/products/${productId}`, {
method: 'PATCH',
body: JSON.stringify(payload),
});
revalidateTag('products');
}이 경우는 경로보다 태그가 훨씬 관리하기 쉽습니다.
자주 하는 실수
1. 모든 것을 no-store로 두는 것
처음에는 가장 덜 헷갈립니다. 하지만 결국 캐시 이점을 거의 다 버리게 됩니다.
2. 반대로 모든 것을 revalidate로 두는 것
관리자 화면이나 사용자 개인 데이터까지 이렇게 두면 stale 데이터로 인한 혼란이 커집니다.
3. 경로 갱신이 필요한데 태그를 안 붙이는 것
수정 후 "왜 홈은 바뀌었는데 목록은 안 바뀌지?" 같은 문제가 생기기 쉽습니다.
4. 영향 범위를 생각하지 않고 revalidatePath만 반복하는 것
경로가 많아지면 유지보수가 어려워집니다. 이럴 때는 태그가 더 낫습니다.
5. 캐시 정책을 화면이 아니라 API 기준으로만 생각하는 것
같은 API를 쓰더라도 화면마다 기대하는 최신성은 다를 수 있습니다.
즉, 기준은 API 자체보다 이 데이터를 어디서 어떻게 보여주는가에 더 가깝습니다.
어떻게 판단하면 좋을까?
실무에서는 아래 순서로 보면 가장 덜 헷갈립니다.
- 이 데이터가 stale해도 되는 시간을 먼저 정한다
- stale하면 안 되면
no-store - 조금 stale해도 되면
revalidate - 수정 후 특정 경로만 다시 보면 되면
revalidatePath - 여러 화면에서 공유하는 데이터면
revalidateTag
즉, 옵션을 외우는 것보다 데이터의 최신성 요구사항과 영향 범위를 먼저 정하는 편이 훨씬 낫습니다.
정리하면
App Router의 캐시/재검증을 한 줄로 줄이면 이렇습니다.
페이지를 어떻게 렌더링할지보다, 데이터를 얼마나 오래 믿을 수 있고 언제 다시 무효화할지를 코드 가까이에서 정하는 방식입니다.
그래서 핵심은 옵션 이름이 아니라 아래 질문입니다.
- 이 데이터는 얼마나 최신이어야 하는가
- stale하면 실제로 문제가 되는가
- 수정 이후 어떤 화면이 같이 갱신돼야 하는가
- 그 범위는 경로 기준인가, 데이터 그룹 기준인가
이 질문에 답할 수 있으면:
revalidateno-storerevalidatePathrevalidateTag
는 훨씬 덜 헷갈립니다.
