프론트엔드·2025. 04. 02·4 min read

Tailwind CSS로 Dark Mode 구현하기

이번에 블로그를 만들면서, 블로그에 다크 모드를 구현하면 좋겠다 싶었다.
마침 Tailwind CSS에서 Dark Mode를 사용할 수 있는 유틸리티 클래스를 제공해서, 이를 통해 구현을 해보았다.

1. Tailwind Custom Variant 설정

먼저, Tailwind를 import하고 있는 global.css 파일을 수정한다.

@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));

@custom-variant 를 통해 다크모드 변수를 만들어준다.
위 클래스(변수)를 html 태그에 삽입하면 다크모드를 수동으로 토글이 가능하다.

2. Dark Mode Custom Hook 구현

다크모드를 설정하기 위한 커스텀 훅을 구현한다.

import { useEffect, useState } from "react";

const useDarkMode = () => {
  const [isDark, setIsDark] = useState<boolean | undefined>(undefined);

  useEffect(() => {
    if (!window.localStorage.getItem("isDark")) {
      window.localStorage.setItem(
        "isDark",
        window.matchMedia("(prefers-color-scheme: dark)").matches.toString(),
      );
    }

    const isBrowserDarkMode = window.localStorage.getItem("isDark") === "true";

    setIsDark(isBrowserDarkMode);
  }, []);

  useEffect(() => {
    if (isDark) {
      document.documentElement.classList.add("dark");
      window.localStorage.setItem("isDark", "true");
    }

    if (!isDark && isDark !== undefined) {
      document.documentElement.classList.remove("dark");
      window.localStorage.setItem("isDark", "false");
    }
  }, [isDark]);

  const toggleDark = () => setIsDark((prev) => !prev);

  return { isDark, toggleDark };
};

export default useDarkMode;

사용자의 다크모드 설정은 localStorage에 저장되어 사용자의 설정을 기억하게 된다.
그리고 window.matchMedia 함수를 통해 현재 사용자 브라우저의 색상 스키마 (라이트/다크) 여부를 확인한다.

3. 토글 버튼 구현

다크모드 토글을 위한 버튼을 구현한다.

"use client";

import React from "react";
import useDarkMode from "@/hooks/useDarkMode";
import LightModeIcon from "@/components/LightModeIcon";
import DarkModeIcon from "@/components/DarkModeIcon";

const ToggleDarkMode = () => {
  const { isDark, toggleDark } = useDarkMode();

  return (
    <header>
      <div>
        <button className="cursor-pointer" onClick={() => toggleDark()}>
          {isDark && <LightModeIcon />}
          {!isDark && isDark !== undefined && (
            <DarkModeIcon />
          )}
        </button>
      </div>
    </header>
  );
};

export default ToggleDarkMode;

4. 배경 색상 깜빡임 문제 해결

처음 페이지를 들어올 때 사용자의 다크 모드가 활성화 되어있음에도 불구하고 라이트 모드 -> 다크 모드로 전환되는 현상이 발생했다.
그래서 동적 스크립트를 삽입하여 html 태그에 동적으로 dark 클래스를 삽입해준다.

const themeScript = `
  (() => {
    const isDark = window.localStorage.getItem("isDark");
    if (!isDark && window.matchMedia("(prefers-color-scheme: dark)").matches) 
      document.documentElement.classList.add("dark");
    if (isDark === "true") document.documentElement.classList.add("dark");
  })();
`;

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ko" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: themeScript }} />
      </head>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}

이때, html 태그 정보가 서버와 클라이언트가 일치하지 않는 hydration 에러가 발생한다.
이를 방지하기 위해 html 태그에 suppressHydrationWarning 속성을 삽입한다.

5. 결과

결과1
결과2
정상 작동됨을 확인할 수 있다!