• ABOUT
  • POSTS
  • GUESTBOOK

ยฉ 2025 BlueCool12 All rights reserved.

2025.07.22Next.js

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

์ตœ๊ทผ์—๋Š” ๋‹คํฌ ๋ชจ๋“œ๊ฐ€ ์›น์‚ฌ์ดํŠธ์˜ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์ฒ˜๋Ÿผ ์ œ๊ณต๋œ๋‹ค. ํŠนํžˆ ๋ธ”๋กœ๊ทธ, ๋ฌธ์„œ ๋ทฐ์–ด, ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์ฒ˜๋Ÿผ ํ…์ŠคํŠธ ์ค‘์‹ฌ์˜ ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์„œ๋น„์Šค์—์„œ๋Š” ๊ฐ€๋…์„ฑ์ด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์˜ ํ•ต์‹ฌ ์š”์†Œ๊ฐ€ ๋œ๋‹ค. ๋ฐ์€ ํ™”๋ฉด์€ ๋ฐค์ด๋‚˜ ์–ด๋‘์šด ํ™˜๊ฒฝ์—์„œ๋Š” ๋ˆˆ๋ถ€์‹ฌ์„ ์œ ๋ฐœํ•ด ํ”ผ๋กœ๊ฐ์„ ์ค„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. Next.js๋ฅผ ์ด์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ ๋‹จ๊ณ„๋ณ„๋กœ ์•Œ์•„๋ณด์ž.
 

* next-themes

Next.js์—์„œ ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ next-themes ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ ๋‹คํฌ/๋ผ์ดํŠธ ํ…Œ๋งˆ ํ† ๊ธ€ ๊ธฐ๋Šฅ, ์‚ฌ์šฉ์ž์˜ ์‹œ์Šคํ…œ ์„ค์ • ๊ฐ์ง€ ๊ธฐ๋Šฅ, ์„ค์ •๋œ ํ…Œ๋งˆ๋ฅผ localStorage์— ์ €์žฅํ•˜๋Š” ๊ธฐ๋Šฅ ๋“ฑ์ด ๋‚ด์žฅ๋˜์–ด ์žˆ๋‹ค. ์‚ฌ์šฉ ๋ฐฉ๋ฒ•๋„ ์•„์ฃผ ๊ฐ„๋‹จํ•œ๋ฐ ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ์ ์šฉํ•  layout์—์„œ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋ฅผ <ThemeProvider>๋กœ ๊ฐ์‹ธ์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด ๋œ๋‹ค.
 

javascript
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

  • ๋‹คํฌ ๋ชจ๋“œ ํ…Œ๋งˆ๋ฅผ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ HTML ํƒœ๊ทธ์— ์ ์šฉํ• ์ง€๋ฅผ ์ •์˜ํ•œ๋‹ค.
  • โ€œclassโ€์˜ ๊ฒฝ์šฐ <html class="dark">๋กœ ์ ์šฉ๋˜๋ฉฐ โ€œdata-themeโ€์˜ ๊ฒฝ์šฐ <html data-theme="dark">์˜ ํ˜•ํƒœ๋กœ ์ ์šฉ๋œ๋‹ค.
  • Tailwind ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒฝ์šฐ class๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ์ง์ ‘ CSS๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๋Š” ๊ฒฝ์šฐ data-theme ๋ฐฉ์‹์„ ์ฃผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.


- defaultTheme

  • ์‚ฌ์šฉ์ž ์„ค์ •์ด ์—†๋Š” ๊ฒฝ์šฐ ์–ด๋–ค ํ…Œ๋งˆ๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์‚ฌ์šฉํ• ์ง€๋ฅผ ์ง€์ •ํ•œ๋‹ค.
  • โ€œlightโ€, โ€œdarkโ€, โ€œsystemโ€ ์„ธ ๊ฐ€์ง€ ์˜ต์…˜์ด ์žˆ๋‹ค.


- enableSystem

  • defaultTheme ์ด โ€œsystemโ€ ์ผ ๋•Œ ์šด์˜์ฒด์ œ์˜ ์„ค์ •์„ ๊ฐ์ง€ํ• ์ง€์˜ ์—ฌ๋ถ€๋ฅผ ์ •์˜ํ•œ๋‹ค.
  • true๋กœ ์„ค์ • ์‹œ ์‚ฌ์šฉ์ž์˜ ์‹œ์Šคํ…œ ํ…Œ๋งˆ์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ์ „ํ™˜๋œ๋‹ค.


- storageKey

  • localStorage์— ํ…Œ๋งˆ ์„ค์ •์„ ์ €์žฅํ•  ๋•Œ ์‚ฌ์šฉํ•  ํ‚ค ์ด๋ฆ„์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๊ธฐ๋ณธ๊ฐ’์€ โ€œthemeโ€์ด๋‹ค.
     


ThemeProvide๋ฅผ ์„ค์ •ํ•œ ์ดํ›„์—๋Š” useTheme() ํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์žฌ ํ…Œ๋งˆ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์ด๋ฅผ ํ† ๊ธ€ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฒ„ํŠผ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค.
 

javascript
'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๋ฅผ ๋ณด์—ฌ์ฃผ๋„๋ก ์ฒ˜๋ฆฌํ•œ๋‹ค.
 

javascript
'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 ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค.
 

javascript
<html lang="ko" suppressHydrationWarning>


๋ชจ๋“  ์„ค์ •์„ ๋งˆ์น˜๊ณ  ๋‚˜๋ฉด ๋งˆ์ง€๋ง‰์œผ๋กœ ๋‹คํฌ ๋ชจ๋“œ์™€ ๋ผ์ดํŠธ ๋ชจ๋“œ์—์„œ ๊ฐ๊ฐ ์ ์šฉํ•  CSS ์Šคํƒ€์ผ์„ globals.css ํŒŒ์ผ์— ์ •์˜ํ•ด ์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด ๋œ๋‹ค. Tailwind๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด dark: ์ ‘๋‘์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šคํƒ€์ผ์„ ๋ถ„๊ธฐํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์ง์ ‘ CSS๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ThemeProvider์˜ attribute ์„ค์ •์— ๋”ฐ๋ผ [data-theme="dark"] ํ˜น์€ .dark ์…€๋ ‰ํ„ฐ๋ฅผ ํ™œ์šฉํ•ด ํ…Œ๋งˆ๋ณ„ ์Šคํƒ€์ผ์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
 

css
/* ๋ผ์ดํŠธ ํ…Œ๋งˆ */
:root {
  --bg-color: #F8F9FA;
  --text-color: #212529;
}

/* ๋‹คํฌ ํ…Œ๋งˆ */
html[data-theme="dark"] {
  --bg-color: #121212;
  --text-color: #E0E0E0;
}

* CSS ๋ณ€์ˆ˜๋ฅผ ์ •์˜ํ•ด๋‘๋ฉด ์ „์ฒด ์Šคํƒ€์ผ์„ ํ†ต์ผํ•˜์—ฌ ์œ ์ง€ ๋ณด์ˆ˜๋ฅผ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค.
 

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