• ABOUT
  • PORTFOLIO
  • POSTS
  • GUESTBOOK

ยฉ 2025 BlueCool12 All rights reserved.

2025.07.22Next.js

๐ŸŒœ Next.js๋กœ ๋‹คํฌ๋ชจ๋“œ ๊ตฌํ˜„ํ•˜๊ธฐ (next-themes)

next-themes ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

next-themes๋Š” Next.js์—์„œ ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ํ…Œ๋งˆ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค. ๋ณ„๋„์˜ ์ƒํƒœ ๊ด€๋ฆฌ๋‚˜ ๋ณต์žกํ•œ ์ฝ”๋“œ ์—†์ด ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ ํ† ๊ธ€์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋‹ค์Œ ๊ธฐ๋Šฅ์„ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•œ๋‹ค.

  • ์‹œ์Šคํ…œ ํ…Œ๋งˆ ์ž๋™ ๊ฐ์ง€
  • ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•œ ํ…Œ๋งˆ๋ฅผ localStorage์— ์ €์žฅ
  • ํ…Œ๋งˆ ๋ณ€๊ฒฝ ์‹œ UI ๋ฐ˜์˜
  • ํ…Œ๋งˆ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ์–ด

์‚ฌ์šฉ ๋ฐฉ๋ฒ•์€ ๊ฐ„๋‹จํ•˜๋‹ค. 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๊ฐ€ ์ฆ‰์‹œ ๋ฐ˜์˜๋œ๋‹ค.


* hydration mismatch ๋ฌธ์ œ

๋‹คํฌ ๋ชจ๋“œ ๊ธฐ๋ณธ ์„ธํŒ…์ด ์ดํ›„ ์ถ”๊ฐ€๋กœ ๊ณ ๋ คํ•ด์•ผ ํ•  ์‚ฌํ•ญ์ด ์žˆ๋‹ค. ๋ฐ”๋กœ ์ดˆ๊ธฐ ๋ Œ๋”๋ง ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๊นœ๋นก์ž„ ํ˜„์ƒ ์ฆ‰ 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)์™€ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ๊ฐ’์„ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ํ˜„์žฌ ํ…Œ๋งˆ์— ๋งž๋Š” ๊ฐ’์ด ์ž๋™์œผ๋กœ ์ ์šฉ๋œ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํ…Œ๋งˆ ๋ณ€๊ฒฝ ์‹œ ์—ฌ๋Ÿฌ ์Šคํƒ€์ผ์„ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด์ „ ๊ธ€
๐Ÿ‘ท @Valid๋กœ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š” ์Šคํ”„๋ง ๋ถ€ํŠธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
๋‹ค์Œ ๊ธ€
๐Ÿ“ ์›น์‚ฌ์ดํŠธ ์„ฑ๋Šฅ ์ธก์ • ๋„๊ตฌ Lighthouse
์žฅ์‹์šฉ ๋กœ๊ณ