๐Ÿ”ฅ Trouble Shooting - Next.js ๋‹คํฌ๋ชจ๋“œ ๊ตฌํ˜„ ์ค‘ ๋ฐœ์ƒํ•œ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ์˜ค๋ฅ˜..

์Š˜ยท2025๋…„ 3์›” 14์ผ

๐Ÿ”ฅ Trouble Shooting

๋ชฉ๋ก ๋ณด๊ธฐ
13/23

๐Ÿ‘€ ๋ฌธ์ œ ์ƒํ™ฉ

Next.js์—์„œ next-themes ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹คํฌ๋ชจ๋“œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ์ค‘ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์ฝ˜์†”์— ๋‚˜ํƒ€๋‚ฌ๋‹ค:

hook.js:608 Warning: Extra attributes from the server: class,style 
Error Component Stack
    at html ()
    at RootLayout [Server] ()

์ด ์˜ค๋ฅ˜๋Š” ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง(SSR)๊ณผ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง(CSR) ์‚ฌ์ด์— ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‚˜ํƒ€๋‚œ๋‹ค. ์ฃผ๋กœ next-themes์™€ ๊ฐ™์ด ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๋™์ž‘ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง๋œ HTML๊ณผ ํด๋ผ์ด์–ธํŠธ์—์„œ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ํ›„์˜ DOM์„ ๋น„๊ตํ•  ๋•Œ ์ฐจ์ด๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ๋ฐœ์ƒํ•œ๋‹ค.

๐Ÿ” Next.js์˜ ๋ Œ๋”๋ง ๊ณผ์ •๊ณผ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋ถˆ์ผ์น˜ ๋ฌธ์ œ

๐Ÿ›œ ์„œ๋ฒ„ ์ธก ๋ Œ๋”๋ง (SSR) ๋‹จ๊ณ„

  1. ์‚ฌ์šฉ์ž๊ฐ€ ์›นํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๋ฉด, Next.js๋Š” ์„œ๋ฒ„์—์„œ React ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‹คํ–‰ํ•จ
  2. ์ด๋•Œ next-themes์™€ ๊ฐ™์€ ํด๋ผ์ด์–ธํŠธ ์ธก ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์•„์ง ์™„์ „ํžˆ ์ž‘๋™ํ•˜์ง€ ์•Š์€ ์ƒํƒœ
  3. ์„œ๋ฒ„๋Š” theme ๊ฐ’์„ ์•Œ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ธฐ๋ณธ๊ฐ’๋งŒ ์•Œ๊ณ  ์žˆ์Œ(์˜ˆ: 'light').
  4. ๋”ฐ๋ผ์„œ ์„œ๋ฒ„๋Š” ์ด ๊ธฐ๋ณธ๊ฐ’ ๊ธฐ์ค€์œผ๋กœ HTML์„ ์ƒ์„ฑ(์˜ˆ: ๋ผ์ดํŠธ ๋ชจ๋“œ ๋ฒ„ํŠผ UI).
  5. ์ƒ์„ฑ๋œ HTML์ด ์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ €๋กœ ์ „์†ก๋œ ์ƒํƒœ

๐Ÿ‘ฉโ€๐Ÿ’ป ํด๋ผ์ด์–ธํŠธ ์ธก ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋‹จ๊ณ„

  1. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ HTML์„ ๋ฐ›์œผ๋ฉด, React๊ฐ€ "ํ•˜์ด๋“œ๋ ˆ์ด์…˜" ๊ณผ์ •์„ ์‹œ์ž‘
  2. ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ์ค‘์— next-themes ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜๊ณ  ์‹ค์ œ ํ…Œ๋งˆ ๊ฐ’ ํ™•์ธ
  3. ์ด๋•Œ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅ๋œ ์‚ฌ์šฉ์ž์˜ ํ…Œ๋งˆ ์„ค์ •(์˜ˆ: 'dark')์ด ์žˆ๋‹ค๋ฉด ๊ทธ๊ฒƒ์„ ๋กœ๋“œ
  4. React๋Š” ์ด ์‹ค์ œ ํ…Œ๋งˆ ๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ DOM์„ ์—…๋ฐ์ดํŠธํ•˜๋ ค๊ณ  ํ•จ

โš ๏ธ ๋ถˆ์ผ์น˜ ๋ฐœ์ƒ

์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ:

  • ์„œ๋ฒ„๋Š” ๊ธฐ๋ณธ ํ…Œ๋งˆ(์˜ˆ: 'light')๋กœ HTML์„ ์ƒ์„ฑ
  • ํด๋ผ์ด์–ธํŠธ๋Š” ์‹ค์ œ ์‚ฌ์šฉ์ž ์„ค์ •(์˜ˆ: 'dark')์— ๋”ฐ๋ผ DOM์„ ์—…๋ฐ์ดํŠธํ•˜๋ ค ์‹œ๋„
  • React๋Š” ์„œ๋ฒ„์—์„œ ์ƒ์„ฑํ•œ HTML๊ณผ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ Œ๋”๋งํ•˜๋ ค๋Š” ๊ฒฐ๊ณผ๊ฐ€ ๋‹ค๋ฅด๋‹ค๋Š” ๊ฒƒ์„ ๊ฐ์ง€
  • ์ด ์ฐจ์ด๊ฐ€ "ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋ถˆ์ผ์น˜" ๊ฒฝ๊ณ ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ  ์žˆ์Œ!

๐Ÿงฉ ๋ฌธ์ œ์˜ ์ฝ”๋“œ

๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค:

"use client";
import { useTheme } from "next-themes";
import React, { useEffect, useState } from "react";
import { IoMoon } from "react-icons/io5";
import { FaSun } from "react-icons/fa";
import cn from "clsx";

const ThemeChange = () => {
  const DEFAULT_BUTTON_WRAPPER = "right-4 bottom-4 rounded-4xl w-[48px] h-[48px] fixed hover:scale-120 transition-all cursor-pointer";
  const DEFAULT_ICON_STYLE = "my-0 mx-auto trans absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2";
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  return (
    <>
      {theme !== "dark" ? (
        <div onClick={() => setTheme("dark")} className={cn(DEFAULT_BUTTON_WRAPPER, "bg-blue-950")}>
          <IoMoon className={cn(DEFAULT_ICON_STYLE, "text-gray-100")} />
        </div>
      ) : (
        <div onClick={() => setTheme("light")} className={cn(DEFAULT_BUTTON_WRAPPER, "bg-blue-300")}>
          <FaSun className={cn(DEFAULT_ICON_STYLE, "text-yellow-500")} />
        </div>
      )}
    </>
  );
};

export default ThemeChange;

๐Ÿค” ์›์ธ ๋ถ„์„

์ด ์˜ค๋ฅ˜์˜ ์ฃผ์š” ์›์ธ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค:

  1. ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋ถˆ์ผ์น˜: ์„œ๋ฒ„์—์„œ๋Š” theme ๊ฐ’์„ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ Œ๋”๋งํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ์‹ค์ œ ํ…Œ๋งˆ ๊ฐ’์„ ์ ์šฉํ•˜์—ฌ ๋ Œ๋”๋งํ•œ๋‹ค. ์ด๋กœ ์ธํ•ด ๋‘ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๊ฐ€ ๋‹ฌ๋ผ์ง„๋‹ค.

๐Ÿ’ก ํ•ด๊ฒฐ ๊ณผ์ •

  1. ์ฒซ์งธ๋กœ, ์ผ๋ฐ˜์ ์ธ ๋ฐฉ์‹์ธ mounted ์ƒํƒœ๋ฅผ ๋„์ž…ํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ๋งˆ์šดํŠธ๋œ ํ›„์—๋งŒ ๋ Œ๋”๋ง๋˜๋„๋ก ํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๊ฒƒ๋งŒ์œผ๋กœ๋Š” ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜์ง€ ์•Š์•˜๋‹ค.

  2. ์บ์‹œ ๋ฌธ์ œ์ธ์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ•๋ ฅ ์ƒˆ๋กœ๊ณ ์นจ๋„ ์‹œ๋„ํ•ด ๋ณด์•˜์ง€๋งŒ, ์˜ค๋ฅ˜๋Š” ๊ณ„์† ๋ฐœ์ƒํ–ˆ๋‹ค.

  3. ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ณด๋˜ ์ค‘ suppressHydrationWarning ์†์„ฑ์„ ๋ฐœ๊ฒฌํ–ˆ๋‹ค. ์ด ์†์„ฑ์€ React์—์„œ ํŠน์ • ์š”์†Œ์˜ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋ถˆ์ผ์น˜ ๊ฒฝ๊ณ ๋ฅผ ๋ฌด์‹œํ•˜๋„๋ก ์ง€์‹œํ•œ๋‹ค.

โœ… ์ตœ์ข… ํ•ด๊ฒฐ์ฑ…

div ์š”์†Œ์— suppressHydrationWarning ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ๋‹ค:

  return (
    <html lang="en" suppressHydrationWarning> // ์†์„ฑ ์ถ”๊ฐ€
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased bg-[#5b5f5e] relative`}
      >
        ...
      </body>
    </html>

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

๐Ÿ“š ๊ตํ›ˆ

์ด ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…์„ ํ†ตํ•ด ๋ฐฐ์šด ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค:

  1. SSR๊ณผ CSR์˜ ์ฐจ์ด์  ์ดํ•ด: Next.js์—์„œ ์„œ๋ฒ„ ๋ Œ๋”๋ง๊ณผ ํด๋ผ์ด์–ธํŠธ ๋ Œ๋”๋ง ์‚ฌ์ด์˜ ์ฐจ์ด์ ์„ ๋ช…ํ™•ํžˆ ์ดํ•ดํ•ด์•ผ ํ•œ๋‹ค.

  2. ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๊ณผ์ •์˜ ์ค‘์š”์„ฑ: React์˜ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ถˆ์ผ์น˜๋ฅผ ์˜ˆ๋ฐฉํ•˜๊ณ  ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„์•ผ ํ•œ๋‹ค.

  3. ํ…Œ๋งˆ ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์ : next-themes์™€ ๊ฐ™์€ ํด๋ผ์ด์–ธํŠธ ์ธก ์ƒํƒœ์— ์˜์กดํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” SSR๊ณผ์˜ ํ˜ธํ™˜์„ฑ์„ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.

  4. React์˜ ํŠน์ˆ˜ ์†์„ฑ ํ™œ์šฉ: suppressHydrationWarning๊ณผ ๊ฐ™์€ React์˜ ํŠน์ˆ˜ ์†์„ฑ์ด ๋ฌธ์ œ ํ•ด๊ฒฐ์— ๋„์›€์ด ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค.

์ด๋ฒˆ ๊ฒฝํ—˜์„ ํ†ตํ•ด Next.js์—์„œ์˜ ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ๋ Œ๋”๋ง ์ฐจ์ด์— ๋Œ€ํ•œ ์ดํ•ด๋„๊ฐ€ ๋”์šฑ ๊นŠ์–ด์กŒ๋‹ค. ๋‹ค์Œ์— ๋น„์Šทํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋” ํšจ์œจ์ ์œผ๋กœ ๋Œ€์ฒ˜ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

profile
์ฃผ๋‹ˆ์–ด ํ”„๋ก ํŠธ์—”๋“œ ์„ฑ์žฅ๊ธฐ ๊ธฐ๋ก๊ธฐ๋ก

0๊ฐœ์˜ ๋Œ“๊ธ€