C.W.K.
Stream
Lesson 02 of 07 · published

Reload 살아남는 dark mode

~13 min · dark-mode, system-preference, tailwind

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
Dark mode 는 하나인 척하는 세 문제: 어떤 모드 활성, 선택 어디 살아남, React 마운트 전 페이지 렌더 어떻게. 셋 다 풀거나 토글이 매 reload 마다 깜빡거려.

세 레이어

  1. 현재 모드 — light, dark, 또는 'system 따라'.
  2. Persistence — 사용자 선택이 페이지 reload 살아남아야 (localStorage).
  3. 초기 렌더 — React 렌더 전에 HTML 이 이미 올바른 theme attribute 가져야 — 안 그러면 페이지가 한 프레임 동안 잘못된 색 깜빡.

Pre-React 스크립트

깜빡 피하려면 index.html 의 inline script 에서 <html> 에 theme attribute 설정 — Vite 번들 로드 전에. localStorage 읽고; prefers-color-scheme 으로 fallback. 이 스크립트가 동기적으로 돌고 페이지가 즉시 올바른 색으로 paint.

React hook

React 안에서 useTheme hook 이 현재 attribute 읽고, setter 노출, localStorage 에 write back. Hook 이 페이지 내 토글 UI 구동. Pre-React 스크립트가 부팅 처리, hook 이 사용자 구동 변경 처리.

'System 따라'

일부 사용자는 OS preference 원함. matchMedia('(prefers-color-scheme: dark)') change 이벤트 listen. 사용자가 명시적 모드 안 골랐으면 (localStorage 항목 없음) 시스템 신호 따라. 모드 고르는 순간 시스템 신호 override.

서버 사이드 기본값에 토글 렌더하고 희망하지 마. SSR (Next.js) 로 옮기면 서버가 사용자 preference 모름. Flash-free 서버 렌더 (쿠키 사용) 출하 또는 hydration 까지 theme-의존 UI 렌더 스킵. SPA 는 이 버그 클래스 전체 피함 — inline 스크립트가 다른 거 전에 돔.

Code

index.html — pre-React 부팅 스크립트·html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My App</title>
    <!-- Vite 번들 BEFORE 에 돔. Flash 없음. -->
    <script>
      (function () {
        try {
          var saved = localStorage.getItem("theme");
          var prefersLight = window.matchMedia("(prefers-color-scheme: light)").matches;
          var theme = saved || (prefersLight ? "light" : "dark");
          if (theme === "light") document.documentElement.setAttribute("data-theme", "light");
        } catch (e) { /* localStorage unavailable, dark 디폴트 */ }
      })();
    </script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
useTheme — hook + 토글 컴포넌트·tsx
import { useEffect, useState } from "react";

type Theme = "light" | "dark";

function getCurrentTheme(): Theme {
  return document.documentElement.getAttribute("data-theme") === "light"
    ? "light"
    : "dark";
}

export function useTheme() {
  const [theme, setThemeState] = useState<Theme>(getCurrentTheme);

  const setTheme = (next: Theme) => {
    setThemeState(next);
    if (next === "light") {
      document.documentElement.setAttribute("data-theme", "light");
    } else {
      document.documentElement.removeAttribute("data-theme");
    }
    try { localStorage.setItem("theme", next); } catch {}
  };

  // 사용자가 명시적 선택 안 했으면 system 따라.
  useEffect(() => {
    if (localStorage.getItem("theme")) return; // 사용자가 골랐음 — override 안 함
    const mql = window.matchMedia("(prefers-color-scheme: light)");
    const onChange = () => setTheme(mql.matches ? "light" : "dark");
    mql.addEventListener("change", onChange);
    return () => mql.removeEventListener("change", onChange);
  }, []);

  return { theme, setTheme };
}

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <button
      onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
      className="px-3 py-1 rounded border border-border text-fg hover:bg-bg-elevated"
      aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
    >
      {theme === "dark" ? "☀️" : "🌙"}
    </button>
  );
}

External links

Exercise

Bootstrap 프로젝트에 dark mode 끝까지 추가: index.html 의 pre-React 부팅 스크립트, useTheme hook, 보이는 자리 토글 버튼. 셋 확인: (1) light 모드 cold reload 가 dark flash 없음; (2) 토글이 reload 살아남음; (3) localStorage 지우고 OS 테마 바꾸면 앱이 따라옴.
Hint
브라우저 DevTools 의 Rendering 아래 'emulate prefers-color-scheme' — 실제 OS 설정 안 바꾸고 system-follow 경로 테스트 가능.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.