๐Ÿš€ Next.js 4์žฅ: Hydration๊ณผ Mismatch โ€” ๋‚ด ํ™”๋ฉด์ด ์ฐข์–ด์ง„ ์ด์œ 

๐Ÿ“‹ ๊ฐœ์š”

Hydration ์›๋ฆฌ์™€ Mismatch ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ด์œ , ๊ทธ๋ฆฌ๊ณ  ์‹ค์ „ ๋””๋ฒ„๊น… ์ „๋žต์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


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

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

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
Hydration ๊ฐœ๋… ์›๋ฆฌ โ†’ ํ”„๋ก ํŠธ์—”๋“œ ์ตœ์•…์˜ ์—๋Ÿฌ Mismatch โ†’ ์›์ธ ๋ถ„์„ โ†’ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค(์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ ์˜ˆ์ œ)

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

  • Next.js์—์„œ ๋ธŒ๋ผ์šฐ์ € ๊ฐ์ฒด(window, localStorage)๋ฅผ ์ปดํฌ๋„ŒํŠธ ์ตœ์ƒ๋‹จ์—์„œ ์ฝ์—ˆ์„ ๋•Œ ํ™”๋ฉด์ด ํ„ฐ์ง€๋Š” ์›๋ฆฌ๋ฅผ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • Hydration ์—๋Ÿฌ๋ฅผ ํšŒํ”ผํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ ๋งˆ์šดํŠธ ๋ณด์žฅ ํŒจํ„ด(Mount Guarantee Pattern)์„ ์Šค์Šค๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

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

  • ์˜์ฒ (์ƒˆ๋กœ ์˜จ ์ฃผ๋‹ˆ์–ด): "์–ด? ์œ ์ €์˜ ๋ธŒ๋ผ์šฐ์ € ์–ธ์–ด ์„ธํŒ…์— ๋งž์ถฐ์„œ 'ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค' / 'Welcome' ์„ ๋ณด์—ฌ์ฃผ๋ ค๊ณ  navigator.language ๋ฅผ ์ผ๋Š”๋ฐ์š”. ์ž๊พธ ํ™”๋ฉด์€ ๋ฉ€์ฉกํ•œ๋ฐ ์ฝ˜์†”์— ์‹œ๋ป˜๊ฑด 'Text content did not match' ์—๋Ÿฌ๊ฐ€ ์Ÿ์•„์ ธ์š”! ๐Ÿ˜ญ"
  • ์˜ํ˜ธ(FE ๋ฆฌ๋“œ): "์˜์ฒ  ๋‹˜... ์„œ๋ฒ„(์˜๊ตญ Vercel)์—์„œ ๊ตฌ์›Œ ๋ณด๋‚ธ '๋นˆ๋Œ€๋–ก(HTML)' ์ด๋ž‘, ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋‹ค์‹œ ๊ตฌ์›Œ๋ณธ '๋นˆ๋Œ€๋–ก' ์˜ ํ† ํ•‘์ด ๋‹ค๋ฅด๋ฉด ๋ฆฌ์•กํŠธ๋Š” ๋ฉ˜๋ถ•์— ๋น ์ง€๊ฑฐ๋“ ์š”. ์„œ๋ฒ„๋Š” ์ ‘์†์ž๊ฐ€ ๋ˆ„๊ตฐ์ง€ ๋ชฐ๋ผ์š”!"

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

Next.js ํ™˜๊ฒฝ์„ ์ฒ˜์Œ ๋‹ค๋ฃจ๋Š” React ๊ฐœ๋ฐœ์ž๊ฐ€ ๋งˆ์ฃผํ•˜๊ฒŒ ๋˜๋Š” ๊ฐ€์žฅ ๋”์ฐํ•˜๊ณ  ๊ฑฐ๋Œ€ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ 1์œ„ ๋Š” ๋ˆ„๊ฐ€ ๋ญ๋ผ ํ•ด๋„ "Hydration Error" ์ผ ๊ฑฐ์•ผ.

๊ธฐ๋ณธ์ ์ธ React(CSR) ์„ธ์ƒ์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ…… ๋นˆ HTML์„ ๋ฐ›๊ณ , ํ•˜์–€ ํ™”๋ฉด ์ƒํƒœ์—์„œ ๋ณธ์ธ์ด ์ง์ ‘ UI๋ฅผ ๊ทธ๋ ธ์–ด. ๋ณธ์ธ์ด ๋งŒ๋“œ๋Š” ๊ฑฐ๋‹ˆ๊นŒ ๋ถˆ์ผ์น˜ํ•  ์ผ์ด ์—†์—ˆ์ง€.

ํ•˜์ง€๋งŒ Next.js์—์„œ๋Š” ์ด์•ผ๊ธฐ๊ฐ€ ๋‹ฌ๋ผ. ์„œ๋ฒ„๊ฐ€ ๋จผ์ € ์™„๋ฒฝํ•œ ๋ชจ์–‘์˜ "HTML ๊ป๋ฐ๊ธฐ" ๋ฅผ ๊ทธ๋ ค์„œ ๋ฐฐ๋‹ฌํ•ด์ค˜. ๊ทธ๋ฆฌ๊ณ  ๋ธŒ๋ผ์šฐ์ €๋Š” ์ด ๊ป๋ฐ๊ธฐ ์œ„์— ๋’ค๋Šฆ๊ฒŒ ๋„์ฐฉํ•œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฌผ(Hydrate) ์„ ๋ถ€์–ด ์ƒ๋ช…์„ ์‚ด๋ ค์•ผ ํ•ด.

์ด๋•Œ ๊ป๋ฐ๊ธฐ ๊ทธ๋ฆผ(์„œ๋ฒ„์—์„œ ๋ณธ ์„ธ์ƒ)๊ณผ ๋‚ด์šฉ๋ฌผ(๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ณธ ์„ธ์ƒ)์ด ๋‹จ 1ํ”ฝ์…€, ๋‹จ 1๊ธ€์ž๋ผ๋„ ๋‹ค๋ฅด๋ฉด, React๋Š” "ํ•ดํ‚น๋‹นํ–ˆ๊ฑฐ๋‚˜ ์ฝ”๋“œ๊ฐ€ ์‹ฌ๊ฐํ•˜๊ฒŒ ๊ผฌ์˜€๋‹ค!" ๊ณ  ํŒ๋‹จํ•˜๋ฉฐ ์•ฑ์„ ํญํŒŒ์‹œ์ผœ๋ฒ„๋ ค. ์ด ๋‘ ์„ธ๊ณ„์˜ ๋ถˆ์ผ์น˜ ๋ฅผ ๋‹ค๋ฃฐ ์ค„ ๋ชจ๋ฅด๋ฉด ์˜์›ํžˆ ์›์ธ ๋ชจ๋ฅผ UI ๋ฒ„๊ทธ์— ์‹œ๋‹ฌ๋ฆฌ๊ฒŒ ๋  ๊ฑฐ์•ผ.


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

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด? (์ด์ผ€์•„ ์กฐ๋ฆฝ ๊ฐ€์ด๋“œ)

SSR(์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง) ์€ ์ด์ผ€์•„ ๊ณต์žฅ์—์„œ ๋ฏธ๋ฆฌ ์ฐ์–ด๋†“์€ ์™„์„ฑ ์‚ฌ์ง„(HTML ๊ป๋ฐ๊ธฐ) ์„ ๋ฐฐ๋‹ฌํ•ด์ฃผ๋Š” ๊ฑฐ์•ผ. ๋ˆˆ์—๋Š” ์™„์„ฑ๋œ ์†ŒํŒŒ์ฒ˜๋Ÿผ ๋ณด์—ฌ.

Hydration(์ˆ˜ํ™”) ์€ ๊ทธ ์†ŒํŒŒ์— ์‹ค์ œ๋กœ ์•‰์„ ์ˆ˜ ์žˆ๊ฒŒ ๋‹ค๋ฆฌ๋ฅผ ์กฐ๋ฆฝํ•˜๊ณ  ์ฟ ์…˜์„ ๋ผ์šฐ๋Š” ์ž‘์—…์ด์•ผ(์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ). ์ด ์ž‘์—…์ด ๋๋‚˜์•ผ ๋น„๋กœ์†Œ ๋ฒ„ํŠผ๋„ ๋ˆŒ๋ฆฌ๊ณ  ํด๋ฆญ๋„ ๋ผ.

Mismatch ์—๋Ÿฌ ๋Š” ์ด๋Ÿฐ ์ƒํ™ฉ์ด์•ผ. ๊ณต์žฅ์—์„œ "๋นจ๊ฐ„ ์†ŒํŒŒ" ์‚ฌ์ง„์„ ๋ณด๋ƒˆ๋Š”๋ฐ, ๋‚ด ์กฐ๋ฆฝ ์„ค๋ช…์„œ(๋ธŒ๋ผ์šฐ์ € JS) ์—๋Š” "ํŒŒ๋ž€ ์†ŒํŒŒ๋ฅผ ์กฐ๋ฆฝํ•˜์„ธ์š”" ๋ผ๊ณ  ์ ํ˜€ ์žˆ๋Š” ๊ฑฐ์•ผ. ๋ฆฌ์•กํŠธ๊ฐ€ "์ด๊ฑฐ ๋‹ค๋ฅธ ์ œํ’ˆ์ด์ž–์•„! ๋‚ด๊ฐ€ ์•„๋Š” ๋ถ€ํ’ˆ์ด๋ž‘ ์•ˆ ๋งž์•„!" ํ•˜๋ฉด์„œ ์กฐ๋ฆฝ์„ ๊ฑฐ๋ถ€ํ•˜๋Š” ๋Œ€์ฐธ์‚ฌ์ง€.


๐Ÿงฉ Hydration (์ˆ˜ํ™”) โ€” ๋นˆ HTML์— ์ƒ๋ช… ๋ถˆ์–ด๋„ฃ๊ธฐ ๐ŸŸข

์ด๊ฒŒ ๋ญ”๊ฐ€? & ์™œ ํ•„์š”ํ•œ๊ฐ€?

Next.js๊ฐ€ ์ž๋ž‘ํ•˜๋Š” ๋น ๋ฅธ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„์˜ ๋น„๋ฐ€์€ ์„œ๋ฒ„์—์„œ HTML ์š”์†Œ๋ฅผ ๋ฏธ๋ฆฌ ๋–กํ•˜๋‹ˆ ๋‹ค ๊ทธ๋ ค์„œ ๋ณด๋‚ด๋Š” ๊ฒƒ์— ์žˆ์–ด. ๊ทธ ๋•์— ๋ˆˆ์œผ๋กœ๋Š” ์™„๋ฒฝํ•œ ์‚ฌ์ดํŠธ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์ง€.
๋‹ค๋งŒ ์ด ์ƒํƒœ์˜ ์‚ฌ์ดํŠธ๋Š” ํด๋ฆญ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ๋„ ๋ฐ˜์‘์ด ์—†๋Š” '์‹๋ฌผ์ธ๊ฐ„' ์ƒํƒœ์•ผ. ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์•„์ง ๋ฒ„ํŠผ์— ๋ฌถ์ด์ง€(Bind) ์•Š์•˜๊ฑฐ๋“ .

๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ด ๊ป๋ฐ๊ธฐ๋ฅผ ๋ฐ›๊ณ  ๋‚˜์„œ์•ผ ๋ฒˆ๋“ค ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•ด. ๊ทธ๋ฆฌ๊ณ  HTML ์š”์†Œ๋งˆ๋‹ค ์ฐฐํ™์„ ๋ฐ”๋ฅด๋“ฏ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ(onClick)๋ฅผ ๋งตํ•‘ํ•ด์„œ ์‚ด์•„ ์›€์ง์ด๊ฒŒ ๋งŒ๋“ค์–ด. ์ด ๊ณผ์ •์ด ๋ฉ”๋งˆ๋ฅธ ๊ป๋ฐ๊ธฐ์— ์ด‰์ด‰ํ•˜๊ฒŒ ์ˆ˜๋ถ„์„ ๊ณต๊ธ‰ํ•œ๋‹ค๋Š” ๋œป์œผ๋กœ Hydration(์ˆ˜ํ™”) ์ด๋ผ๊ณ  ๋ถˆ๋Ÿฌ.


๐Ÿšจ Text content did not match (Mismatch ์—๋Ÿฌ์˜ ๊ณตํฌ) ๐ŸŸก

๊ฐ€์žฅ ์•…๋ช… ๋†’์€ ์—๋Ÿฌ๋Š” ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋งํ•  ๋•Œ ๋ฑ‰์–ด๋‚ธ ๊ฒฐ๊ณผ๋ฌผ๊ณผ, ๋‚˜์ค‘์— ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ทธ ์ฝ”๋“œ ๋ผ์ธ์„ ๋‹ค์‹œ ์‹คํ–‰ํ–ˆ์„ ๋•Œ ๊ธฐ๋Œ€ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฌผ์ด ์„œ๋กœ ๋‹ค๋ฅผ ๋•Œ ๋ฐœ์ƒํ•ด.

โŒ ์˜์ฒ ์ด์˜ ํญํƒ„ ์ฝ”๋“œ: ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€ ๋งน์‹ 

์˜์ฒ ์ด๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์„ค์ •ํ•œ ๋‹คํฌ๋ชจ๋“œ ํ…Œ๋งˆ๋ฅผ ๊บผ๋‚ด์˜ค๊ณ  ์‹ถ์—ˆ์–ด.

// โŒ ์ˆœ์ง„ํ•œ ์ฝ”๋“œ (Naive Approach) - ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ
'use client'
 
export default function ThemeGreeting() {
  // ์ด ์ฝ”๋“œ๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์™„๋ฒฝํ•˜๊ฒŒ "dark"๋ฅผ ๊บผ๋‚ธ๋‹ค.
  // ํ•˜์ง€๋งŒ ์ด ์ฝ”๋“œ๊ฐ€ "Next.js ์„œ๋ฒ„"์—์„œ ์ฝํž ๋•Œ๋Š” ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€๊ฐ€ ์—†์œผ๋ฏ€๋กœ ํ„ฐ์ง€๊ฑฐ๋‚˜ null์ด ๋œ๋‹ค.
  const theme = typeof window !== 'undefined' ? localStorage.getItem('theme') : 'light';
 
  return (
    <div className={theme === 'dark' ? 'bg-black text-white' : 'bg-white text-black'}>
      ์•ˆ๋…•ํ•˜์„ธ์š”, ์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ์ž…๋‹ˆ๋‹ค!
    </div>
  )
}

[๋ฆฌ์•กํŠธ ์—”์ง„์ด ํ™”๋ฅผ ๋‚ด๋Š” ๊ณผ์ •]

  1. ์„œ๋ฒ„(Node.js): "์Œ, window๊ฐ€ ์—†๊ตฐ. ๋‚˜๋Š” ์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฌด์กฐ๊ฑด light ํ•˜์–€์ƒ‰ ํ…Œ๋งˆ์˜ HTML ๊ป๋ฐ๊ธฐ๋กœ ๋งŒ๋“ค์–ด์„œ ๋ณด๋‚ด์•ผ์ง€."
  2. ๋ธŒ๋ผ์šฐ์ €: ํ•˜์–€ ๊ป๋ฐ๊ธฐ๋ฅผ ๋ฐ›์•˜์–ด. ๋ˆˆ์—๋Š” ํ•˜์–€ ํ™”๋ฉด์ด ๋ณด์—ฌ.
  3. ๋ธŒ๋ผ์šฐ์ € (Hydrate ์‹œ์ž‘): "์ž ์ด์ œ ๋ฌผ๊ฐ์„ ์น ํ•ด๋ณผ๊นŒ? ์–ด? ๋‚ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋Š” theme ๋ณ€์ˆ˜์— ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€ ๊ธ€์ž์ธ 'dark'๊ฐ€ ๋‹ด๊ธด๋‹ค๊ณ  ๋งํ•˜๋„ค! ๋‚˜๋Š” ๊นŒ๋งŒ ๋ฐ”ํƒ•์„ ๊ทธ๋ ค์•ผ ํ•˜๋Š”๋ฐ ์„œ๋ฒ„ ๋†ˆ์€ ํ•˜์–€ ๋ฐ”ํƒ•์„ ์คฌ์ž–์•„?! Text content did not match! ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ์˜ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ค๋ฅด๋‹ค!"

๐Ÿ›ก๏ธ ์šฐ์•„ํ•œ ํ•ด๊ฒฐ์ฑ…: useEffect + State ๋งˆ์šดํŠธ ์ง€์—ฐ ๐ŸŸก

์‹œ๋‹ˆ์–ด ๊ฐœ๋ฐœ์ž์ธ ์˜ํ˜ธ๋Š” ์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ ์˜์› ๋ถˆ๋ณ€์˜ ํ•ด๊ฒฐ์ฑ…์ธ ๋งˆ์šดํŠธ ๋ณด์žฅ ํŒจํ„ด(Mount Guarantee Pattern) ์„ ์‚ฌ์šฉํ•ด.

๐Ÿ’ก ํ•ต์‹ฌ ๋…ผ๋ฆฌ
๋ธŒ๋ผ์šฐ์ €(๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€, ์œˆ๋„์šฐ ๋„ˆ๋น„ ๋“ฑ)๋งŒ์˜ ๊ณ ์œ  ์ •๋ณด๋ฅผ ์ฝ์–ด์•ผ๋งŒ ํ•˜๋Š” UI๊ฐ€ ์žˆ๋‹ค๋ฉด, ์„œ๋ฒ„์™€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ฒ˜์Œ ๋ Œ๋”๋งํ•˜๋Š” 1์ฐจ ์Šค๋ƒ…์ƒท์€ ๋ฌด์กฐ๊ฑด ๋™์ผํ•˜๊ฒŒ ํ†ต์ผ(๋ณดํ†ต ๋น„์–ด์žˆ๊ฒŒ)ํ•ด์ฃผ๊ณ , ๊ทธ ์ฆ‰์‹œ **2์ฐจ ๋ฆฌ๋ Œ๋”๋ง(ํด๋ผ์ด์–ธํŠธ ์ „์šฉ)**์„ ์ด‰๋ฐœํ•ด์„œ ๋ธŒ๋ผ์šฐ์ € ์ •๋ณด๋กœ ๊ฐˆ์•„์น˜์›Œ๋ผ!

โœ… ์˜ํ˜ธ์˜ ๋ฆฌํŒฉํ† ๋ง: Mounted ์ƒํƒœ ๋ถ„๋ฆฌ

// โœ… ์šฐ์•„ํ•œ ์ฝ”๋“œ (Pro Approach)
'use client'
import { useState, useEffect } from 'react'
 
export default function ThemeGreeting() {
  // 1. ์ฒ˜์Œ์—” ๋ฌด์กฐ๊ฑด '๋กœ๋”ฉ ์ค‘' ํ˜น์€ '๊ธฐ๋ณธ๊ฐ’' ์œผ๋กœ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•ฉ์˜๋ฅผ ๋ณธ๋‹ค.
  const [mounted, setMounted] = useState(false);
  const [theme, setTheme] = useState('light');
 
  // 2. useEffect๋Š” ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ๋„์ค‘์—๋Š” ์ ˆ๋Œ€ ๋ฐœ๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค. 
  // ์˜ค์ง ๋ธŒ๋ผ์šฐ์ €์—์„œ 'Hydration' ์ด ๋๋‚œ ์งํ›„ 1ํšŒ๋งŒ ๋ฐœ๋™ํ•œ๋‹ค!
  useEffect(() => {
    setTheme(localStorage.getItem('theme') || 'light');
    setMounted(true); // "๋‚˜ ๋ธŒ๋ผ์šฐ์ €์— ์•ˆ์ „ํ•˜๊ฒŒ ์ฐฉ๋ฅ™(Mount) ์™„๋ฃŒํ–ˆ์–ด!"
  }, []);
 
  // 3. ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ์˜ ์ฒซ 1์ฐจ ๋ Œ๋”๋ง ์Šค๋ƒ…์ƒท ์‹œ์ . (๋‘˜ ๋‹ค mounted = false) -> ๊ป๋ฐ๊ธฐ ์™„๋ฒฝ ์ผ์น˜!
  if (!mounted) {
    return <div className="bg-white">์Šค์ผˆ๋ ˆํ†ค UI (ํ•ฉ์˜๋œ ๋นˆ ๊ป๋ฐ๊ธฐ)</div>;
  }
 
  // 4. useEffect ๋ฐœ๋™ ํ›„ (2์ฐจ ๋ Œ๋”๋ง) -> ๋ธŒ๋ผ์šฐ์ €์—์„œ๋งŒ ์กฐ์šฉํžˆ ๋‹คํฌ๋ชจ๋“œ๋กœ ๊ฐˆ์•„๋ผ์›Œ์ง
  return (
    <div className={theme === 'dark' ? 'bg-black text-white' : 'bg-white text-black'}>
      ์•ˆ๋…•ํ•˜์„ธ์š”, ์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ์ž…๋‹ˆ๋‹ค!
    </div>
  )
}

์ด ํŒจํ„ด์˜ ์•„๋ฆ„๋‹ค์›€:
์„œ๋ฒ„๊ฐ€ ๋ Œ๋”๋งํ•œ ์ดˆ๊ธฐ HTML๊ณผ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฒซ ๋ Œ๋”๋งํ•˜๋Š” JSX ์š”์†Œ(mounted === false ์ƒํƒœ)๊ฐ€ ์™„๋ฒฝํ•˜๊ฒŒ ๋™์ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฆฌ์•กํŠธ๊ฐ€ ์•„๋ฌด๋Ÿฐ ๋ถˆํ‰(์—๋Ÿฌ) ์—†์ด ์ˆ˜ํ™”(Hydration)๋ฅผ ์™„๋ฃŒํ•ด. ์•ˆ์ฐฉ์ด ์„ฑ๊ณตํ•œ ์งํ›„ useEffect๊ฐ€ ๋น„๋กœ์†Œ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์ฝ๊ณ  2์ฐจ ๋ Œ๋”๋ง์œผ๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ UI ๊ต์ฒด ๋งˆ์ˆ ์„ ๋ถ€๋ฆฌ๋Š” ๊ฑฐ์•ผ.


๐ŸŒ™ ๋‹คํฌํ…Œ๋งˆ mounted ํŒจํ„ด์˜ ์„ฑ๋Šฅ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„์™€ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • mounted ํŒจํ„ด์„ ๋‹คํฌํ…Œ๋งˆ์— ์“ธ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์„ฑ๋Šฅ ์†ํ•ด๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • CSS ๋ณ€์ˆ˜๋ฅผ ํ™œ์šฉํ•ด ๊นœ๋นก์ž„ ์—†๋Š” ๋‹คํฌํ…Œ๋งˆ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•ˆ๋‹ค
  • next-themes ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์–ด๋–ป๊ฒŒ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š”์ง€ ์ดํ•ดํ•œ๋‹ค

mounted ํŒจํ„ด์˜ ์ˆจ๊ฒจ์ง„ ๋น„์šฉ

์•ž์—์„œ ๋ฐฐ์šด mounted ํŒจํ„ด์€ ์ž‘๋™์€ ์™„๋ฒฝํ•˜์ง€๋งŒ, ๋‹คํฌํ…Œ๋งˆ์— ์ ์šฉํ•˜๋ฉด ๋‘ ๊ฐ€์ง€ ๋น„์šฉ์ด ๋”ฐ๋ผ์™€:

1. ์ถ”๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง (Extra Render)
์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฌด์กฐ๊ฑด 2๋ฒˆ ๋ Œ๋”๋ง๋ผ. ์ฒซ ๋ฒˆ์งธ๋Š” ์Šค์ผˆ๋ ˆํ†ค(ํฐ ํ™”๋ฉด), ๋‘ ๋ฒˆ์งธ๊ฐ€ ์‹ค์ œ ํ…Œ๋งˆ ์ ์šฉ. ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ณต์žกํ• ์ˆ˜๋ก ์ด ๋น„์šฉ์ด ์ปค์ ธ.

2. ๋ ˆ์ด์•„์›ƒ ์‹œํ”„ํŠธ (Layout Shift)
์ฒซ ๋ Œ๋”(ํฐ ๋ฐฐ๊ฒฝ) โ†’ ํ…Œ๋งˆ ์ ์šฉ(๊ฒ€์€ ๋ฐฐ๊ฒฝ)์œผ๋กœ ๋ฐ”๋€Œ๋Š” ์ˆœ๊ฐ„ ํ™”๋ฉด์ด ์ˆœ๊ฐ„ ๋ฐ˜์ง์ด๋Š” ํ˜„์ƒ์ด ์ƒ๊ฒจ. ๋‹คํฌ๋ชจ๋“œ ์œ ์ €๋ผ๋ฉด ํฐ ํ™”๋ฉด์ด 0.1~0.2์ดˆ ๋ณด์˜€๋‹ค๊ฐ€ ์‚ฌ๋ผ์ง€๋Š” ๋ถˆ์พŒํ•œ ๊ฒฝํ—˜์ด์•ผ.

// mounted ํŒจํ„ด์˜ ๋น„์šฉ์ด ๋ณด์ด๋Š” ์ˆœ๊ฐ„
if (!mounted) {
  return <div className="bg-white">์Šค์ผˆ๋ ˆํ†ค</div>;  // 1์ฐจ ๋ Œ๋”: ํฐ ํ™”๋ฉด ์ž ๊น ๋ณด์ž„
}
return <div className={theme === 'dark' ? 'bg-black' : 'bg-white'}>...</div>; // 2์ฐจ ๋ Œ๋”

์ด๊ฒŒ "์–ด์ฉ” ์ˆ˜ ์—†๋Š” ์†ํ•ด"์ผ๊นŒ? ์•„๋‹ˆ์•ผ. CSS ๋ณ€์ˆ˜๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ด ๋น„์šฉ์„ ๊ฑฐ์˜ ์—†์•จ ์ˆ˜ ์žˆ์–ด.

๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค: CSS ๋ณ€์ˆ˜ + suppressHydrationWarning

CSS ๋ณ€์ˆ˜ ๋ฐฉ์‹์€ HTML์˜ data-theme ์†์„ฑ ํ•˜๋‚˜๋กœ ํ…Œ๋งˆ๋ฅผ ์ „ํ™˜ํ•ด. ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๊ฐœ์ž…ํ•˜๊ธฐ๋„ ์ „์—, ์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ localStorage์—์„œ ํ…Œ๋งˆ๋ฅผ ์ฝ์–ด <html> ํƒœ๊ทธ์— ์†์„ฑ์„ ์ ์šฉํ•˜๊ฑฐ๋“ .

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    // suppressHydrationWarning: html ํƒœ๊ทธ์˜ data-theme ์†์„ฑ์ด
    // ์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธ ๊ฐ„ ๋‹ค๋ฅด๋”๋ผ๋„ ๊ฒฝ๊ณ ๋ฅผ ์–ต์ œํ•˜๊ฒ ๋‹ค๋Š” ์„ ์–ธ
    <html lang="ko" suppressHydrationWarning>
      <head>
        {/* ํŽ˜์ด์ง€ ํŒŒ์‹ฑ ์งํ›„ ์ฆ‰์‹œ ์‹คํ–‰๋˜์–ด ๊นœ๋นก์ž„ ๋ฐฉ์ง€ */}
        <script dangerouslySetInnerHTML={{
          __html: `
            (function() {
              var theme = localStorage.getItem('theme') || 'light';
              document.documentElement.setAttribute('data-theme', theme);
            })();
          `
        }} />
      </head>
      <body>{children}</body>
    </html>
  )
}
/* globals.css โ€” CSS ๋ณ€์ˆ˜๋กœ ํ…Œ๋งˆ ์ „ํ™˜ */
[data-theme='light'] {
  --bg-primary: #ffffff;
  --text-primary: #000000;
}
 
[data-theme='dark'] {
  --bg-primary: #1a1a1a;
  --text-primary: #ffffff;
}
 
body {
  background-color: var(--bg-primary);
  color: var(--text-primary);
}

์ด ๋ฐฉ์‹์—์„œ๋Š” ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ useState(false) + useEffect ํŒจํ„ด์ด ํ•„์š” ์—†์–ด. CSS๊ฐ€ ์•Œ์•„์„œ ํ…Œ๋งˆ๋ฅผ ์ ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์ด์•ผ.

next-themes ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ด ๋ชจ๋“  ๊ฑธ ํ•ด๊ฒฐํ•ด์ค˜

์‹ค๋ฌด์—์„œ๋Š” ์ด ๋ณต์žกํ•œ ๋กœ์ง์„ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๋Œ€์‹ , next-themes ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋งŽ์ด ์จ. ๋‚ด๋ถ€์ ์œผ๋กœ CSS ๋ณ€์ˆ˜ ๋ฐฉ์‹๊ณผ suppressHydrationWarning์„ ์กฐํ•ฉํ•ด์„œ ๊นœ๋นก์ž„ ์—†๋Š” ๋‹คํฌํ…Œ๋งˆ๋ฅผ ์ œ๊ณตํ•ด.

// app/layout.tsx
import { ThemeProvider } from 'next-themes'
 
export default function RootLayout({ children }) {
  return (
    <html suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="data-theme"    // HTML ์†์„ฑ์œผ๋กœ ํ…Œ๋งˆ ๊ด€๋ฆฌ
          defaultTheme="system"     // OS ๋‹คํฌ๋ชจ๋“œ ์„ค์ • ๋”ฐ๋ผ๊ฐ
          enableSystem              // ์‹œ์Šคํ…œ ํ…Œ๋งˆ ๊ฐ์ง€ ํ™œ์„ฑํ™”
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}
// ๊ฐœ๋ณ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ
'use client'
import { useTheme } from 'next-themes'
 
export function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  return (
    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      {theme === 'dark' ? 'โ˜€๏ธ' : '๐ŸŒ™'}
    </button>
  )
}

โš ๏ธ ์ฃผ์˜: next-themes์˜ useTheme()๋„ ๋‚ด๋ถ€์ ์œผ๋กœ ๋งˆ์šดํŠธ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ด. mounted ์ƒํƒœ๊ฐ€ false์ผ ๋•Œ theme์ด undefined๋กœ ๋‚˜์˜ฌ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ดˆ๊ธฐ ์•„์ด์ฝ˜์„ ๊ทธ๋ฆด ๋•Œ๋Š” ๋ฐฉ์–ด ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด.

์–ธ์ œ ์–ด๋–ค ๋ฐฉ์‹์„ ์จ์•ผ ํ•ด?

์ƒํ™ฉ๊ถŒ์žฅ ๋ฐฉ์‹
๊ฐ„๋‹จํ•œ ์ปดํฌ๋„ŒํŠธ 1~2๊ฐœ์—๋งŒ ํ…Œ๋งˆ ์ ์šฉmounted ํŒจํ„ด์œผ๋กœ ์ถฉ๋ถ„
์•ฑ ์ „์ฒด ๋‹คํฌํ…Œ๋งˆ + ๊นœ๋นก์ž„ ์—†์• ์•ผ ํ•จnext-themes + CSS ๋ณ€์ˆ˜
์ง์ ‘ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์ŒCSS ๋ณ€์ˆ˜ + ์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ + suppressHydrationWarning
์ฟ ํ‚ค ๊ธฐ๋ฐ˜ ํ…Œ๋งˆ (SSR๊ณผ๋„ ์ผ์น˜ํ•ด์•ผ ํ•จ)์„œ๋ฒ„์—์„œ ์ฟ ํ‚ค ์ฝ์–ด data-theme ์„ค์ •

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
mounted ํŒจํ„ด์€ Hydration ์—๋Ÿฌ๋ฅผ ๋ง‰๋Š” ๊ธด๊ธ‰์ฒ˜๋ฐฉ์ด์•ผ. ์‹ค๋ฌด ๋‹คํฌํ…Œ๋งˆ๋Š” CSS ๋ณ€์ˆ˜ + suppressHydrationWarning์œผ๋กœ ๊นœ๋นก์ž„๊นŒ์ง€ ํ•ด๊ฒฐํ•ด. next-themes๋ฅผ ์“ฐ๋ฉด ์ด ๋ชจ๋“  ๋ณต์žกํ•จ์„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋Œ€์‹  ์ฒ˜๋ฆฌํ•ด์ค˜.


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

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

โŒ window is not defined

์›์ธ: SSR ๊ณผ์ •์—์„œ ์ฆ‰์‹œ ์‹คํ–‰๋˜๋Š” ๋ณธ๋ฌธ ๋ ˆ๋ฒจ(Top level)์— ๋ธŒ๋ผ์šฐ์ € ์ „์šฉ ๊ฐ์ฒด๋ฅผ ์„ ์–ธํ•ด ์ฝ์—ˆ์„ ๋•Œ.
ํ•ด๊ฒฐ์ฑ…: useEffect ๋‚ด๋ถ€๋กœ ์ฝ”๋“œ๋ฅผ ๋„ฃ๊ฑฐ๋‚˜, typeof window !== 'undefined' ์กฐ๊ฑด๋ฌธ์œผ๋กœ ๊ฐ์‹ผ๋‹ค (๋‹จ ์กฐ๊ฑด๋ฌธ ๋ Œ๋”๋ง์€ ์œ„์—์„œ ๋ฐฐ์šด Mismatch ์—๋Ÿฌ๋ฅผ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ Mounted ํŒจํ„ด์„ ๋™์‹œ์— ์จ์•ผ ํ•œ๋‹ค).

โŒ Expected server HTML to contain a matching <div> in <body> ํ˜น์€ <p> cannot appear as a descendant of <p>

์›์ธ: Hydration ์—๋Ÿฌ์˜ ๋ณ€ํ˜•. ์„œ๋ฒ„์—์„œ๋Š” ๋ฌธ๋ฒ•์ด ๋งž์•˜๋Š”๋ฐ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ฝ์–ด๋ณด๋‹ˆ ๋น„์ •์ƒ์ ์ธ HTML ํŠธ๋ฆฌ๋ผ์„œ ํŒŒ์„œ๊ฐ€ ๋ฉ‹๋Œ€๋กœ DOM์„ ์ˆ˜์ •ํ•ด๋ฒ„๋ฆฐ ๊ฒฝ์šฐ. ์ฃผ๋กœ <p> ํƒœ๊ทธ ์•ˆ์— <div> ๊ฐ™์€ ๋ธ”๋ก ํƒœ๊ทธ๋ฅผ ๋„ฃ์—ˆ์„ ๋•Œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ฐ•์ œ๋กœ <p>๋ฅผ ๋‹ซ์•„๋ฒ„๋ฆฌ๋ฉด์„œ ์„œ๋ฒ„ ๋ทฐ ํŠธ๋ฆฌ์™€ ํด๋ผ์ด์–ธํŠธ ๋ทฐ ํŠธ๋ฆฌ๊ฐ€ ์–ด๊ธ‹๋‚จ.
ํ•ด๊ฒฐ์ฑ…: ์›น ํ‘œ์ค€ ๋ฌธ๋ฒ•์„ ์ฒ ์ €ํžˆ ์ง€์ผœ์„œ ๋งˆํฌ์—…ํ•œ๋‹ค. (inline ํƒœ๊ทธ ์•ˆ์— block ํƒœ๊ทธ ๊ธˆ์ง€ ๋“ฑ)


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

์ƒํ™ฉ๋ฌธ์ œ (์—๋Ÿฌ)ํ•ด๊ฒฐ์ฑ… (Pro Pattern)
๋ธŒ๋ผ์šฐ์ € ์ „์šฉ(localStorage) ๊ฐ’์„ ์ดˆ๊ธฐ UI๋กœ ์จ์•ผ ํ•  ๋•Œ์„œ๋ฒ„(๊ฐ’ ์—†์Œ) vs ํด๋ผ(๊ฐ’ ์žˆ์Œ) ๋ถˆ์ผ์น˜๋กœ Mismatch ์—๋Ÿฌ ํญ๋ฐœuseState(false)๋กœ Mounted ํ”Œ๋ž˜๊ทธ ํŒจํ„ด ์ ์šฉ. 1์ฐจ ๋กœ๋”ฉ ์Šค์ผˆ๋ ˆํ†ค, useEffect ํ›„ 2์ฐจ ๋ Œ๋”๋ง ๋„์ถœ
window.innerWidth๋ฅผ ์ฝ๊ณ  ์‹ถ์Œ์„œ๋ฒ„์—์„œ ํ„ฐ์ง (window is not defined)์—ญ์‹œ useEffect ์Šค์ฝ”ํ”„ ๋‚ด๋ถ€๋กœ ๋กœ์ง ์œ„์ž„
<p> <div> </div> </p> ํ˜•ํƒœ ์ฝ”๋”ฉ๋ธŒ๋ผ์šฐ์ € ํŒŒ์„œ๊ฐ€ DOM์„ ๊ฐ•์ œ ์ˆ˜์ •ํ•ด Mismatch ๋ฐœ์ƒ๋ถ€๋ชจ p ํƒœ๊ทธ๋ฅผ div๋กœ ๋ฐ”๊พธ๋Š” ๋“ฑ ์ฒ ์ €ํ•œ ์›น ํ˜ธํ™˜ ๋งˆํฌ์—… ๊ฒ€์ˆ˜

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ(use client) ๋ผ ํ• ์ง€๋ผ๋„ 1์ฐจ ๋ Œ๋”๋ง ๋‹จ ํ•œ ๋ฒˆ์€ ๋ฌด์กฐ๊ฑด ๊ณต์žฅ(์„œ๋ฒ„) ์—์„œ ๊ตฌ์›Œ์ ธ์„œ ์˜จ๋‹ค. ์„œ๋ฒ„์™€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ณด๋Š” 1์ฐจ ๊ทธ๋ฆผ์€ ์–ด๋–ค ์ˆ˜๊ฐ€ ์žˆ์–ด๋„ 100% ๊ฐ™์•„์•ผ ํ•œ๋‹ค. ๋ธŒ๋ผ์šฐ์ €๋งŒ์˜ ๊ณ ์œ  ์ •๋ณด๋Š” ์ˆ˜ํ™”(Hydrate) ์™„๋ฃŒ๋ฅผ ํ™•์ธ๋ฐ›์€ ๋’ค, useEffect ํƒ€์ด๋ฐ๋ถ€ํ„ฐ ๋‹น๋‹นํ•˜๊ฒŒ ์ฝ์–ด๋ผ!


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

์˜ค๋Š˜์€ ์ •๋ง ์‹์€๋•€ ํ˜๋ฆฌ๋Š” ํ•˜๋ฃจ์˜€์–ด. ๋‚ด ํ™”๋ฉด์ด ์™œ ์ฐข์–ด์ง€๋Š”์ง€(Mismatch) ๋“œ๋””์–ด ์•Œ๊ฒŒ ๋๊ฑฐ๋“ . ์„œ๋ฒ„์—์„œ ์˜จ ๋นˆ๋Œ€๋–ก์ด๋ž‘ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ตฌ์šด ๋นˆ๋Œ€๋–ก์ด ํ† ํ•‘์ด ๋‹ค๋ฅด๋ฉด ๋ฆฌ์•กํŠธ๊ฐ€ ํ™”๋ฅผ ๋‚ธ๋‹ค๋Š” ์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜ ์˜ ๋น„์œ ๊ฐ€ ๊ท€์— ์™ ๋ฐ•ํžˆ๋”๋ผ.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ์˜ ์ฒซ ๋งŒ๋‚จ์€ ์–ธ์ œ๋‚˜ 100% ์ผ์น˜ํ•ด์•ผ ํ•œ๋‹ค! ๋ธŒ๋ผ์šฐ์ € ์ •๋ณด๋Š” Hydration ์ดํ›„์—๋งŒ ๊บผ๋‚ด์ž."

typeof window !== 'undefined' ์กฐ๊ฑด๋ฌธ๋งŒ ๋ฏฟ๊ณ  ์•ˆ์ผํ•˜๊ฒŒ ์ฝ”๋”ฉํ–ˆ๋˜ ๋‚˜ ์ž์‹ ์„ ๋ฐ˜์„ฑํ•˜๋ฉฐ... ์ด์ œ๋Š” '๋งˆ์šดํŠธ ๋ณด์žฅ ํŒจํ„ด' ์„ ๋‚ด ์ฃผ๋ฌด๊ธฐ๋กœ ์‚ผ์•„์•ผ๊ฒ ์–ด. ๊ทธ๋ž˜๋„ ์—๋Ÿฌ๋ฅผ ์žก๊ณ  ๋‚˜๋‹ˆ ๋งˆ์Œ์ด ํ•œ๊ฒฐ ๊ฐ€๋ณ๋‹ค! ์˜ค๋Š˜ ์šด๋™ ๊ฐ€์„œ ๋•€ ์ข€ ์ซ™ ๋นผ๊ณ  ์ŠคํŠธ๋ ˆ์Šค ํ’€์–ด์•ผ์ง€. ๋‚ด์ผ์€ ๋ฆฌ์•กํŠธ๋ณด๋‹ค ๋” ๋‚ ์นด๋กœ์šด ๋ˆˆ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ง€์ผœ๋ณด๊ฒ ์–ด! ๐Ÿฃ


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