๐Ÿš€ Next.js ์‹ฌํ™” 3์žฅ: Cache Invalidation โ€” ํ™”์„์„ ๊นจ๋ถ€์ˆ˜๋Š” ์ฒ ํ‡ด

๐Ÿ“‹ ๊ฐœ์š”

revalidatePath, revalidateTag, on-demand ISR๋กœ ์บ์‹œ๋ฅผ ์ •๋ฐ€ํ•˜๊ฒŒ ๋ฌดํšจํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 15๋ถ„ (์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 8๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ์˜์ฒ (์‹ ์ž…): "๊ฒŒ์‹œ๋ฌผ ์ˆ˜์ • ๋ฒ„ํŠผ ๋ˆ„๋ฅด๊ณ  ์„œ๋ฒ„ API๋กœ DB ์—…๋ฐ์ดํŠธ๋ฅผ ์˜๊ณ  ๋‚˜์„œ window.location.reload()๋กœ ์ƒˆ๋กœ๊ณ ์นจ์„ ๊ฐˆ๊ฒผ๋Š”๋ฐ, ํ™”๋ฉด์—” ๊ณ„์† ๋˜‘๊ฐ™์€ ์˜ˆ์ „ ๊ธ€์”จ๋งŒ ๋– ์š”! Next.js ์„œ๋ฒ„์˜ ์˜๊ตฌ ์บ์‹œ(Data Cache)๋ฅผ ๋ชป ๋ถ€์ˆ˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฑด ์•Œ๊ฒ ์–ด์š”. ์–ด๋–ป๊ฒŒ ๋ถ€์ˆ˜์ฃ ?"
  • ์˜ํ˜ธ(๋ฆฌ๋“œ): "์˜์ฒ  ๋‹˜... ๋ธŒ๋ผ์šฐ์ €์—์„œ '์ƒˆ๋กœ๊ณ ์นจ' ๋ฐฑ๋‚  ๋ˆŒ๋Ÿฌ๋ด์•ผ, ๋น™ํ•˜ ์†(์„œ๋ฒ„)์—์„œ ์˜์›ํžˆ ์ž ๋“  ์–ผ์Œ์กฐ๊ฐ์€ ๊นจ์ง€์ง€ ์•Š์•„์š”! ์„œ๋ฒ„ ์ฝ”์–ด ๊นŠ์ˆ™์ด ์ ‘๊ทผํ•ด์„œ revalidatePath ๋‚˜ revalidateTag ๋ผ๋Š” ํญํŒŒ ๋ช…๋ น์„ ๋‚ด๋ ค์•ผ๋งŒ ๋น„๋กœ์†Œ ์–ผ์Œ๋ฒฝ์ด ๋ฌด๋„ˆ์ง„๋‹ค๊ณ ์š”!"

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
์ •์  ์บ์‹œ ๋ฌดํšจํ™”์˜ ํ•„์š”์„ฑ โ†’ ๊ฒฝ๋กœ ๋‹จ์œ„ ํŒŒ๊ดด โ†’ ํƒœ๊ทธ ๊ธฐ๋ฐ˜ ํŒŒ๊ดด โ†’ ์‹ค๋ฌด ์•„ํ‚คํ…์ฒ˜

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • CMS/์–ด๋“œ๋ฏผ ์‚ฌ์ดํŠธ์—์„œ [์ €์žฅ] ๋ฒ„ํŠผ์ด ๋ˆŒ๋ฆฌ๋Š” ์ฆ‰์‹œ, ํผ๋ธ”๋ฆญ ์œ ์ €๋“ค์ด ๋ณด๋Š” ๋ฉ”์ธ ์ •์  ์›น์‚ฌ์ดํŠธ์˜ ํŠน์ • ์บ์‹œ๋งŒ ์šฐ์•„ํ•˜๊ฒŒ ์ดˆ๊ธฐํ™”ํ•ด์ฃผ๋Š” ์›นํ›…(Webhook) ์„œ๋ฒ„ API๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ฉ์ฒญํ•œ revalidatePath ๋Œ€์‹ , ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์— ์ ์กฐ์ง์ฒ˜๋Ÿผ ํฉ์–ด์ง„ ๋™์ผํ•œ API ๊ฒฐ๊ณผ๋ฅผ ํ•œ ๋ฐฉ์— ๋ถ€์„œ๋œจ๋ฆฌ๋Š” tag ๊ธฐ๋ฐ˜ Invalidation์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

Next.js App Router ์•„ํ‚คํ…์ฒ˜๋Š” ๊ทน๋‹จ์ ์ธ ์บ์‹ฑ(Caching)์„ ์ „์ œ๋กœ ๋งŒ๋“ค์–ด์กŒ์–ด. ์‹ฌํ™” 1/2์žฅ์—์„œ ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์žฅ ํšจ์œจ์ ์œผ๋กœ ํ‰์ƒ ์–ผ๋ ค๋ฒ„๋ฆด ์ˆ˜ ์žˆ์„๊นŒ๋ฅผ ๋ฐฐ์› ๋‹ค๋ฉด, ์ง€๊ธˆ๋ถ€ํ„ฐ๋Š” ๊ทธ ์–ผ์Œ์„ "๋‚ด๊ฐ€ ์›ํ•  ๋•Œ, ์›ํ•˜๋Š” ๋ถ€๋ถ„๋งŒ" ๋ฐ•์‚ด ๋‚ด๋Š” ๋ฒ•(Invalidation)์„ ๋ฐฐ์›Œ์•ผ ํ•ด.

๋ฐ์ดํ„ฐ ์ƒ์„ฑ(C), ์ˆ˜์ •(U), ์‚ญ์ œ(D) ์•ก์…˜์ด ์ผ์–ด๋‚ฌ์„ ๋•Œ, ๊ด€๋ จ ํ™”๋ฉด์˜ ์บ์‹œ ๊ป์งˆ์„ ์ฐข์ง€ ๋ชปํ•˜๋ฉด ์•ฑ์€ ์ˆœ์‹๊ฐ„์— ๋‚ก์€ ์ •๋ณด(Stale Data) ๋งŒ ๋ฑ‰์–ด๋‚ด๋Š” ๊นกํ†ต์ด ๋ผ. ํ”„๋ก ํŠธ์—”๋“œ์˜ ์ฒด๊ฐ ํ€„๋ฆฌํ‹ฐ์˜ 90%๋Š” ์ด "์ˆ˜์ •ํ•œ ๊ฒŒ ๋ฐ”๋กœ๋ฐ”๋กœ ํ™”๋ฉด์— ๋ฐ˜์˜๋˜๋Š”๊ฐ€?" ์—์„œ ํŒ๊ฐ€๋ฆ„ ๋‚˜์ง€. ์ด๊ฒƒ์ด ๋ฐ”๋กœ ์‹œ๋‹ˆ์–ด์˜ ์บ์‹ฑ ํ†ต์ œ๋ ฅ์ด์•ผ.


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด? (์น ํŒ ์ง€์šฐ๊ธฐ)

์„ ์ƒ๋‹˜(Next.js)์ด ์น ํŒ์— ์˜ค๋Š˜ ๊ธ‰์‹ ๋ฉ”๋‰ด(Data Cache)๋ฅผ ์ž”๋œฉ ์ ์–ด๋†จ์–ด. ์•„์ด๋“ค์€ ๊ทธ๊ฑธ ๋ณด๊ณ  ํ™˜ํ˜ธํ•˜์ง€.
์˜์ฒ ์ด๊ฐ€ ๊ต๋ฌด์‹ค์— ๊ฐ€์„œ ๊ธ‰์‹ ๋ฉ”๋‰ด๋ฅผ "์ œ์œก๋ณถ์Œ"์—์„œ "๋ˆ๊นŒ์Šค"๋กœ ๋ฐ”๊ฟจ์–ด(DB ์—…๋ฐ์ดํŠธ).

โŒ ๊ณผ๊ฑฐ (๋ธŒ๋ผ์šฐ์ € ์ƒˆ๋กœ๊ณ ์นจ)
์˜์ฒ ์ด๊ฐ€ ๋ณต๋„์—์„œ ์•„๋ฌด๋ฆฌ "์ƒˆ๋กœ๊ณ ์นจ! ๋ˆ๊นŒ์Šค์•ผ!" ์†Œ๋ฆฌ์ณ๋ด์•ผ, ์„ ์ƒ๋‹˜์˜ ์น ํŒ์—๋Š” ์—ฌ์ „ํžˆ ์ œ์œก๋ณถ์Œ์ด ์ ํ˜€์žˆ์–ด.

โœ… ํ˜„์žฌ (revalidate ํญํŒŒ ๋ช…๋ น)
์˜์ฒ ์ด๊ฐ€ ์„ ์ƒ๋‹˜ ๊ท€์— ๋Œ€๊ณ  "์„ ์ƒ๋‹˜, ์ €๊ธฐ 2๋ฒˆ์งธ ์ค„(๊ฒฝ๋กœ/ํƒœ๊ทธ) ๊ธ‰์‹ ๋ฉ”๋‰ด ๋‚ก์•˜์–ด์š”, ์ข€ ์ง€์›Œ์ฃผ์„ธ์š”!" ๋ผ๊ณ  ์„œ๋ฒ„ ๋‚ด๋ถ€ ๋ช…๋ น๋ง์œผ๋กœ ์†์‚ญ์—ฌ.
์„ ์ƒ๋‹˜์ด ๊ทธ์ œ์•ผ "์–ด์ฟ !" ํ•˜๊ณ  ์ œ์œก๋ณถ์Œ์„ ์ง€์›Œ๋ฒ„๋ ค(Cache Purge). ๋‹ค์Œ ์•„์ด๊ฐ€ ์น ํŒ์„ ๋ดค์„ ๋• ์ œ์œก์ด ์—†์œผ๋‹ˆ ์š”๋ฆฌ์‚ฌํ•œํ…Œ ๋›ฐ์–ด๊ฐ€ "์ƒˆ ๋ฉ”๋‰ด ์ค˜!" ํ•˜๊ณ  ๋ˆ๊นŒ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ์ง„์ •์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๊ฒŒ ๋˜๋Š” ๊ฑฐ์•ผ.


๐Ÿงฉ revalidatePath: ๋ฌด์‹ํ•˜๊ฒŒ ๋„๋ผ๋กœ ๋‚ด๋ ค์ฐ๊ธฐ ๐ŸŸข

์ œ์ผ ์ง๊ด€์ ์ด๊ณ  ๋ฌด์‹(?)ํ•œ ํŒŒ๊ดด ๋ฐฉ์‹์ด์•ผ.
"ํŠน์ • ์ฃผ์†Œ(๊ฒฝ๋กœ)์™€ ์—ฐ๊ด€๋œ ๋ Œ๋”๋ง ํ™”์„๊ณผ ๋ฐ์ดํ„ฐ ์บ์‹œ๋ฅผ ๋ชจ์กฐ๋ฆฌ ๋‹ค ๋‚ ๋ ค๋ฒ„๋ ค๋ผ!"

์‚ฌ์šฉ๋ฒ•

์ฃผ๋กœ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ/์ˆ˜์ • API(Route Handler app/api/... ๋‚ด๋ถ€)๋‚˜, ๋‹ค์Œ ์žฅ์—์„œ ๋ฐฐ์šธ ์„œ๋ฒ„ ์•ก์…˜ ๋‹จ์—์„œ ์‚ฌ์šฉ ๋ผ.

import { revalidatePath } from 'next/cache'
 
// ์œ ์ €๊ฐ€ ์ƒํ’ˆ์„ ๊ตฌ๋งคํ•œ ์งํ›„ ์‹คํ–‰๋˜๋Š” ์„œ๋ฒ„ ๋กœ์ง์ด๋ผ ๊ฐ€์ •
export async function POST() {
  await db.purchaseItem(...) // 1. DB ์—…๋ฐ์ดํŠธ (์ˆ˜๋Ÿ‰ ๊ฐ์†Œ)
 
  // 2. ๋„๋ผ๋กœ ๊ฒฝ๋กœ ๊ฐ•ํƒ€!
  // "/shop" ํŽ˜์ด์ง€์˜ ํ†ต์งธ๋กœ ๊ตฌ์›Œ์ง„ ํ™”์„ ํŒŒ์ผ(HTML)๊ณผ ๊ทธ ์•ˆ์˜ ๋ชจ๋“  Data Cache๋ฅผ ๋ฐ•์‚ด๋‚ธ๋‹ค!
  revalidatePath('/shop') 
  
  return Response.json({ success: true })
}

์žฅ๋‹จ์ 

  • ์žฅ์ : ์—„์ฒญ๋‚˜๊ฒŒ ์ง๊ด€์ ์ž„. "๊ทธ๋ƒฅ ๊ทธ ํŽ˜์ด์ง€ ๋‚ก์€ ๊ฑฐ ๋‹ค ์‹น ์ง€์›Œ!"
  • ๋‹จ์ : ๋งŒ์•ฝ /shop ํŽ˜์ด์ง€ ์•ˆ์— ์•„๋ฌด ์ž˜๋ชป ์—†๋Š”(์•ˆ ๋ฐ”๋€) ๊ณต์ง€์‚ฌํ•ญ์ด๋‚˜ ๋ฐฐ๋„ˆ ์บ์‹œ ๋ฐ์ดํ„ฐ๊นŒ์ง€ ๋ชจ์กฐ๋ฆฌ ๊ฐ™์ด ๋‚ ์•„๊ฐ€ ๋ฒ„๋ฆผ.
    ๊ทธ๋ฆฌ๊ณ  ๋” ์น˜๋ช…์ ์ธ ๊ฑด, /shop ์—๋„ ์ƒํ’ˆ ๋ชฉ๋ก์ด ์žˆ๊ณ  /mypage/recent ์—๋„ ์ƒํ’ˆ ๋ชฉ๋ก์ด ์žˆ๋‹ค๋ฉด? ๋„๋ผ๋ฅผ ๋“ค๊ณ  ๋‹ค๋‹ˆ๋ฉฐ ์ € ๊ฒฝ๋กœ๋“ค์„ ์ผ์ผ์ด ์ „๋ถ€ ๋…ธ๊ฐ€๋‹ค๋กœ ์ ์–ด์„œ ํŒŒ๊ดด๋ช…๋ น์„ ๋‚ด๋ ค์•ผ ํ•ด.

๐ŸŒฑ revalidateTag: ์šฐ์•„ํ•˜๊ณ  ์ •๋ฐ€ํ•œ ์ €๊ฒฉ์ด ๐ŸŸก

Next.js ์ฝ”์–ด ํŒ€์ด ๊ฐ€์žฅ ๋ฏธ๋Š” ์šฐ์•„ํ•˜๊ณ  ์™„๋ฒฝํ•œ ํ•ด๊ฒฐ์ฑ…์ด์•ผ.
"์•„๋ฌด๋ฆฌ ์—ฌ๋Ÿฌ ํŒŒ์ผ๊ณผ ์—ฌ๋Ÿฌ ์ฃผ์†Œ์— ํฉ์–ด์ ธ์žˆ์–ด๋„, ๋˜‘๊ฐ™์€ '์ด๋ฆ„ํ‘œ(Tag)'๋ฅผ ๋‹ฌ๊ณ  ์žˆ๋Š” ์บ์‹œ๋“ค๋งŒ ๊ณจ๋ผ์„œ ํ•€์…‹์œผ๋กœ ์ €๊ฒฉํ•ด ํญํŒŒํ•œ๋‹ค!"

1) ๋ช…์ฐฐ ๋ถ™์ด๊ธฐ (๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ)

์ด๊ฑด page.tsx๋“  ์–ด๋””๋“  fetch๋ฅผ ํ•˜๋Š” ๋…€์„์—๊ฒŒ ๊ผฌ๋ฆฌํ‘œ๋ฅผ ๋‹ฌ์•„์ฃผ๋Š” ๊ฑฐ์•ผ.

// app/shop/page.tsx
const res = await fetch('https://api.my.com/bestseller', {
  next: { tags: ['bestseller-list'] } // ๐Ÿท๏ธ ์ด ํ†ต์‹  ๊ฒฐ๊ณผ๋ฌผ ๋ฉ์–ด๋ฆฌ ์บ์‹œ์— 'bestseller-list'๋ผ๋Š” ์ด๋งˆํ‘œ๋ฅผ ๋ถ™์ธ๋‹ค!
})
// app/mypage/page.tsx
const res2 = await fetch('https://api.my.com/bestseller?limit=3', {
  next: { tags: ['bestseller-list'] } // ๐Ÿท๏ธ ์™„์ „ ๋‹ค๋ฅธ ํŽ˜์ด์ง€์ธ๋ฐ ์—ฌ๊ธฐ๋„ ๊ฐ™์€ ์ด๋งˆํ‘œ๋ฅผ ๋ถ™์ธ๋‹ค!
})

2) ์ €๊ฒฉ ํƒ•! (๋ฐ์ดํ„ฐ๊ฐ€ ์ˆ˜์ •๋  ๋•Œ)

์–ด๋“œ๋ฏผ ์„œ๋ฒ„์—์„œ ๋ฒ ์ŠคํŠธ์…€๋Ÿฌ ์ƒํ’ˆ 1๊ฐœ๋ฅผ ๊ฐ•์ œ๋กœ ๊ต์ฒด(์ˆ˜์ •)ํ–ˆ์–ด. ์ด๋•Œ ์›นํ›…(Webhook) ์„œ๋ฒ„์—์„œ ๋‹จ ํ•œ ๋ฐœ์˜ ์ด์•Œ๋งŒ ์˜๋ฉด ๋ผ.

import { revalidateTag } from 'next/cache'
 
export async function POST() {
  await db.updateBestseller(...) 
  
  // ๐Ÿ”ซ ์ €๊ฒฉ ํƒ•!
  // "/shop" ์ด๊ณ  "/mypage" ์ด๊ณ  ๋‚˜๋ฐœ์ด๊ณ  ๋ฌป์ง€๋„ ๋”ฐ์ง€์ง€๋„ ์•Š๊ณ  
  // Next.js ์ „์—ญ์—์„œ 'bestseller-list' ๋ช…์ฐฐ์ด ๋ถ™์€ ๋ชจ๋“  ์–ผ์Œ ์กฐ๊ฐ๋งŒ ์‹น๋‹ค ์ฐพ์•„๋‚ด ํŒŒ๊ดดํ•จ!
  revalidateTag('bestseller-list') 
  
  return Response.json({ success: true })
}

์ด๊ฒŒ ๋ฐ”๋กœ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜(MSA) ๊ธ‰์˜ ์šฐ์•„ํ•œ ์บ์‹œ ๋ถ„์‚ฐ ํ†ต์ œ์ˆ (Cache Orchestration)์ด์•ผ.


๐Ÿ›ก๏ธ router.refresh(): ์œ ์ € ๋ธŒ๋ผ์šฐ์ € ์†์‚ด ์ง€์šฐ๊ธฐ ๐Ÿ”ด

์œ„ ๋‘ ๊ฐœ์˜ ํญํƒ„ ๊ฒฐ์žฌ์„ (Path, Tag)์€ ์ „๋ถ€ ์„œ๋ฒ„(Node.js ๋‹จ) ์—์„œ๋งŒ ๋ฐœ๋™ ๊ฐ€๋Šฅํ•œ ๋ฌด๊ธฐ๋“ค์ด์•ผ.
ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ(use client) ๋‚ด๋ถ€์˜ onClick ์ด๋ฒคํŠธ์—์„œ๋Š” ์ € ํญํƒ„ ์Šค์œ„์น˜๋ฅผ ํ•จ๋ถ€๋กœ ๋ˆ„๋ฅผ ์ˆ˜๊ฐ€ ์—†์–ด! (๋ณด์•ˆ ์ •์ฑ… ์ƒ)

ํด๋ผ์ด์–ธํŠธ ๋‹จ์—์„œ ์ง์ ‘ "๋‚˜ ๋ฐฉ๊ธˆ ๊ธ€ ์ง€์› ๋Š”๋ฐ, ํ™”๋ฉด ์ข€ ์ฆ‰์‹œ ๋‹ค์‹œ ๊ทธ๋ ค์ฃผ๋ผ!" ๋ผ๊ณ  ๋ช…๋ นํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ?
๋ฐ”๋กœ Next.js์˜ ํด๋ผ์ด์–ธํŠธ ๋ผ์šฐํ„ฐ ํ›…์— ๋‹ฌ๋ฆฐ refresh() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์•ผ.

'use client'
import { useRouter } from 'next/navigation'
 
export default function DeleteButton({ id }) {
  const router = useRouter()
 
  const handleDelete = async () => {
    // 1. ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ๋ฐฑ์—”๋“œ ์‚ญ์ œ API ํ˜ธ์ถœ 
    await fetch(`/api/posts/${id}`, { method: 'DELETE' }); 
    
    // 2. ๋ธŒ๋ผ์šฐ์ €์•ผ! ๋„ค ๋ฉ”๋ชจ๋ฆฌ(Router Cache)์— ๋„์›Œ๋‘” ๋‚ก์€ HTML ๊ป๋ฐ๊ธฐ๋ฅผ ๋ฒ„๋ ค!
    // ๊ทธ๋ฆฌ๊ณ  ๊ฐ•์ œ๋กœ ํ™”๋ฉด ๊นœ๋นก์ž„(F5) ์—†์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ์„œ๋ฒ„๊ฐ€ ์ฃผ๋Š” ์ตœ์‹  ๋ Œ๋”๋ง ๋ทฐ๋ฅผ ๋‹ค์‹œ ๊ธ์–ด์™€!
    router.refresh(); 
  }
 
  return <button onClick={handleDelete}>๊ธ€ ์‚ญ์ œ</button>
}

๐Ÿ”ฅ ์ฃผ์˜ํ•  ์ !
router.refresh() ๋Š” ์‹ฌํ™” 1์žฅ์—์„œ ๋ฐฐ์šด [๊ฐ€์žฅ ์ฒซ ๋ฒˆ์งธ ๋ฐฉ์–ด๋ง‰์ธ ๋ธŒ๋ผ์šฐ์ € ๋‹จ์˜ Router Cache] ๋งŒ์„ ๋ถ€์ˆด๋ฒ„๋ฆฌ๋Š” ํ–‰์œ„์•ผ!
๋งŒ์•ฝ ๋ฐฑ์—”๋“œ ์‚ญ์ œ API(DELETE) ์ชฝ์—, ๋ณธ์งˆ์ ์ธ Next.js ์„œ๋ฒ„์˜ ์–ผ์Œ์„ ๋ถ€์ˆ˜๋Š” revalidatePath('/posts') ๋กœ์ง์ด ์•ˆ ๋“ค์–ด๊ฐ€ ์žˆ๋‹ค๋ฉด?
refresh() ๋ช…๋ น์„ ๋ฐฑ๋‚  ์ณ์„œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์„œ๋ฒ„์— HTML์„ ๋‹ค์‹œ ๋‹ฌ๋ผ๊ณ  ์กฐ๋ฅด๋”๋ผ๋„, ์„œ๋ฒ„๋Š” "์–ด? ๋„ˆ ์ €๋ฒˆ ๋‚ก์€ ํ™”์„ ๋‚ด๊ฐ€ ๊ฐ–๊ณ  ์žˆ์ž–๋ƒ ๊ฐ€์ ธ๋ผ ใ…‹" ํ•˜๊ณ  ๋‚ก์€ ํ™”๋ฉด์„ ๋‹ค์‹œ ์ฃผ๊ฒŒ ๋˜๋ฏ€๋กœ ์˜์›ํžˆ ์ง€์˜ฅ๋„๋ฅผ ๋งด๋Œ๊ฒŒ ๋ผ. ํ•ญ์ƒ ์„œ๋ฒ„ ๋ฌดํšจํ™”(revalidate) + ํด๋ผ ๋ฌดํšจํ™”(refresh)๋Š” ์›ํด๋ฆญ ํ•œ ๋ชธ ์„ธํŠธ๋กœ ๊ฐ€์•ผ ํ•ด.


๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ

์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด Ctrl+F๋กœ ๊ฒ€์ƒ‰ํ•ด๋ด.

โŒ revalidateTag is not a function ํ˜น์€ ์„œ๋ฒ„ ์—๋Ÿฌ

์›์ธ: revalidateTag ๋“ฑ ์บ์‹œ ํŒŒ๊ดด ์ง€์‹œ์ž๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ("use client") ์˜์—ญ์—์„œ import ํ•ด์„œ ๋ฒ„ํŠผ onClick ํ•จ์ˆ˜ ์•ˆ์—์„œ ์ง์ ‘ ๋ถ€๋ฅด๋ ค๊ณ  ์‹œ์ „ํ–ˆ์Œ.
ํ•ด๊ฒฐ์ฑ…: ์ด ํŒŒ๊ดด ํ•จ์ˆ˜๋Š” ๋ณด์•ˆ์ƒ ์˜ค๋กœ์ง€ [์„œ๋ฒ„ ์•ก์…˜(Server Actions)] ์ด๋‚˜ Route Handlers(app/api/) ํ™˜๊ฒฝ๊ฐ™์ด ์•ˆ์ „ํ•œ Node.js ์˜์—ญ ๋‚ด๋ถ€์—์„œ๋งŒ ์ˆ˜์ž…(next/cache)ํ•ด์„œ ๋Œ๋ฆด ์ˆ˜ ์žˆ๋‹ค.

โŒ ํƒœ๊ทธ ์ €๊ฒฉ์ด์„ ์ˆ๋Š”๋ฐ๋„ ๊ธ€์”จ๊ฐ€ ์•ˆ ๋ณ€ํ•ด์š”!

์›์ธ: fetch ํ†ต์‹ ๋ถ€์— { next: { tags: ['my-tag'] } } ๊ผฌ๋ฆฌํ‘œ๋ฅผ ๋ช…์‹œํ•˜๋Š” ๊ฑธ ๊นœ๋นกํ–ˆ๊ฑฐ๋‚˜ ์˜คํƒ€๊ฐ€ ๋‚œ ๊ฒฝ์šฐ.
ํ•ด๊ฒฐ์ฑ…: ์„œ๋ฒ„ ์บ์‹œ ๋คํ”„ ๋กœ๊ทธ๋ฅผ ์ฐ์–ด๋ณด๊ณ (logging: { fetches: { fullUrl: true } } config ํ™œ์šฉ), ๋‚ด๊ฐ€ ํƒ€๊ฒฉํ•œ ํƒœ๊ทธ ์ด๋ฆ„ํ‘œ์˜ ์ฒ ์ž๊ฐ€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š”์ง€ ์ ๊ฒ€ํ•  ๊ฒƒ.


๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

๋ฌดํšจํ™”(๋ถ€์ˆ˜๊ธฐ) ์ˆ˜๋‹จ์ž‘๋™ ์œ„์น˜์žฅ๋‹จ์  ๋ฐ ์‚ฌ์šฉ์ฒ˜
revalidatePath(์ฃผ์†Œ)์„œ๋ฒ„ (Node.js)URL ํ•˜๋‚˜ ๊ธฐ๋ฐ˜์˜ ์ „๋ฉด ํ†ต์งธ ํ™”์„ ํญํŒŒ. ๋‹จ์ˆœํ•˜์ง€๋งŒ ์ •๋ฐ€ ํƒ€๊ฒฉ ๋ถˆ๊ฐ€.
revalidateTag(ํƒœ๊ทธ๋ช…)์„œ๋ฒ„ (Node.js)์ด๋ฆ„ํ‘œ๊ฐ€ ๋ถ™์€ ์บ์‹œ๋“ค๋งŒ ์ „ ์„ธ๊ณ„ ํŒŒํŽธ์„ ๋’ค์ ธ ๋ฉธ์ข…์‹œํ‚ด. ๊ณ ๋‚œ๋„ ์•„ํ‚คํ…์ฒ˜ ํ•„์ˆ˜ ์„ค๊ณ„
router.refresh()ํด๋ผ์ด์–ธํŠธ (๋ธŒ๋ผ์šฐ์ €)์„œ๋ฒ„๊ฐ€ ๋ฑ‰์–ด๋‚ด๋Š” ๋‚ก์€ RSC Payload๋ฅผ ํ˜„์žฌ ๋ธŒ๋ผ์šฐ์ € ํƒญ์—์„œ ์ง€์›Œ๋ฒ„๋ฆฌ๊ณ  UI ์žฌ์š”์ฒญ ๊ฐฑ์‹  (ํ™”๋ฉด ์•ˆ ๊นœ๋นก์ž„)

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

๋ฐฐ์› ์œผ๋ฉด ํ•œ ๋ฒˆ ๋น„ํ‹€์–ด์„œ ํ™•์ธํ•ด๋ด์•ผ ํ•ด.

  1. ์ƒํ™ฉ: ์˜์ฒ ์ด๊ฐ€ ์‡ผํ•‘๋ชฐ์˜ ๋ฉ”์ธ ์ƒํ’ˆ ๋ชฉ๋ก ํŽ˜์ด์ง€(/)์™€ ๊ฐœ์ธ ๊ด€์‹ฌ ์ƒํ’ˆ ํŽ˜์ด์ง€(/mypage/like) 2๊ณณ์— ๋…ธ์ถœ๋˜๊ณ  ์žˆ๋Š” item_id=10 ๋ฒˆ ์ƒํ’ˆ์˜ ๊ฐ€๊ฒฉ์„ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์—์„œ 10๋งŒ ์›์—์„œ 5๋งŒ ์›์œผ๋กœ ์ˆ˜์ •(UPDATE)ํ•˜๋Š” API๋ฅผ ์งฐ๋‹ค. ๋งŒ์•ฝ ์˜์ฒ ์ด๊ฐ€ revalidatePath('/') ๋„๋ผ์งˆ ํ•˜๋‚˜๋งŒ ํ•˜๊ณ  ๋๋ƒˆ๋‹ค๋ฉด, ๊ณ ๊ฐ C๊ฐ€ /mypage/like ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐ”์„ ๋•Œ ์ด ์ƒํ’ˆ ๊ฐ€๊ฒฉ์€ 10๋งŒ ์›์ผ๊นŒ, 5๋งŒ ์›์ผ๊นŒ?

์ •๋‹ต ๋ฐ ํ•ด์„ค:
๊ณ ๊ฐ C๋Š” ์—ฌ์ „ํžˆ ๋‚ก๊ณ  ๋น„์‹ผ 10๋งŒ ์›์งœ๋ฆฌ ๋ฒ„๊ทธ ํ™”๋ฉด์„ ๋ณด๊ฒŒ ๋œ๋‹ค.
revalidatePath('/') ๋Š” ์˜ค๋กœ์ง€ ๋ฃจํŠธ(๋ฉ”์ธ ํ™ˆํŽ˜์ด์ง€) ํ•˜๋‚˜์— ์Œ“์ธ ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ์˜ ์–ผ์Œ ์žฅ๋ฒฝ๋งŒ ๊นจ๋ถ€์‰ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. /mypage/like ์ชฝ์— ๋ฌป์–ด์žˆ๋Š” ํ•ด๋‹น ์ƒํ’ˆ ์ปดํฌ๋„ŒํŠธ๋‚˜ ์บ์‹œ๋Š” ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์•˜๋‹ค.
์ด ์ฐธ์‚ฌ๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด ์ƒํ’ˆ ๊ฐ€๊ฒฉ์„ ๊ทธ๋ฆฌ๋Š” ๋ชจ๋“  fetch ๋‹จ์— { next: { tags: ['item-10'] } } ์ฒ˜๋Ÿผ ๊ณ ์œ  ํƒœ๊ทธ๋ฅผ ๋ฐ•์•„ ๋‘” ๋’ค, ๊ด€๋ฆฌ์ž ์ €์žฅ ๋กœ์ง์—์„œ revalidateTag('item-10') ํ•œ ๋ฐœ๋งŒ ์ˆ์–ด์•ผ ๋ชจ๋“  ์€์‹ ์ฒ˜์˜ ๊ฐ€๊ฒฉ ํ‘œ๊ธฐ๊ฐ€ ์ •์ƒ ๊ฐ€๊ฒฉ์ธ 5๋งŒ ์›์œผ๋กœ ๊ฐฑ์‹ ๋  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์˜ค๋Š˜์€ ์ •๋ง ๋„ฅ์ŠคํŠธ์˜ '์–ผ์Œ ๊นจ๊ธฐ(Invalidation)' ๊ธฐ์ˆ ์„ ๋ฐฐ์šฐ๋ฉด์„œ ๊ฐํƒ„์„ ๊ธˆ์น˜ ๋ชปํ–ˆ์–ด. ์–ด์ œ ๋งŒ๋“  ํ™”์„์ด ์˜ค๋Š˜๊นŒ์ง€ ๋‚จ์•„์žˆ์–ด ๋‹นํ™ฉํ–ˆ๋Š”๋ฐ, ์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜์ด ์•Œ๋ ค์ฃผ์‹  '์ •๋ฐ€ ํƒ€๊ฒฉ(revalidateTag)' ๋•๋ถ„์— ์ด์ œ๋Š” ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ๋ถ€๋ถ„๋งŒ ์ฝ• ์ง‘์–ด์„œ ์ตœ์‹ ํ™”ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋์–ด!

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "์บ์‹œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ๋ณด๋‹ค ๋” ์ค‘์š”ํ•œ ๊ฑด, ์–ธ์ œ ์–ด๋–ป๊ฒŒ ๋ถ€์ˆ ์ง€ ๊ฒฐ์ •ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. revalidatePath ๋กœ ํผ์งํ•˜๊ฒŒ, revalidateTag ๋กœ ์ •๊ตํ•˜๊ฒŒ ์–ผ์Œ์„ ๊นจ์ž!"

๊ฐ€์žฅ ์‹ ๊ธฐํ–ˆ๋˜ ๊ฑด ๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ์–ต๋ ฅ(router.refresh())๊ณผ ์„œ๋ฒ„์˜ ๊ธฐ์–ต๋ ฅ์„ ๋™์‹œ์— ์ปจํŠธ๋กคํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ ์ด์—ˆ์–ด. ๋‘˜ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์–ด๊ธ‹๋‚˜๋ฉด ์œ ์ €๋Š” ๋‚ก์€ ํ™”๋ฉด์„ ๋ณด๊ฒŒ ๋œ๋‹ค๋Š” ์‚ฌ์‹ค์— ์ •์‹ ์ด ๋ฒˆ์ฉ ๋“ค๋”๋ผ. ์˜ค๋Š˜ ๋„ˆ๋ฌด ์ง‘์ค‘ํ•ด์„œ ์ผํ–ˆ๋”๋‹ˆ ์–ด๊นจ๊ฐ€ ๋ป๊ทผํ•˜๋„ค. ํ‡ด๊ทผํ•˜๊ณ  ์ง‘ ๊ฐ€์„œ ์‹œ์›ํ•œ ๋งฅ์ฃผ ํ•œ ์บ” ๋งˆ์‹œ๋ฉฐ ์˜ค๋Š˜ ๋ฐฐ์šด '์บ์‹œ ํญํŒŒ' ๋กœ์ง์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ๋จธ๋ฆฟ์†์œผ๋กœ ๊ทธ๋ ค๋ด์•ผ๊ฒ ์–ด. ๋‚ด์ผ์€ ๋” ์šฐ์•„ํ•œ ๋ฌดํšจํ™” ์ฝ”๋“œ๋ฅผ ์งœ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋  ๊ฑฐ์•ผ! ๐Ÿฃ


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ