iBetter Books
수정

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 훅으로 서버 응답을 받고, 유효성 검사와 에러 처리를 어떻게 하는지 알아봅니다.