이번에 블로그를 만들면서, 블로그에 다크 모드를 구현하면 좋겠다 싶었다.
마침 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. 결과
정상 작동됨을 확인할 수 있다!