[๐Ÿค–] Next.js App Router ์บ์‹ฑ ์ „๋žต: ๋ฐ์ดํ„ฐ ์žฌ๊ฒ€์ฆ (revalidatePath, revalidateTag) ์™„๋ฒฝ ๊ฐ€์ด๋“œ

Next.js 14 App Router์—์„œ ํšจ์œจ์ ์ธ ๋ฐ์ดํ„ฐ ์บ์‹ฑ ์ „๋žต๊ณผ revalidatePath, revalidateTag๋ฅผ ์ด์šฉํ•œ ๋ฐ์ดํ„ฐ ์žฌ๊ฒ€์ฆ ๋ฐฉ๋ฒ•์„ ์‹ค๋ฌด ์˜ˆ์‹œ์™€ ํ•จ๊ป˜ ์ž์„ธํžˆ ์•Œ์•„๋ณด๊ณ  ์›น ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ๋ณด์„ธ์š”.

16๋ถ„
๋‹จ์–ด: 1,512๊ฐœ
๊ฒŒ์‹œ๊ธ€ ์ธ๋„ค์ผ
์ •๋ณด

๐Ÿค– ์ด ํฌ์ŠคํŒ…์€ Gemini 2.5 Flash AI๊ฐ€ ์ž‘์„ฑํ–ˆ์–ด์š”. ๋‚ด์šฉ์˜ ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณค์ง€๋งŒ, ์‹ค๋ฌด ์ ์šฉ ์ „ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ•จ๊ป˜ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

์œ ์šฉํ•œ ํŒ

Next.js 14 App Router์—์„œ ๋ฐ์ดํ„ฐ ์บ์‹ฑ์˜ ํ•ต์‹ฌ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๊ณ , revalidatePath, revalidateTag๋ฅผ ํ™œ์šฉํ•œ ํšจ๊ณผ์ ์ธ ๋ฐ์ดํ„ฐ ์žฌ๊ฒ€์ฆ ์ „๋žต์„ ์‹ค๋ฌด ์˜ˆ์‹œ์™€ ํ•จ๊ป˜ ์ž์„ธํžˆ ๋‹ค๋ฃจ๊ณ  ์žˆ์–ด์š”.

๐Ÿค” ๋ฌธ์ œ/๋ฐฐ๊ฒฝ

0๏ธโƒฃ ์™œ ์ด ์ฃผ์ œ๋ฅผ ๋‹ค๋ฃจ๋Š”๊ฐ€

Next.js 13๋ถ€ํ„ฐ ๋„์ž…๋œ App Router๋Š” ๊ธฐ์กด Pages Router์™€๋Š” ์ „ํ˜€ ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๋ฐ ์บ์‹ฑ ์ „๋žต์„ ์ œ๊ณตํ•ด์š”.
ํŠนํžˆ Server Components์˜ ๋“ฑ์žฅ๊ณผ ํ•จ๊ป˜ ์บ์‹ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ๋”์šฑ ๋ณต์žกํ•ด์ ธ์„œ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋ถ„๋“ค์ด ์‹ค๋ฌด์—์„œ ํ˜ผ๋ž€์„ ๊ฒช๊ณ  ๊ณ„์„ธ์š”.
์ƒˆ๋กœ์šด ์บ์‹ฑ ์ „๋žต์„ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜์ง€ ๋ชปํ•˜๋ฉด, ์˜ˆ์ƒ์น˜ ๋ชปํ•œ stale data ๋ฌธ์ œ๋‚˜ ๋น„ํšจ์œจ์ ์ธ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์œผ๋กœ ์ธํ•ด ์„ฑ๋Šฅ ์ €ํ•˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.
์ด ๊ธ€์—์„œ๋Š” App Router์˜ ์บ์‹ฑ ์›๋ฆฌ๋ฅผ ๋ช…ํ™•ํžˆ ์ดํ•ดํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฅผ ์ตœ์‹  ์ƒํƒœ๋กœ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ํ•„์ˆ˜์ ์ธ ์žฌ๊ฒ€์ฆ(Revalidation) ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ์•„๋ณด๋ ค๊ณ  ํ•ด์š”.

1๏ธโƒฃ ๊ธฐ์กด ๋ฐฉ์‹์˜ ํ•œ๊ณ„

Pages Router์—์„œ๋Š” getServerSideProps, getStaticProps, getStaticPaths ๋“ฑ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ํŽ˜์นญ๊ณผ ์บ์‹ฑ์„ ์ œ์–ดํ–ˆ์–ด์š”.
getStaticProps์˜ revalidate ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ res.revalidate๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ISR(Incremental Static Regeneration)์„ ๊ตฌํ˜„ํ–ˆ์ฃ .
ํ•˜์ง€๋งŒ App Router์—์„œ๋Š” ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์ด ์‚ฌ๋ผ์ง€๊ณ , React์˜ fetch API ํ™•์žฅ๊ณผ ์„œ๋ฒ„ ์•ก์…˜(Server Actions)์„ ํ†ตํ•ด ์บ์‹ฑ๊ณผ ์žฌ๊ฒ€์ฆ์„ ๊ด€๋ฆฌํ•ด์š”.
์ด ๋ณ€ํ™”๋Š” ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ๋” ํฐ ์œ ์—ฐ์„ฑ์„ ์ œ๊ณตํ•˜์ง€๋งŒ, ๋™์‹œ์— ์–ด๋””์„œ ์–ด๋–ป๊ฒŒ ์บ์‹ฑ์ด ๋ฐœ์ƒํ•˜๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์–ธ์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ๊ฒ€์ฆํ•ด์•ผ ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์ƒˆ๋กœ์šด ํ•™์Šต์ด ํ•„์š”ํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ์–ด์š”.

โš™๏ธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

0๏ธโƒฃ ํ•ต์‹ฌ ์•„์ด๋””์–ด

Next.js App Router์˜ ์บ์‹ฑ ์ „๋žต์€ ํฌ๊ฒŒ 3๊ฐ€์ง€ ๋ ˆ์ด์–ด์—์„œ ์ž‘๋™ํ•ด์š”.

  1. Request Memoization: ๋™์ผํ•œ fetch ํ˜ธ์ถœ์ด ํ•œ ๋ฒˆ์˜ ๋ Œ๋”๋ง ์ฃผ๊ธฐ ๋‚ด์—์„œ ์ค‘๋ณต ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ด์š”.
  2. Data Cache (Full Route Cache): fetch ์š”์ฒญ์˜ ๊ฒฐ๊ณผ๋ฅผ ์„œ๋ฒ„์— ์ €์žฅํ•ด์„œ ๋™์ผํ•œ ์š”์ฒญ์— ๋Œ€ํ•ด ์žฌ์‚ฌ์šฉํ•ด์š”. ์ด๋Š” SSG/ISR๊ณผ ์œ ์‚ฌํ•œ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•˜๋ฉฐ, revalidate ์˜ต์…˜์œผ๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด์š”.
  3. Router Cache: ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ Soft Navigation ์‹œ ์ด์ „์— ๋ฐฉ๋ฌธํ–ˆ๋˜ ๋ผ์šฐํŠธ์˜ React Server Components ํŽ˜์ด๋กœ๋“œ(payload)๋ฅผ ์บ์‹ฑํ•ด์š”.

์ด ์ค‘์—์„œ ์šฐ๋ฆฌ๊ฐ€ ์ค‘์ ์ ์œผ๋กœ ๋‹ค๋ฃฐ ๋ถ€๋ถ„์€ Data Cache์™€ ์ด๋ฅผ ๋ฌดํšจํ™”ํ•˜๋Š” **๋ฐ์ดํ„ฐ ์žฌ๊ฒ€์ฆ(Revalidation)**์ด์—์š”.
Next.js๋Š” fetch ์š”์ฒญ์— ๋Œ€ํ•ด ๊ธฐ๋ณธ์ ์œผ๋กœ force-cache ์ „๋žต์„ ์‚ฌ์šฉํ•˜๋ฉฐ, revalidate ์˜ต์…˜์„ ํ†ตํ•ด ์บ์‹ฑ ์‹œ๊ฐ„์„ ์ œ์–ดํ•ด์š”.
ํ•˜์ง€๋งŒ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ์ฆ‰์‹œ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•˜๊ณ  ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์ฃ .
์ด๋•Œ revalidatePath์™€ revalidateTag ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŠน์ • ๊ฒฝ๋กœ(path) ๋˜๋Š” ํƒœ๊ทธ(tag)์™€ ์—ฐ๊ด€๋œ ์บ์‹œ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋ฌดํšจํ™”ํ•  ์ˆ˜ ์žˆ์–ด์š”.

1๏ธโƒฃ ์ ์šฉ ๋ฐฉ๋ฒ•

1. fetch ์บ์‹ฑ ์˜ต์…˜ ํ™œ์šฉ

Next.js๋Š” fetch API๋ฅผ ํ™•์žฅํ•˜์—ฌ ์บ์‹ฑ ๋™์ž‘์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์–ด์š”.
๊ธฐ๋ณธ์ ์œผ๋กœ fetch๋Š” ์บ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ, ํŠน์ • ์‹œ๊ฐ„(์ดˆ ๋‹จ์œ„) ์ดํ›„์— ์žฌ๊ฒ€์ฆํ•˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”.

async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 } // 1์‹œ๊ฐ„๋งˆ๋‹ค ๋ฐ์ดํ„ฐ ์žฌ๊ฒ€์ฆ (ISR๊ณผ ์œ ์‚ฌ) }); if (!res.ok) throw new Error('Failed to fetch posts'); return res.json(); } // ์บ์‹œ ์‚ฌ์šฉ ์•ˆ ํ•จ (SSR๊ณผ ์œ ์‚ฌ) async function getDynamicData() { const res = await fetch('https://api.example.com/dynamic', { cache: 'no-store' // ์ด ์š”์ฒญ์€ ์บ์‹œํ•˜์ง€ ์•Š์Œ }); if (!res.ok) throw new Error('Failed to fetch dynamic data'); return res.json(); }

next: { revalidate: N } ์˜ต์…˜์€ N์ดˆ๋งˆ๋‹ค ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ๊ฒ€์ฆํ•˜์—ฌ stale-while-revalidate ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•˜๊ฒŒ ํ•ด์š”.
cache: 'no-store'๋Š” ์บ์‹œ๋ฅผ ์ „ํ˜€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋งค๋ฒˆ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋„๋ก ๊ฐ•์ œํ•ด์š”.

2. revalidatePath๋กœ ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ ์žฌ๊ฒ€์ฆ

revalidatePath ํ•จ์ˆ˜๋Š” ํŠน์ • ๊ฒฝ๋กœ์™€ ๊ด€๋ จ๋œ ๋ชจ๋“  ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•ด์š”.
์ฃผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋œ ํ›„ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๊ฐ€ ํ‘œ์‹œ๋˜๋Š” ํŽ˜์ด์ง€์˜ ์บ์‹œ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ์‚ฌ์šฉํ•ด์š”.

'use server'; // Server Action์—์„œ ์‚ฌ์šฉ import { revalidatePath } from 'next/cache'; export async function addPost(formData: FormData) { const title = formData.get('title'); const content = formData.get('content'); // ... ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๊ฒŒ์‹œ๋ฌผ ์ถ”๊ฐ€ ๋กœ์ง ... await new Promise(resolve => setTimeout(resolve, 500)); // DB ์ž‘์—… ์‹œ๋ฎฌ๋ ˆ์ด์…˜ console.log('๊ฒŒ์‹œ๋ฌผ ์ถ”๊ฐ€๋จ:', { title, content }); // ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก ํŽ˜์ด์ง€์˜ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•˜์—ฌ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋„๋ก ํ•จ revalidatePath('/posts'); }

์œ„ ์˜ˆ์‹œ์—์„œ๋Š” ์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๋ฌผ์„ ์ถ”๊ฐ€ํ•œ ํ›„ /posts ๊ฒฝ๋กœ์˜ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•˜์—ฌ, ์‚ฌ์šฉ์ž๊ฐ€ /posts ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•  ๋•Œ ์ตœ์‹  ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก์„ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ํ•ด์š”.
revalidatePath๋Š” Server Action์ด๋‚˜ Route Handler ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•ด์š”.

3. revalidateTag๋กœ ํƒœ๊ทธ ๊ธฐ๋ฐ˜ ์žฌ๊ฒ€์ฆ

revalidateTag ํ•จ์ˆ˜๋Š” fetch ์š”์ฒญ์— next: { tags: ['tag-name'] } ์˜ต์…˜์œผ๋กœ ์„ค์ •๋œ ํŠน์ • ํƒœ๊ทธ์™€ ๊ด€๋ จ๋œ ๋ชจ๋“  ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•ด์š”.
์ด๋Š” ์—ฌ๋Ÿฌ ๊ฒฝ๋กœ์—์„œ ๋™์ผํ•œ ๋ฐ์ดํ„ฐ(์˜ˆ: ํŠน์ • ์‚ฌ์šฉ์ž ์ •๋ณด, ์ƒํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ)๋ฅผ ๊ณต์œ ํ•  ๋•Œ ์œ ์šฉํ•ด์š”.

// app/posts/[id]/page.tsx ๋˜๋Š” app/components/PostList.tsx async function getPost(id: string) { const res = await fetch(`https://api.example.com/posts/${id}`, { next: { tags: ['posts'] } // 'posts' ํƒœ๊ทธ๋ฅผ ์ด ์š”์ฒญ์— ํ• ๋‹น }); if (!res.ok) throw new Error('Failed to fetch post'); return res.json(); } // app/users/[id]/page.tsx async function getUserPosts(userId: string) { const res = await fetch(`https://api.example.com/users/${userId}/posts`, { next: { tags: ['userPosts', `user-${userId}-posts`] } // ์—ฌ๋Ÿฌ ํƒœ๊ทธ ํ• ๋‹น ๊ฐ€๋Šฅ }); if (!res.ok) throw new Error('Failed to fetch user posts'); return res.json(); } // app/actions.ts (Server Action) 'use server'; import { revalidateTag } from 'next/cache'; export async function updatePost(postId: string, newContent: string) { // ... ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํŠน์ • ๊ฒŒ์‹œ๋ฌผ ์—…๋ฐ์ดํŠธ ๋กœ์ง ... await new Promise(resolve => setTimeout(resolve, 500)); // DB ์ž‘์—… ์‹œ๋ฎฌ๋ ˆ์ด์…˜ console.log('๊ฒŒ์‹œ๋ฌผ ์—…๋ฐ์ดํŠธ๋จ:', { postId, newContent }); // 'posts' ํƒœ๊ทธ๊ฐ€ ์žˆ๋Š” ๋ชจ๋“  fetch ์š”์ฒญ์˜ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™” revalidateTag('posts'); // ๋งŒ์•ฝ ํŠน์ • ์‚ฌ์šฉ์ž ๊ฒŒ์‹œ๋ฌผ๋งŒ ์—…๋ฐ์ดํŠธ๋˜์—ˆ๋‹ค๋ฉด ํ•ด๋‹น ์‚ฌ์šฉ์ž ํƒœ๊ทธ๋งŒ ๋ฌดํšจํ™” // revalidateTag(`user-${userId}-posts`); }

revalidateTag๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํŠน์ • ํ…Œ์ด๋ธ”์ด๋‚˜ ์ปฌ๋ ‰์…˜๊ณผ ๊ฐ™์ด ๋…ผ๋ฆฌ์ ์œผ๋กœ ๋ฌถ์ธ ๋ฐ์ดํ„ฐ ๊ทธ๋ฃน์˜ ์บ์‹œ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์˜ˆ์š”.
๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ, ๊ด€๋ จ ํƒœ๊ทธ๋งŒ ๋ฌดํšจํ™”ํ•˜๋ฉด ํ•ด๋‹น ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ชจ๋“  ํŽ˜์ด์ง€๊ฐ€ ๋‹ค์Œ ์š”์ฒญ ์‹œ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฒŒ ๋œ๋‹ต๋‹ˆ๋‹ค.

๐Ÿงช ์˜ˆ์‹œ

0๏ธโƒฃ ์ฝ”๋“œ/์„ค์ • ์˜ˆ์‹œ

๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก๊ณผ ์ƒ์„ธ ํŽ˜์ด์ง€๊ฐ€ ์žˆ๊ณ , ์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๋ฌผ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ฐ€์ •ํ•ด๋ณผ๊ฒŒ์š”.

1. ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก ํŽ˜์ด์ง€ (app/posts/page.tsx)

import Link from 'next/link'; async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { tags: ['posts'], revalidate: 60 } // 'posts' ํƒœ๊ทธ๋ฅผ ํ• ๋‹นํ•˜๊ณ  60์ดˆ๋งˆ๋‹ค ์žฌ๊ฒ€์ฆ }); if (!res.ok) { throw new Error('๊ฒŒ์‹œ๋ฌผ์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์–ด์š”.'); } return res.json(); } export default async function PostsPage() { const posts = await getPosts(); return ( <div className="container mx-auto p-4"> <h1 className="text-2xl font-bold mb-4">๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก</h1> <Link href="/posts/new" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mb-4 inline-block"> ์ƒˆ ๊ฒŒ์‹œ๋ฌผ ์ž‘์„ฑ </Link> <ul className="space-y-2"> {posts.map((post: any) => ( <li key={post.id} className="border p-3 rounded shadow-sm"> <Link href={`/posts/${post.id}`} className="text-lg font-semibold text-blue-600 hover:underline"> {post.title} </Link> <p className="text-gray-600">{post.content.substring(0, 100)}...</p> </li> ))} </ul> </div> ); }

2. ์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๋ฌผ ์ž‘์„ฑ ํŽ˜์ด์ง€ (app/posts/new/page.tsx)

import { redirect } from 'next/navigation'; import { revalidatePath, revalidateTag } from 'next/cache'; // ํ•„์š”ํ•œ ํ•จ์ˆ˜ ์ž„ํฌํŠธ // Server Action ์ •์˜ async function createPost(formData: FormData) { 'use server'; const title = formData.get('title') as string; const content = formData.get('content') as string; if (!title || !content) { console.error('์ œ๋ชฉ๊ณผ ๋‚ด์šฉ์„ ๋ชจ๋‘ ์ž…๋ ฅํ•ด์•ผ ํ•ด์š”.'); return; } // ์‹ค์ œ API ํ˜ธ์ถœ ๋˜๋Š” DB ์ €์žฅ ๋กœ์ง console.log('์ƒˆ ๊ฒŒ์‹œ๋ฌผ ์ €์žฅ ์ค‘:', { title, content }); await new Promise(resolve => setTimeout(resolve, 1000)); // ๋น„๋™๊ธฐ ์ž‘์—… ์‹œ๋ฎฌ๋ ˆ์ด์…˜ // ์ €์žฅ ํ›„ ์บ์‹œ ๋ฌดํšจํ™” revalidateTag('posts'); // 'posts' ํƒœ๊ทธ๋ฅผ ๊ฐ€์ง„ ๋ชจ๋“  fetch ์บ์‹œ ๋ฌดํšจํ™” revalidatePath('/posts'); // '/posts' ๊ฒฝ๋กœ์˜ ์บ์‹œ ๋ฌดํšจํ™” (Router Cache ํฌํ•จ) redirect('/posts'); // ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ } export default function NewPostPage() { return ( <div className="container mx-auto p-4"> <h1 className="text-2xl font-bold mb-4">์ƒˆ ๊ฒŒ์‹œ๋ฌผ ์ž‘์„ฑ</h1> <form action={createPost} className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"> <div className="mb-4"> <label htmlFor="title" className="block text-gray-700 text-sm font-bold mb-2">์ œ๋ชฉ</label> <input type="text" id="title" name="title" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required /> </div> <div className="mb-6"> <label htmlFor="content" className="block text-gray-700 text-sm font-bold mb-2">๋‚ด์šฉ</label> <textarea id="content" name="content" rows={5} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required></textarea> </div> <div className="flex items-center justify-between"> <button type="submit" className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"> ๊ฒŒ์‹œ๋ฌผ ์ €์žฅ </button> </div> </form> </div> ); }

1๏ธโƒฃ ์ ์šฉ ๊ฒฐ๊ณผ

์œ„ ์˜ˆ์‹œ ์ฝ”๋“œ์—์„œ createPost Server Action์ด ์‹คํ–‰๋˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ผ๋“ค์ด ๋ฐœ์ƒํ•ด์š”.

  1. ๋ฐ์ดํ„ฐ ์ €์žฅ: ์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๋ฌผ์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋ผ์š”.
  2. revalidateTag('posts'): getPosts ํ•จ์ˆ˜์—์„œ next: { tags: ['posts'] }๋กœ ์„ค์ •๋œ ๋ชจ๋“  fetch ์š”์ฒญ์˜ Data Cache๊ฐ€ ๋ฌดํšจํ™”๋ผ์š”.
    ์ด๋Š” /posts ํŽ˜์ด์ง€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ๋‹ค๋ฅธ ํŽ˜์ด์ง€์—์„œ posts ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒŒ์‹œ๋ฌผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ชจ๋“  ๊ณณ์—์„œ ์บ์‹œ๊ฐ€ ๋ฌดํšจํ™”๋จ์„ ์˜๋ฏธํ•ด์š”.
  3. revalidatePath('/posts'): /posts ๊ฒฝ๋กœ์™€ ๊ด€๋ จ๋œ Router Cache ๋ฐ Data Cache๊ฐ€ ๋ฌดํšจํ™”๋ผ์š”.
    ์‚ฌ์šฉ์ž๊ฐ€ /posts ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋˜๊ฑฐ๋‚˜ ๋‹ค์‹œ ๋ฐฉ๋ฌธํ•  ๋•Œ, Next.js๋Š” ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ํŽ˜์นญํ•˜๊ฒŒ ๋ผ์š”.
    revalidatePath๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ revalidateTag์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋  ๋•Œ ๋” ํšจ๊ณผ์ ์ด์—์š”.
    revalidateTag๋Š” ๋ฐ์ดํ„ฐ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•˜๊ณ , revalidatePath๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์˜ Router Cache๋ฅผ ๋ฌดํšจํ™”ํ•˜์—ฌ ์ฆ‰๊ฐ์ ์ธ UI ์—…๋ฐ์ดํŠธ๋ฅผ ๋ณด์žฅํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ค˜์š”.

์ด๋Ÿฌํ•œ ๊ณผ์ •์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž๋Š” ์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๋ฌผ์ด ์ถ”๊ฐ€๋œ ์ตœ์‹  ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก์„ ์ฆ‰์‹œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด์ฃ .

๐Ÿ“ ์ •๋ฆฌ

0๏ธโƒฃ ํ•ต์‹ฌ ์š”์•ฝ

Next.js App Router์˜ ์บ์‹ฑ ์ „๋žต์€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์„ฑ๋Šฅ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์ด์—์š”.
fetch API์˜ next: { revalidate: N } ์˜ต์…˜์„ ํ†ตํ•ด ISR๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์žฌ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ณ , cache: 'no-store'๋กœ ์บ์‹ฑ์„ ๋น„ํ™œ์„ฑํ™”ํ•  ์ˆ˜๋„ ์žˆ์–ด์š”.
๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ์ฆ‰์‹œ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•˜์—ฌ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜์˜ํ•˜๋ ค๋ฉด, revalidatePath์™€ revalidateTag๋ฅผ ํ™œ์šฉํ•ด์•ผ ํ•ด์š”.

  • revalidatePath(path): ํŠน์ • ๊ฒฝ๋กœ์™€ ๊ด€๋ จ๋œ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•˜์—ฌ, ํ•ด๋‹น ๊ฒฝ๋กœ๋กœ ์ ‘๊ทผ ์‹œ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋„๋ก ํ•ด์š”.
  • revalidateTag(tag): fetch ์š”์ฒญ์— ์„ค์ •๋œ ํŠน์ • ํƒœ๊ทธ์™€ ๊ด€๋ จ๋œ ๋ชจ๋“  ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•˜์—ฌ, ๋…ผ๋ฆฌ์ ์œผ๋กœ ๋ฌถ์ธ ๋ฐ์ดํ„ฐ ๊ทธ๋ฃน์˜ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค˜์š”.

์ด ๋‘ ํ•จ์ˆ˜๋ฅผ Server Action์ด๋‚˜ Route Handler ๋‚ด์—์„œ ์ ์ ˆํžˆ ์‚ฌ์šฉํ•˜์—ฌ, ์‚ฌ์šฉ์ž์—๊ฒŒ ํ•ญ์ƒ ์ตœ์‹ ์˜ ์ •ํ™•ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๋ฉด์„œ๋„ ๋›ฐ์–ด๋‚œ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

1๏ธโƒฃ ๋‹ค์Œ ์•ก์…˜

  • ๊ณต์‹ ๋ฌธ์„œ ์‹ฌํ™” ํ•™์Šต: Next.js ๊ณต์‹ ๋ฌธ์„œ์˜ Caching ์„น์…˜์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ๊ผผ๊ผผํžˆ ์ฝ์–ด๋ณด๋ฉฐ ๊ฐ ์บ์‹ฑ ๋ ˆ์ด์–ด์˜ ๋™์ž‘ ๋ฐฉ์‹์„ ๋” ๊นŠ์ด ์ดํ•ดํ•ด ๋ณด์„ธ์š”.
  • ์‹ค์ œ ํ”„๋กœ์ ํŠธ ์ ์šฉ: ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ Next.js App Router ํ”„๋กœ์ ํŠธ์— revalidatePath์™€ revalidateTag๋ฅผ ์ ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ํ›„ ์บ์‹œ ๋ฌดํšจํ™”๊ฐ€ ์–ด๋–ป๊ฒŒ ์ด๋ฃจ์–ด์ง€๋Š”์ง€ ์ง์ ‘ ๊ฒฝํ—˜ํ•ด ๋ณด์„ธ์š”.
  • ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง: ์บ์‹ฑ ์ „๋žต ์ ์šฉ ํ›„ ์›น ์„ฑ๋Šฅ ์ง€ํ‘œ(Core Web Vitals ๋“ฑ)๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜์—ฌ ์‹ค์ œ ์„ฑ๋Šฅ ๊ฐœ์„  ํšจ๊ณผ๋ฅผ ์ธก์ •ํ•ด ๋ณด๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”.

์ด ๊ธ€์ด Next.js App Router์˜ ์บ์‹ฑ๊ณผ ์žฌ๊ฒ€์ฆ ์ „๋žต์„ ์ดํ•ดํ•˜๊ณ  ์‹ค๋ฌด์— ์ ์šฉํ•˜๋Š” ๋ฐ ํฐ ๋„์›€์ด ๋˜๊ธฐ๋ฅผ ๋ฐ”๋ผ์š”.

๐Ÿ“ฎ ์ฐธ๊ณ 

์„ฑ๊ณต

์—ฐ๊ด€๋œ ํฌ์ŠคํŠธ๊ฐ€ ์—†์–ด์„œ ๋žœ๋คํ•œ ํฌ์ŠคํŠธ๋กœ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค.