๐ Next.js 4์ฅ: Hydration๊ณผ Mismatch โ ๋ด ํ๋ฉด์ด ์ฐข์ด์ง ์ด์
๐ ๊ฐ์
Hydration ์๋ฆฌ์ Mismatch ์๋ฌ๊ฐ ๋ฐ์ํ๋ ์ด์ , ๊ทธ๋ฆฌ๊ณ ์ค์ ๋๋ฒ๊น ์ ๋ต์ ๋ค๋ฃน๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ Hydration (์ํ) โ ๋น HTML์ ์๋ช ๋ถ์ด๋ฃ๊ธฐ ๐ข
- ๐จ Text content did not match (Mismatch ์๋ฌ์ ๊ณตํฌ) ๐ก
- ๐ก๏ธ ์ฐ์ํ ํด๊ฒฐ์ฑ
:
useEffect+ State ๋ง์ดํธ ์ง์ฐ ๐ก - ๐ ๋คํฌํ
๋ง
mountedํจํด์ ์ฑ๋ฅ ํธ๋ ์ด๋์คํ์ ๋ฒ ์คํธ ํ๋ํฐ์ค ๐ก - ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 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>
)
}[๋ฆฌ์กํธ ์์ง์ด ํ๋ฅผ ๋ด๋ ๊ณผ์ ]
- ์๋ฒ(Node.js): "์,
window๊ฐ ์๊ตฐ. ๋๋ ์ด ์ปดํฌ๋ํธ๋ฅผ ๋ฌด์กฐ๊ฑดlightํ์์ ํ ๋ง์ HTML ๊ป๋ฐ๊ธฐ๋ก ๋ง๋ค์ด์ ๋ณด๋ด์ผ์ง." - ๋ธ๋ผ์ฐ์ : ํ์ ๊ป๋ฐ๊ธฐ๋ฅผ ๋ฐ์์ด. ๋์๋ ํ์ ํ๋ฉด์ด ๋ณด์ฌ.
- ๋ธ๋ผ์ฐ์ (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' ์กฐ๊ฑด๋ฌธ๋ง ๋ฏฟ๊ณ ์์ผํ๊ฒ ์ฝ๋ฉํ๋ ๋ ์์ ์ ๋ฐ์ฑํ๋ฉฐ... ์ด์ ๋ '๋ง์ดํธ ๋ณด์ฅ ํจํด' ์ ๋ด ์ฃผ๋ฌด๊ธฐ๋ก ์ผ์์ผ๊ฒ ์ด. ๊ทธ๋๋ ์๋ฌ๋ฅผ ์ก๊ณ ๋๋ ๋ง์์ด ํ๊ฒฐ ๊ฐ๋ณ๋ค! ์ค๋ ์ด๋ ๊ฐ์ ๋ ์ข ์ซ ๋นผ๊ณ ์คํธ๋ ์ค ํ์ด์ผ์ง. ๋ด์ผ์ ๋ฆฌ์กํธ๋ณด๋ค ๋ ๋ ์นด๋ก์ด ๋์ผ๋ก ์ฝ๋๋ฅผ ์ง์ผ๋ณด๊ฒ ์ด! ๐ฃ