next-themes를 활용한 다크모드

짱효·2026년 2월 2일

4. next-themes를 활용한 다크모드 (쉽게 이해하기)

🎯 핵심 개념: 다크모드를 쉽게 관리하는 도구

next-themes는 다크모드를 쉽게 구현할 수 있게 해주는 라이브러리입니다.
복잡한 설정 없이 몇 줄의 코드로 다크모드를 만들 수 있어요!

1️⃣ 기본 설정 (3단계)

Step 1: ThemeProvider로 앱 감싸기

// src/main.tsx
import { ThemeProvider } from "next-themes";

<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
  <App />
</ThemeProvider>

설명:

  • attribute="class": HTML에 class="dark"를 추가해서 다크모드 적용
  • defaultTheme="system": 처음엔 컴퓨터 설정을 따라감
  • enableSystem: 컴퓨터 다크모드 설정을 자동으로 감지

Step 2: 테마 변경 버튼 만들기

// src/components/layout/global-layout.tsx
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

export default function GlobalLayout() {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  // ⚠️ 중요: 이 부분이 왜 필요한지 아래에서 설명!
  useEffect(() => {
    setMounted(true);
  }, []);

  const toggleTheme = () => {
    setTheme(theme === "dark" ? "light" : "dark");
  };

  // mounted가 false면 기본 아이콘만 보여줌
  if (!mounted) {
    return <MoonIcon />;
  }

  // mounted가 true면 실제 테마에 맞는 아이콘 보여줌
  return (
    <Button onClick={toggleTheme}>
      {theme === "dark" ? <SunIcon /> : <MoonIcon />}
    </Button>
  );
}

2️⃣ 왜 mounted가 필요한가? (하이드레이션 이슈)

문제 상황을 비유로 이해하기

상황: 친구에게 "오늘 날씨 어때?"라고 물어봤는데, 친구는 아직 밖에 나가지 않아서 모른다고 해요.

// ❌ 문제가 있는 코드
const { theme } = useTheme();

// 처음 렌더링: theme이 아직 정해지지 않음 (undefined)
// 나중에 렌더링: theme이 "dark"로 정해짐
// → 처음과 나중이 달라서 React가 경고를 냄!
{theme === "dark" ? <SunIcon /> : <MoonIcon />}

해결책: 친구가 밖에 나가서 날씨를 확인할 때까지 기다리기

// ✅ 올바른 코드
const [mounted, setMounted] = useState(false);

useEffect(() => {
  setMounted(true); // "이제 테마를 확인했어요!"
}, []);

// 아직 확인 안 했으면 기본값 보여주기
if (!mounted) {
  return <MoonIcon />; // 기본 아이콘
}

// 확인했으면 실제 테마 보여주기
{theme === "dark" ? <SunIcon /> : <MoonIcon />}

왜 이렇게 하나요?

  • 처음 렌더링: 브라우저가 localStorage를 읽기 전 → 테마 모름
  • useEffect 실행 후: localStorage 읽음 → 테마 알게 됨
  • mounted로 "준비됐는지" 체크해서 에러 방지!

3️⃣ 시스템 테마 자동 감지 (간단 설명)

컴퓨터가 다크모드면 웹사이트도 자동으로 다크모드가 되는 기능입니다.

// 브라우저가 알아서 체크해줌
// 컴퓨터가 다크모드면 → 웹사이트도 다크모드
// 컴퓨터가 라이트모드면 → 웹사이트도 라이트모드

사용자가 "시스템 설정 따르기"를 선택하면:

  • 컴퓨터 설정 바뀌면 → 웹사이트도 자동으로 바뀜
  • 별도 코드 작성 불필요!

4️⃣ 실제 사용 코드 (복사해서 쓰기)

// src/components/layout/global-layout.tsx
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { SunIcon, MoonIcon } from "lucide-react";
import { Button } from "@/components/ui/button";

export default function GlobalLayout() {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  const toggleTheme = () => {
    setTheme(theme === "dark" ? "light" : "dark");
  };

  if (!mounted) {
    return (
      <Button variant="ghost" size="icon" disabled>
        <MoonIcon className="h-5 w-5" />
      </Button>
    );
  }

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={toggleTheme}
      aria-label="테마 변경"
    >
      {theme === "dark" ? (
        <SunIcon className="h-5 w-5" />
      ) : (
        <MoonIcon className="h-5 w-5" />
      )}
    </Button>
  );
}

5️⃣ theme vs resolvedTheme 차이 (쉽게 이해)

const { theme, resolvedTheme } = useTheme();

theme (사용자가 선택한 값):

  • "light" → 사용자가 라이트모드 선택
  • "dark" → 사용자가 다크모드 선택
  • "system" → 사용자가 "컴퓨터 설정 따르기" 선택

resolvedTheme (실제로 적용된 값):

  • "light" → 지금 라이트모드로 보임
  • "dark" → 지금 다크모드로 보임

예시:

// 사용자가 "시스템 설정 따르기" 선택 + 컴퓨터가 다크모드
theme === "system"        // 사용자가 선택한 것
resolvedTheme === "dark"  // 실제로 보이는 것

// 사용자가 "다크모드" 직접 선택
theme === "dark"          // 사용자가 선택한 것
resolvedTheme === "dark"  // 실제로 보이는 것 (같음)

언제 뭘 써야 하나요?

  • 아이콘 표시할 때: resolvedTheme 사용 (실제 보이는 모드에 맞춰야 하니까)
  • 설정 저장할 때: theme 사용 (사용자가 선택한 값 저장)

6️⃣ Tailwind CSS와 연동 (자동으로 됨)

// ThemeProvider가 알아서 <html class="dark"> 추가해줌
<ThemeProvider attribute="class">
  {/* 다크모드일 때 자동으로 */}
  <html class="dark">
    {/* Tailwind의 dark: 클래스가 작동함 */}
    <div className="bg-white dark:bg-gray-900">
      ...
    </div>
  </html>
</ThemeProvider>

설명:

  • attribute="class" 설정하면 자동으로 class="dark" 추가
  • Tailwind의 dark: 접두사가 자동으로 작동
  • 별도 설정 불필요!

7️⃣ localStorage 자동 저장 (신경 쓸 필요 없음)

// next-themes가 알아서 해줌
// 사용자가 다크모드 선택 → localStorage에 저장
// 페이지 새로고침 → 자동으로 복원
// 코드 작성 불필요!

8️⃣ 실전 팁

✅ 이렇게 하세요

// 1. 항상 mounted 체크
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);

// 2. 아이콘 표시할 때는 resolvedTheme 사용
{resolvedTheme === "dark" ? <SunIcon /> : <MoonIcon />}

// 3. 접근성 고려 (스크린 리더용)
<Button aria-label="테마 변경" onClick={toggleTheme}>

❌ 이렇게 하지 마세요

// 1. mounted 체크 없이 theme 바로 사용
{theme === "dark" && <DarkComponent />} // ❌ 에러 발생 가능

// 2. 서버에서 theme 사용
// 서버에서는 localStorage 없어서 theme이 undefined

9️⃣ 전체 흐름 정리 (한눈에 보기)

1. 앱 시작
   ↓
2. ThemeProvider가 localStorage 확인
   ↓
3. 저장된 테마 있으면 → 그걸로 적용
   없으면 → 시스템 테마 확인
   ↓
4. <html class="dark"> 또는 <html> 추가
   ↓
5. Tailwind의 dark: 클래스 작동
   ↓
6. 사용자가 버튼 클릭 → setTheme() 호출
   ↓
7. localStorage에 저장 + 즉시 적용

🔟 완성 코드 (복사해서 바로 사용)

// src/main.tsx
import { ThemeProvider } from "next-themes";

<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
  <App />
</ThemeProvider>

// src/components/theme-toggle.tsx
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { SunIcon, MoonIcon } from "lucide-react";
import { Button } from "@/components/ui/button";

export function ThemeToggle() {
  const { theme, setTheme, resolvedTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return (
      <Button variant="ghost" size="icon" disabled>
        <MoonIcon className="h-5 w-5" />
      </Button>
    );
  }

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
      aria-label="테마 변경"
    >
      {resolvedTheme === "dark" ? (
        <SunIcon className="h-5 w-5" />
      ) : (
        <MoonIcon className="h-5 w-5" />
      )}
    </Button>
  );
}

📝 핵심만 정리

  1. ThemeProvider로 감싸기 → 다크모드 기본 설정 완료
  2. mounted 체크 → 에러 방지 (처음 렌더링 때 테마 모를 수 있음)
  3. resolvedTheme 사용 → 실제 보이는 모드에 맞춰 아이콘 표시
  4. localStorage 자동 저장 → 신경 쓸 필요 없음
  5. Tailwind 자동 연동dark: 클래스 그냥 쓰면 됨

결론: 복잡해 보이지만 실제로는 ThemeProvider 감싸고, mounted 체크하고, 버튼 만들면 끝! 🎉

profile
✨🌏확장해 나가는 프론트엔드 개발자입니다✏️

0개의 댓글