Ch 03. 태그로 캐시 지우기 — revalidateTag와 revalidatePath
마트의 유통기한 스티커
마트에서 식품에 유통기한 스티커를 붙이는 것처럼, Next.js의 fetch에도 태그를 붙일 수 있습니다. 나중에 특정 태그가 붙은 캐시를 한 번에 무효화할 수 있습니다.
새 게시글이 작성되었을 때, "posts" 태그가 붙은 모든 캐시를 즉시 지울 수 있습니다. 다음 요청부터는 새 데이터를 가져옵니다.
fetch 시 태그 지정
next: { tags: [...] } 옵션으로 태그를 붙입니다.
// 파일: app/posts/page.tsx
interface Post {
id: number;
title: string;
excerpt: string;
}
async function getPosts(): Promise<Post[]> {
const res = await fetch('https://api.example.com/posts', {
next: {
revalidate: 3600,
tags: ['posts'], // "posts" 태그를 붙임
},
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>블로그</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
```text
게시글 상세 페이지도 태그를 붙입니다.
```tsx
// 파일: app/posts/[slug]/page.tsx
interface Props {
params: Promise<{ slug: string }>;
}
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
revalidate: 3600,
tags: ['posts', `post-${slug}`], // 여러 태그 지정 가능
},
});
return res.json();
}
export default async function PostPage({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
```text
### `revalidateTag`로 캐시 무효화
Server Action에서 `revalidateTag`를 호출하면 해당 태그의 캐시가 즉시 무효화됩니다.
```ts
// 파일: app/actions/post.ts
'use server';
import { revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
const res = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
const post = await res.json();
// "posts" 태그가 붙은 모든 캐시를 무효화
revalidateTag('posts');
redirect(`/posts/${post.slug}`);
}
export async function updatePost(slug: string, formData: FormData) {
const title = formData.get('title') as string;
await fetch(`https://api.example.com/posts/${slug}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
// 특정 게시글과 전체 목록 캐시 무효화
revalidateTag(`post-${slug}`);
revalidateTag('posts');
}
```text
### `revalidatePath`로 특정 경로 캐시 초기화
태그 대신 경로로 캐시를 무효화할 수도 있습니다.
```ts
// 파일: app/actions/post.ts (이전 코드에서 확장)
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function deletePost(slug: string) {
await fetch(`https://api.example.com/posts/${slug}`, {
method: 'DELETE',
});
// /posts 경로의 캐시를 무효화
revalidatePath('/posts');
redirect('/posts');
}
```text
`revalidatePath`는 해당 경로와 그 하위 경로의 캐시를 무효화합니다. `revalidatePath('/posts', 'layout')`처럼 두 번째 인자를 주면 해당 레이아웃부터 아래를 모두 무효화합니다.
### 게시글 작성 후 목록 즉시 갱신하기
전체 흐름을 보여주는 예시입니다.
```tsx
// 파일: app/posts/new/page.tsx
import { createPost } from '@/actions/post';
export default function NewPostPage() {
return (
<div>
<h1>새 글 작성</h1>
<form action={createPost}>
<input name="title" placeholder="제목" required />
<textarea name="content" placeholder="내용" required />
<button type="submit">게시</button>
</form>
</div>
);
}
폼 제출 → createPost Server Action 실행 → 새 게시글 생성 → revalidateTag('posts') 호출 → 목록 페이지 캐시 무효화 → redirect('/posts') → 게시글 목록 페이지에서 새 데이터 가져옴.
이 흐름이 Next.js의 데이터 뮤테이션 패턴입니다.
다음 챕터에서는
다음 챕터에서는 Server Actions를 더 깊이 살펴봅니다. useActionState 훅으로 서버 응답을 받고, 유효성 검사와 에러 처리를 어떻게 하는지 알아봅니다.