Post

(ํ•ด๊ฒฐ ์™„๋ฃŒ) Hydration failed because the initial UI does not match what was rendered on the server.

๐Ÿ“Œ์‹œ์ž‘ํ•˜๋ฉฐ

์ด ๊ธ€์€ ์•„๋ž˜์˜ ์˜ค๋ฅ˜๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ , ์ž‘์„ฑํ•˜๋Š” ํฌ์ŠคํŒ…์ž…๋‹ˆ๋‹ค.

โ›”๋ฌธ์ œ์ƒํ™ฉ

๋ฌธ์ œ์ƒํ™ฉ์„ ๋ณต๊ธฐํ•ด๋ณด์ž๋ฉด, Header.tsx ์ปดํฌ๋„ŒํŠธ์—์„œ, localStorage์™€ Tailwind๋ฅผ ์ด์šฉํ•ด light/dark mode๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋„์ค‘ ํ•ด๋‹น ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching <path> in <svg>. See more info here: https://nextjs.org/docs/messages/react-hydration-error

๋ Œ๋”๋ง ์‹œ์  ๋ฌธ์ œ๋ผ๋Š” ๊ฒƒ์€ ํŒŒ์•…ํ•ด์„œ, useEffect๋‚˜ useLayoutEffect ๋“ฑ์„ ์ด์šฉํ•ด ๋‚˜๋ฆ„ ํ•ด๊ฒฐ์„ ์‹œ๋„ํ•˜์˜€์œผ๋‚˜ ํ•ด๊ฒฐํ•˜์ง€ ๋ชปํ–ˆ์—ˆ๋Š”๋ฐ, ์•„์˜ˆ ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ์ž‘์„ฑํ•ด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.๐Ÿ˜Ž

โŒ๊ธฐ์กด ์ž‘์„ฑ ์ฝ”๋“œ ํ๋ฆ„

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const initialTheme: ThemeType =
  typeof window !== "undefined"
    ? (localStorage.getItem("theme") as ThemeType) || "light"
    : "light"
const [theme, setTheme] = useState<ThemeType>(initialTheme)

useEffect(() => {
  if (typeof window !== "undefined") {
    localStorage.setItem("theme", theme)
    if (theme === "dark") {
      const htmlElement = document.querySelector("html") as HTMLElement
      htmlElement.classList.add("dark")
    } else {
      const htmlElement = document.querySelector("html") as HTMLElement
      htmlElement.classList.remove("dark")
    }
  }
}, [theme])
  • initialTheme: ์ฝ”๋“œ๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์‹คํ–‰๋˜๋Š”์ง€ ํ™•์ธํ•œ ๋‹ค์Œ, ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ด์ „์— ์ €์žฅ๋œ ํ…Œ๋งˆ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. ๋งŒ์•ฝ, ์ €์žฅ๋œ ํ…Œ๋งˆ๊ฐ€ ์—†๋‹ค๋ฉด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ โ€œlightโ€ ํ…Œ๋งˆ๋ฅผ ์„ค์ •ํ•˜๊ณ  state์— ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์ง€์ •ํ•œ๋‹ค.

  • useEffect: theme ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ํ˜ธ์ถœ๋œ๋‹ค. ์ฝ”๋“œ๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์‹คํ–‰๋˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ํ…Œ๋งˆ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ƒˆ๋กœ์šด ํ…Œ๋งˆ๋ฅผ ์ €์žฅํ•œ๋‹ค. ์ด๋•Œ, dark ๋ชจ๋“œ ์ธ ๊ฒฝ์šฐ ์ตœ์ƒ์œ„ <html>์š”์†Œ์— dark ํด๋ž˜์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด tailwind์˜ darkmode๋ฅผ ์ ์šฉํ•œ๋‹ค.

๐Ÿค”์ด ์ฝ”๋“œ๋„ ๊ณ„์† ๋ฐœ์ƒํ•˜๋˜ ์˜ค๋ฅ˜๋ฅผ ํ•ด๊ฒฐํ•˜์ง€ ๋ชปํ•ด ๊ณจ๋จธ๋ฆฌ๋ฅผ ์•“์•˜๋Š”๋ฐ, ๊ฒฐ๊ตญ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์€ ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ์กฐ๊ธˆ์”ฉ ๊ณ ์น˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ๋‹ค์‹œ ์ž‘์„ฑํ•˜๋Š” ๊ฑฐ์˜€๋‹ค!๐Ÿ˜Ž

โญ•๊ฐœ์„ ํ•œ ์ฝ”๋“œ ํ๋ฆ„

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// theme ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ
const [theme, setTheme] = useState<ThemeType>("light")

//theme ์„ค์ • ํ•จ์ˆ˜๋“ค
const setLightTheme = () => {
  localStorage.setItem("theme", "light")
  setTheme("light")
  const htmlElement = document.querySelector("html") as HTMLElement
  htmlElement.classList.remove("dark")
}

const setDarkTheme = () => {
  localStorage.setItem("theme", "dark")
  setTheme("dark")
  const htmlElement = document.querySelector("html") as HTMLElement
  htmlElement.classList.add("dark")
}

useEffect(() => {
  const storedTheme = localStorage.getItem("theme") as ThemeType

  if (storedTheme) {
    if (storedTheme === "dark") {
      setDarkTheme()
    } else if (storedTheme === "light") {
      setLightTheme()
    }
  } else {
    setLightTheme()
  }
}, [])
  • useState: ๊ธฐ์กด์— initialTheme๋ฅผ ํ™•์ธํ•˜๊ณ  state ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š๊ณ , ์ฒ˜์Œ์—๋Š” ์ดˆ๊ธฐ ํ…Œ๋งˆ๋ฅผ โ€œlightโ€๋กœ ์„ค์ •ํ•ด์ฃผ์—ˆ๋‹ค.

  • useEffect: ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งˆ์šดํŠธ๋  ๋•Œ ์‹คํ–‰๋œ๋‹ค. ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ด์ „์— ์ €์žฅ๋œ ํ…Œ๋งˆ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , ์ €์žฅ๋œ ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’ โ€˜lightโ€™๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ์žˆ์œผ๋ฉด ๊ฐ๊ฐ์˜ ํ…Œ๋งˆ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

  • setLightTheme / setDarkTheme: ๊ฐ๊ฐ์˜ ํ…Œ๋งˆ๋ฅผ ์„ค์ •ํ•œ๋‹ค. ์ตœ์ƒ์œ„ <html>ํƒœ๊ทธ์— dark class๋ฅผ ๋„ฃ๊ฑฐ๋‚˜ ์ œ๊ฑฐํ•˜์—ฌ Tailwind์˜ darkmode๊ฐ€ ์ ์šฉ๋˜๋„๋ก ํ•œ๋‹ค.

    • ๊ธฐ์กด์˜ ์ฝ”๋“œ์—์„œ๋Š” ์ด ๋ถ€๋ถ„์„ useEffect๋กœ theme๊ฐ€ ๋ฐ”๋€”๋•Œ๋งˆ๋‹ค ์‹คํ–‰๋˜๊ฒŒ ํ–ˆ์ง€๋งŒ, ์ด ์ฝ”๋“œ์—์„œ๋Š” dark/light ์•„์ด์ฝ˜์„ ํด๋ฆญํ•˜๋ฉด ๊ฐ ์•„์ด์ฝ˜์— ํ•ด๋‹นํ•˜๋Š” ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค.

โœ…๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฝ๊ณ  ๋ณต๊ธฐ

๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฝ์œผ๋ฉด์„œ ํ•ด๋‹น ์—๋Ÿฌ๋ฅผ ๋‹ค์‹œ ๋ณต๊ธฐํ–ˆ๋Š”๋ฐ, ์ž˜๋ชป ์ดํ•ดํ•œ ๋ถ€๋ถ„์ด ์žˆ๋˜ ๊ฒƒ์„ ๊นจ๋‹ฌ์•˜๊ณ , ์ด ๋ถ€๋ถ„๋„ ์ˆ˜์ •ํ•ด์ฃผ์—ˆ๋‹ค.๐Ÿ˜…

ํ•ด๋‹น ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ

  1. ๋ Œ๋”๋ง ๋กœ์ง์—์„œ typeof window !== 'undefined'์„ ์ฒดํฌํ•  ๋•Œ
  2. ๋ธŒ๋ผ์šฐ์ € ์ „์šฉ API์ธ window๋‚˜ localStorage๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ

์ฒ˜์Œ ์ดํ•ดํ•  ๋• 1๋ฒˆ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด ์—๋Ÿฌ๋ฅผ ์—†์•จ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์œผ๋กœ ์˜คํ•ดํ–ˆ์—ˆ๋‹คโ€ฆ๐Ÿฅฒ ์ดํ›„ ์ด ๋ถ€๋ถ„์„ ์‚ญ์ œ ํ•ด์ฃผ ์—ˆ๊ณ , ๋ธŒ๋ผ์šฐ์ € ์ „์šฉ API์ธ localStorage๋Š” useEffect์—์„œ ๋งˆ์šดํŠธ ๋œ ์ดํ›„์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ์ฝ”๋“œ๋ฅผ ๊ฐœ์„ ํ•˜๋ฉฐ, ์ด ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

This post is licensed under CC BY 4.0 by the author.