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-themes์ ๊ฐ์ ํด๋ผ์ด์ธํธ ์ธก ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์์ง ์์ ํ ์๋ํ์ง ์์ ์ํtheme ๊ฐ์ ์ ์ ์๊ฑฐ๋ ๊ธฐ๋ณธ๊ฐ๋ง ์๊ณ ์์(์: 'light').next-themes ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ด๊ธฐํ๋๊ณ ์ค์ ํ
๋ง ๊ฐ ํ์ธ์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์:
๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ๋ค:
"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;
์ด ์ค๋ฅ์ ์ฃผ์ ์์ธ์ ๋ค์๊ณผ ๊ฐ๋ค:
theme ๊ฐ์ ์ ์ ์๊ธฐ ๋๋ฌธ์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ๋ ๋๋งํ๊ณ , ํด๋ผ์ด์ธํธ์์๋ ์ค์ ํ
๋ง ๊ฐ์ ์ ์ฉํ์ฌ ๋ ๋๋งํ๋ค. ์ด๋ก ์ธํด ๋ ๋ ๋๋ง ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ง๋ค.์ฒซ์งธ๋ก, ์ผ๋ฐ์ ์ธ ๋ฐฉ์์ธ mounted ์ํ๋ฅผ ๋์
ํ์ฌ ์ปดํฌ๋ํธ๊ฐ ํด๋ผ์ด์ธํธ์ ๋ง์ดํธ๋ ํ์๋ง ๋ ๋๋ง๋๋๋ก ํ๋ค. ํ์ง๋ง ์ด๊ฒ๋ง์ผ๋ก๋ ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋์ง ์์๋ค.
์บ์ ๋ฌธ์ ์ธ์ง ํ์ธํ๊ธฐ ์ํด ๊ฐ๋ ฅ ์๋ก๊ณ ์นจ๋ ์๋ํด ๋ณด์์ง๋ง, ์ค๋ฅ๋ ๊ณ์ ๋ฐ์ํ๋ค.
๋ค์ํ ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด๋ ์ค suppressHydrationWarning ์์ฑ์ ๋ฐ๊ฒฌํ๋ค. ์ด ์์ฑ์ React์์ ํน์ ์์์ ํ์ด๋๋ ์ด์
๋ถ์ผ์น ๊ฒฝ๊ณ ๋ฅผ ๋ฌด์ํ๋๋ก ์ง์ํ๋ค.
div ์์์ suppressHydrationWarning ์์ฑ์ ์ถ๊ฐํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ค:
return (
<html lang="en" suppressHydrationWarning> // ์์ฑ ์ถ๊ฐ
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-[#5b5f5e] relative`}
>
...
</body>
</html>
์ด ์์ฑ์ ์ถ๊ฐํ ํ์๋ ํ์ด๋๋ ์ด์ ๊ฒฝ๊ณ ๊ฐ ๋ ์ด์ ๋ํ๋์ง ์์๊ณ , ๋คํฌ๋ชจ๋ ๊ธฐ๋ฅ์ด ์ ์์ ์ผ๋ก ์๋ํ๊ฒ ๋์๋ค.
์ด ํธ๋ฌ๋ธ์ํ ์ ํตํด ๋ฐฐ์ด ์ ์ ๋ค์๊ณผ ๊ฐ๋ค:
SSR๊ณผ CSR์ ์ฐจ์ด์ ์ดํด: Next.js์์ ์๋ฒ ๋ ๋๋ง๊ณผ ํด๋ผ์ด์ธํธ ๋ ๋๋ง ์ฌ์ด์ ์ฐจ์ด์ ์ ๋ช ํํ ์ดํดํด์ผ ํ๋ค.
ํ์ด๋๋ ์ด์ ๊ณผ์ ์ ์ค์์ฑ: React์ ํ์ด๋๋ ์ด์ ๊ณผ์ ์์ ๋ฐ์ํ ์ ์๋ ๋ถ์ผ์น๋ฅผ ์๋ฐฉํ๊ณ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ์์์ผ ํ๋ค.
ํ
๋ง ๊ด๋ จ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ์ ์ฃผ์์ : next-themes์ ๊ฐ์ ํด๋ผ์ด์ธํธ ์ธก ์ํ์ ์์กดํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๋๋ SSR๊ณผ์ ํธํ์ฑ์ ๊ณ ๋ คํด์ผ ํ๋ค.
React์ ํน์ ์์ฑ ํ์ฉ: suppressHydrationWarning๊ณผ ๊ฐ์ React์ ํน์ ์์ฑ์ด ๋ฌธ์ ํด๊ฒฐ์ ๋์์ด ๋ ์ ์๋ค๋ ์ ์ ์๊ฒ ๋์๋ค.
์ด๋ฒ ๊ฒฝํ์ ํตํด Next.js์์์ ํด๋ผ์ด์ธํธ-์๋ฒ ๋ ๋๋ง ์ฐจ์ด์ ๋ํ ์ดํด๋๊ฐ ๋์ฑ ๊น์ด์ก๋ค. ๋ค์์ ๋น์ทํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด ๋ ํจ์จ์ ์ผ๋ก ๋์ฒํ ์ ์์ ๊ฒ์ด๋ค.