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