์ต๊ทผ์๋ ๋คํฌ ๋ชจ๋๊ฐ ์น์ฌ์ดํธ์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ์ฒ๋ผ ์ ๊ณต๋๋ค. ํนํ ๋ธ๋ก๊ทธ, ๋ฌธ์ ๋ทฐ์ด, ๊ฐ๋ฐ์ ๋๊ตฌ์ฒ๋ผ ํ
์คํธ ์ค์ฌ์ ์ฝํ
์ธ ๋ฅผ ์ ๊ณตํ๋ ์๋น์ค์์๋ ๊ฐ๋
์ฑ์ด ์ฌ์ฉ์ ๊ฒฝํ์ ํต์ฌ ์์๊ฐ ๋๋ค. ๋ฐ์ ํ๋ฉด์ ๋ฐค์ด๋ ์ด๋์ด ํ๊ฒฝ์์๋ ๋๋ถ์ฌ์ ์ ๋ฐํด ํผ๋ก๊ฐ์ ์ค ์ ์๊ธฐ ๋๋ฌธ์ด๋ค. Next.js๋ฅผ ์ด์ฉํ๋ฉด ์ฝ๊ฒ ๋คํฌ ๋ชจ๋๋ฅผ ๊ตฌํํ ์ ์๋๋ฐ ๋จ๊ณ๋ณ๋ก ์์๋ณด์.
Next.js์์ ๋คํฌ ๋ชจ๋๋ฅผ ๊ฐ๋จํ๊ฒ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ next-themes ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค. ๊ฐ๋ฐ์๊ฐ ์ง์ ์ฝ๋๋ฅผ ์์ฑํ์ง ์์๋ ๋คํฌ/๋ผ์ดํธ ํ
๋ง ํ ๊ธ ๊ธฐ๋ฅ, ์ฌ์ฉ์์ ์์คํ
์ค์ ๊ฐ์ง ๊ธฐ๋ฅ, ์ค์ ๋ ํ
๋ง๋ฅผ localStorage์ ์ ์ฅํ๋ ๊ธฐ๋ฅ ๋ฑ์ด ๋ด์ฅ๋์ด ์๋ค. ์ฌ์ฉ ๋ฐฉ๋ฒ๋ ์์ฃผ ๊ฐ๋จํ๋ฐ ๋คํฌ ๋ชจ๋๋ฅผ ์ ์ฉํ layout์์ ์์ ์ปดํฌ๋ํธ๋ฅผ <ThemeProvider>๋ก ๊ฐ์ธ์ฃผ๊ธฐ๋ง ํ๋ฉด ๋๋ค.
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="data-theme"
defaultTheme="system"
enableSystem={true}
>
{children}
</ThemeProvder>
);
}
ThemeProvider ์ปดํฌ๋ํธ๋ ์ฌ๋ฌ ์ต์
์ props๋ก ์ ๊ณตํ๋ค. ๊ทธ์ค์์๋ ์์ฃผ ์ฌ์ฉ๋๋ ์ฃผ์ ์ค์ ์ ๋ค์๊ณผ ๊ฐ๋ค.
- attribute
- defaultTheme
- enableSystem
- storageKey
ThemeProvide๋ฅผ ์ค์ ํ ์ดํ์๋ useTheme() ํ
์ ์ฌ์ฉํ์ฌ ํ์ฌ ํ
๋ง ์ํ๋ฅผ ๊ฐ์ ธ์ค๊ณ ์ด๋ฅผ ํ ๊ธ ํ ์ ์๋ ๋ฒํผ๋ง ์ถ๊ฐํ๋ฉด ๋๋ค.
'use client';
import { useTheme } from 'next-themes';
import { MdOutlineDarkMode, MdOutlineLightMode } from 'react-icons/md';
export const Header: React.FC = () => {
const { theme, resolvedTheme, setTheme } = useTheme();
return (
// ๋คํฌ ๋ชจ๋ ํ ๊ธ ๋ฒํผ
<div onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
{resolvedTheme === 'light' ? (
<MdOutlineDarkMode size={24} />
) : (
<MdOutlineLightMode size={24} />
)}
</div>
);
}
resolvedTheme์ ๊ฒฝ์ฐ next-themes์์ ์ ๊ณตํ๋ ๊ฐ ์ค ํ๋๋ก ํ์ฌ ์ ์ฉ๋ ํ
๋ง๋ฅผ ์๋ฏธํ๋ค. ์ฌ์ฉ์๊ฐ theme ๊ฐ์ โsystemโ์ผ๋ก ์ค์ ํ ๊ฒฝ์ฐ ๋ธ๋ผ์ฐ์ ์ ์์คํ
ํ
๋ง์ ๋ฐ๋ผ ์ค์ ์ ์ฉ๋ ํ
๋ง๊ฐ ๋ฌ๋ผ์ง๋ฏ๋ก ์ด๋์ ์ ํํ ์ํ๋ฅผ ํ์
ํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ค.
์ด๋ ๊ฒ ๊ธฐ๋ณธ์ ์ธ ๋คํฌ ๋ชจ๋ ์ธํ
์ด ๋๋๊ณ ๋๋ฉด ์ถ๊ฐ์ ์ผ๋ก ๊ณ ๋ คํด์ผ ํ ์ฌํญ์ด ์๋ค. ๋ฐ๋ก ์ด๊ธฐ ๋ ๋๋ง ์ ๋ฐ์ํ ์ ์๋ ๊น๋นก์ ํ์ ๋ค๋ฅธ ๋ง๋ก FOUC(Flash of Unstyled Content) ํ์์ด๋ค.
Next.js๋ฅผ ์ด์ฉํ ์๋ฒ์ฌ์ด๋ ๋ ๋๋ง์ ๊ตฌํํ๋ฉด ์ฌ์ฉ์๊ฐ ํ์ด์ง์ ์ ์ํ์ ๋ localStorage์ ์ ์ฅ๋์ด ์๋ ๋คํฌ ๋ชจ๋ ์ค์ ์ ์๋ฒ ์ธก์์๋ ์ ์ ์๊ณ ์ด๋ก ์ธํด ์ฌ์ฉ์๊ฐ ์ด์ ์ ๋คํฌ ๋ชจ๋๋ฅผ ์ค์ ํด๋์๋๋ผ๋ ๊ธฐ๋ณธ ํ
๋ง์ธ ๋ผ์ดํธ ํ
๋ง๋ก HTML์ ๋ ๋๋ง ํ๊ฒ ๋๋ค.
์ดํ ํด๋ผ์ด์ธํธ ์ธก์์ ์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋๋ฉด์ localStorage์ ๋คํฌ ๋ชจ๋ ์ค์ ์ ๋ฐ๋ผ ํ
๋ง๋ฅผ ๋ฐ๊พธ๊ฒ ๋๋๋ฐ ์ด ๊ณผ์ ์์ UI๊ฐ ํ ๋ฒ ๊น๋นก์ด๋ ํ์์ด ๋ฐ์ํ๋ค.
์ด๋ฌํ ํ์์ ๋ฐฉ์งํ๊ธฐ ์ํด ํด๋ผ์ด์ธํธ๊ฐ ํ
๋ง ์ ๋ณด๋ฅผ ์ฝ๊ธฐ ์ ๊น์ง ํ
๋ง์ ์์กดํ๋ UI ์์๋ฅผ ์ผ์์ ์ผ๋ก ์จ๊ธฐ๊ฑฐ๋ ๋ ๋๋ง ํ์ง ์์ ํ์๊ฐ ์๋ค. ๋ฐ๋ผ์ useTheme() ํ
์ ์ฌ์ฉํ ๋ mounted ์ํ๋ฅผ ํ์ฉํ์ฌ ํด๋ผ์ด์ธํธ์์ ๋ง์ดํธ๊ฐ ์๋ฃ๋์์ ๋๋ง UI๋ฅผ ๋ณด์ฌ์ฃผ๋๋ก ์ฒ๋ฆฌํ๋ค.
'use client';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
import { MdOutlineDarkMode, MdOutlineLightMode } from 'react-icons/md';
export const Header: React.FC = () => {
const { theme, resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// ํด๋ผ์ด์ธํธ์์ ๋ง์ดํธ๊ฐ ๋์ง ์์ผ๋ฉด ๋ ๋๋ง ํ์ง ์๋๋ค.
if (!mounted) return null;
return (
// ๋คํฌ ๋ชจ๋ ํ ๊ธ ๋ฒํผ...(์๋ต)
);
}
์ถ๊ฐ๋ก SSR๊ณผ CSR์ด ์ผ์นํ์ง ์์ ๊ฒฝ์ฐ Hydration mismatch ๊ฒฝ๊ณ ๊ฐ ๋ฐ์ํ๊ฒ ๋๋๋ฐ ์ด๋ฅผ ์จ๊ธฐ๊ณ ์ถ๋ค๋ฉด HTML ํ๊ทธ ํน์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ํ๊ทธ์ suppressHydrationWarning ์ ์ถ๊ฐํ๋ฉด ๋๋ค.
<html lang="ko" suppressHydrationWarning>
๋ชจ๋ ์ค์ ์ ๋ง์น๊ณ ๋๋ฉด ๋ง์ง๋ง์ผ๋ก ๋คํฌ ๋ชจ๋์ ๋ผ์ดํธ ๋ชจ๋์์ ๊ฐ๊ฐ ์ ์ฉํ CSS ์คํ์ผ์ globals.css ํ์ผ์ ์ ์ํด ์ฃผ๊ธฐ๋ง ํ๋ฉด ๋๋ค. Tailwind๋ฅผ ์ฌ์ฉํ๋ค๋ฉด dark: ์ ๋์ด๋ฅผ ์ฌ์ฉํ์ฌ ์คํ์ผ์ ๋ถ๊ธฐํ ์ ์์ง๋ง ์ง์ CSS๋ฅผ ์์ฑํ๋ ๊ฒฝ์ฐ์๋ ThemeProvider์ attribute ์ค์ ์ ๋ฐ๋ผ [data-theme="dark"] ํน์ .dark ์
๋ ํฐ๋ฅผ ํ์ฉํด ํ
๋ง๋ณ ์คํ์ผ์ ์ ์ฉํ ์ ์๋ค.
/* ๋ผ์ดํธ ํ
๋ง */
:root {
--bg-color: #F8F9FA;
--text-color: #212529;
}
/* ๋คํฌ ํ
๋ง */
html[data-theme="dark"] {
--bg-color: #121212;
--text-color: #E0E0E0;
}
* CSS ๋ณ์๋ฅผ ์ ์ํด๋๋ฉด ์ ์ฒด ์คํ์ผ์ ํต์ผํ์ฌ ์ ์ง ๋ณด์๋ฅผ ์ฝ๊ฒ ํ ์ ์๋ค.
