본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 8장: 다크 모드와 테마 전략
2026년 3월 1일·웹 개발·

8장: 다크 모드와 테마 전략

Tailwind CSS v4의 다크 모드 전략, CSS 커스텀 프로퍼티 기반 런타임 테마 전환, 다중 테마 패턴, 그리고 next-themes와의 통합을 실전 중심으로 다룹니다.

10분641자6개 섹션
frontend
공유
tailwind-v48 / 11
1234567891011
이전7장: 고급 애니메이션과 트랜지션다음9장: 커스텀 변형과 플러그인 시스템

7장에서 애니메이션과 트랜지션 기법을 배웠습니다. 이 장에서는 Tailwind CSS v4의 다크 모드 전략과 테마 시스템을 다룹니다. v4에서 변경된 다크 모드 설정, CSS 커스텀 프로퍼티를 활용한 런타임 테마 전환, 그리고 실전 프로젝트에서의 테마 관리 패턴을 살펴봅니다.

v4의 다크 모드 기본 동작

미디어 쿼리 기반 (기본)

v4에서 dark: 변형은 기본적으로 prefers-color-scheme: dark 미디어 쿼리를 사용합니다. 별도의 설정 없이 바로 사용할 수 있습니다.

기본 다크 모드
html
<div class="bg-white dark:bg-neutral-900">
  <h1 class="text-neutral-900 dark:text-neutral-100">제목</h1>
  <p class="text-neutral-600 dark:text-neutral-400">본문 텍스트</p>
</div>

클래스 기반 전환

사용자가 직접 테마를 선택할 수 있게 하려면, dark: 변형을 클래스 기반으로 전환해야 합니다. v4에서는 @custom-variant를 사용합니다.

클래스 기반 다크 모드
css
@import "tailwindcss";
 
@custom-variant dark (&:where(.dark, .dark *));

이제 <html class="dark">로 다크 모드를 제어할 수 있습니다.

클래스 기반 사용
html
<!-- html에 dark 클래스 추가/제거로 제어 -->
<html class="dark">
  <body class="bg-white dark:bg-neutral-900">
    <h1 class="text-neutral-900 dark:text-neutral-100">제목</h1>
  </body>
</html>
Info

v3에서는 tailwind.config.js의 darkMode: 'class'로 설정했지만, v4에서는 @custom-variant로 CSS 안에서 직접 정의합니다. &:where(.dark, .dark *) 구문은 .dark 클래스가 있는 요소와 그 자손 모두에 적용됩니다.

CSS 커스텀 프로퍼티 기반 테마

의미론적 색상 토큰

dark: 변형을 모든 요소에 일일이 적용하는 대신, CSS 커스텀 프로퍼티를 사용하면 한 곳에서 테마를 관리할 수 있습니다.

의미론적 테마 토큰
css
@import "tailwindcss";
 
@custom-variant dark (&:where(.dark, .dark *));
 
/* 라이트 테마 (기본) */
@theme {
  --color-background: oklch(0.99 0 0);
  --color-surface: oklch(1 0 0);
  --color-surface-alt: oklch(0.97 0 0);
  --color-foreground: oklch(0.15 0 0);
  --color-foreground-muted: oklch(0.45 0 0);
  --color-border: oklch(0.90 0 0);
  --color-primary: oklch(0.60 0.22 250);
  --color-primary-hover: oklch(0.53 0.24 252);
  --color-primary-foreground: oklch(1 0 0);
}
 
/* 다크 테마 오버라이드 */
.dark {
  --color-background: oklch(0.12 0.01 260);
  --color-surface: oklch(0.17 0.01 260);
  --color-surface-alt: oklch(0.22 0.01 260);
  --color-foreground: oklch(0.93 0 0);
  --color-foreground-muted: oklch(0.60 0 0);
  --color-border: oklch(0.28 0.01 260);
  --color-primary: oklch(0.65 0.20 250);
  --color-primary-hover: oklch(0.72 0.18 250);
  --color-primary-foreground: oklch(0.15 0 0);
}

이제 컴포넌트에서 dark: 변형 없이 의미론적 토큰만 사용합니다.

dark: 변형 없는 테마 대응
html
<!-- dark: 변형이 필요 없음 — 토큰이 자동으로 전환됨 -->
<div class="bg-background text-foreground">
  <div class="bg-surface border-border rounded-lg p-6">
    <h2 class="text-foreground">제목</h2>
    <p class="text-foreground-muted">설명 텍스트</p>
    <button class="bg-primary text-primary-foreground hover:bg-primary-hover">
      버튼
    </button>
  </div>
</div>
Tip

의미론적 토큰 패턴의 가장 큰 장점은 컴포넌트가 테마에 대해 알 필요가 없다는 것입니다. bg-background, text-foreground만 사용하면 라이트/다크 모드 전환이 자동으로 처리됩니다. 새로운 테마를 추가해도 컴포넌트 코드를 수정할 필요가 없습니다.

다중 테마 시스템

CSS 커스텀 프로퍼티를 활용하면 라이트/다크를 넘어 다양한 테마를 지원할 수 있습니다.

다중 테마
css
@import "tailwindcss";
 
@theme {
  --color-background: oklch(0.99 0 0);
  --color-surface: oklch(1 0 0);
  --color-primary: oklch(0.60 0.22 250);
  --color-foreground: oklch(0.15 0 0);
}
 
/* 다크 테마 */
[data-theme="dark"] {
  --color-background: oklch(0.12 0.01 260);
  --color-surface: oklch(0.17 0.01 260);
  --color-primary: oklch(0.65 0.20 250);
  --color-foreground: oklch(0.93 0 0);
}
 
/* 세피아 테마 */
[data-theme="sepia"] {
  --color-background: oklch(0.95 0.02 80);
  --color-surface: oklch(0.97 0.015 80);
  --color-primary: oklch(0.55 0.15 45);
  --color-foreground: oklch(0.25 0.03 60);
}
 
/* 고대비 테마 */
[data-theme="high-contrast"] {
  --color-background: oklch(1 0 0);
  --color-surface: oklch(1 0 0);
  --color-primary: oklch(0.30 0.25 250);
  --color-foreground: oklch(0 0 0);
}
테마 적용
html
<html data-theme="dark">
  <!-- 전체 UI에 다크 테마 적용 -->
</html>
테마 전환 로직
typescript
function setTheme(theme: string) {
  document.documentElement.setAttribute('data-theme', theme);
  localStorage.setItem('theme', theme);
}
 
// 시스템 테마 감지
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
const savedTheme = localStorage.getItem('theme');
 
if (savedTheme) {
  setTheme(savedTheme);
} else if (prefersDark.matches) {
  setTheme('dark');
}

next-themes와의 통합

Next.js 프로젝트에서는 next-themes 라이브러리와 자연스럽게 통합됩니다.

ThemeProvider 설정
typescript
// app/providers.tsx
'use client';
 
import { ThemeProvider } from 'next-themes';
 
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider
      attribute="class"
      defaultTheme="system"
      enableSystem
      disableTransitionOnChange
    >
      {children}
    </ThemeProvider>
  );
}
next-themes와 호환되는 CSS
css
@import "tailwindcss";
 
@custom-variant dark (&:where(.dark, .dark *));
 
@theme {
  --color-background: oklch(0.99 0 0);
  --color-foreground: oklch(0.15 0 0);
}
 
.dark {
  --color-background: oklch(0.12 0.01 260);
  --color-foreground: oklch(0.93 0 0);
}
테마 전환 버튼
typescript
'use client';
 
import { useTheme } from 'next-themes';
 
function ThemeToggle() {
  const { theme, setTheme } = useTheme();
 
  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      className="p-2 rounded-lg bg-surface-alt hover:bg-border transition-colors"
      aria-label="테마 전환"
    >
      {/* 아이콘 */}
    </button>
  );
}
Warning

next-themes의 attribute="class"를 사용할 때, v4의 @custom-variant dark 설정과 일치해야 합니다. attribute="data-theme"을 사용한다면 @custom-variant도 그에 맞게 수정하세요.

테마 전환 시 플래시 방지

서버 사이드 렌더링 환경에서 테마 전환 시 FOUC(Flash of Unstyled Content)가 발생할 수 있습니다.

FOUC 방지 스크립트
html
<head>
  <script>
    (function() {
      const theme = localStorage.getItem('theme');
      if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
        document.documentElement.classList.add('dark');
      }
    })();
  </script>
</head>

전환 애니메이션 제어

테마 전환 시 모든 요소에 트랜지션이 적용되면 어색할 수 있습니다. disableTransitionOnChange(next-themes)를 사용하거나, 직접 구현합니다.

테마 전환 시 트랜지션 비활성화
css
html.disable-transitions,
html.disable-transitions * {
  transition: none !important;
}
트랜지션 비활성화 로직
typescript
function setThemeWithoutTransition(theme: string) {
  document.documentElement.classList.add('disable-transitions');
  setTheme(theme);
  // 다음 프레임에서 트랜지션 복원
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      document.documentElement.classList.remove('disable-transitions');
    });
  });
}

실전 패턴: 완전한 테마 시스템

프로덕션 테마 시스템
css
@import "tailwindcss";
 
@custom-variant dark (&:where(.dark, .dark *));
 
@theme {
  /* === 중성 색상 === */
  --color-neutral-50: oklch(0.985 0.002 250);
  --color-neutral-100: oklch(0.97 0.004 250);
  --color-neutral-200: oklch(0.92 0.006 250);
  --color-neutral-300: oklch(0.87 0.008 250);
  --color-neutral-400: oklch(0.71 0.01 250);
  --color-neutral-500: oklch(0.55 0.012 250);
  --color-neutral-600: oklch(0.45 0.012 250);
  --color-neutral-700: oklch(0.37 0.01 250);
  --color-neutral-800: oklch(0.27 0.008 250);
  --color-neutral-900: oklch(0.21 0.006 250);
  --color-neutral-950: oklch(0.13 0.004 250);
 
  /* === 의미론적 토큰 (라이트 기본) === */
  --color-background: var(--color-neutral-50);
  --color-surface: white;
  --color-surface-alt: var(--color-neutral-100);
  --color-foreground: var(--color-neutral-900);
  --color-foreground-muted: var(--color-neutral-500);
  --color-border: var(--color-neutral-200);
 
  /* === 브랜드 === */
  --color-primary: oklch(0.60 0.22 250);
  --color-primary-hover: oklch(0.53 0.24 252);
  --color-primary-foreground: white;
 
  /* === 기능 색상 === */
  --color-success: oklch(0.65 0.19 150);
  --color-warning: oklch(0.80 0.15 80);
  --color-error: oklch(0.58 0.24 25);
}
 
/* === 다크 테마 === */
.dark {
  --color-background: var(--color-neutral-950);
  --color-surface: var(--color-neutral-900);
  --color-surface-alt: var(--color-neutral-800);
  --color-foreground: var(--color-neutral-100);
  --color-foreground-muted: var(--color-neutral-400);
  --color-border: var(--color-neutral-700);
 
  --color-primary: oklch(0.65 0.20 250);
  --color-primary-hover: oklch(0.72 0.18 250);
  --color-primary-foreground: var(--color-neutral-950);
 
  --color-success: oklch(0.70 0.17 150);
  --color-warning: oklch(0.85 0.13 80);
  --color-error: oklch(0.65 0.22 25);
}

정리

Tailwind CSS v4의 다크 모드는 @custom-variant로 유연하게 설정할 수 있으며, CSS 커스텀 프로퍼티와 결합하면 의미론적 토큰 기반의 강력한 테마 시스템을 구축할 수 있습니다. 라이트/다크를 넘어 다중 테마, 컴포넌트 수준 테마, 런타임 테마 전환까지 — CSS만으로 모든 것이 가능합니다.

다음 장에서는 Tailwind v4의 커스텀 변형 정의와 플러그인 시스템을 다룹니다. @custom-variant, @plugin 디렉티브, 그리고 기존 JS 플러그인에서 CSS 기반 플러그인으로의 전환을 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#frontend

관련 글

웹 개발

9장: 커스텀 변형과 플러그인 시스템

Tailwind CSS v4의 @custom-variant, @plugin 디렉티브, CSS 기반 확장 시스템, 그리고 기존 JS 플러그인에서의 마이그레이션을 실전 중심으로 다룹니다.

2026년 3월 3일·9분
웹 개발

7장: 고급 애니메이션과 트랜지션

Tailwind CSS v4의 애니메이션 시스템, @keyframes 정의, @starting-style 활용, transition-behavior, 그리고 성능 최적화된 애니메이션 패턴을 다룹니다.

2026년 2월 27일·11분
웹 개발

10장: 디자인 시스템 구축 실전

Tailwind CSS v4로 프로덕션 디자인 시스템을 구축합니다. 토큰 계층 설계, 컴포넌트 라이브러리, 테마 시스템, 접근성, 팀 협업 전략을 실전 중심으로 다룹니다.

2026년 3월 5일·10분
이전 글7장: 고급 애니메이션과 트랜지션
다음 글9장: 커스텀 변형과 플러그인 시스템

댓글

목차

약 10분 남음
  • v4의 다크 모드 기본 동작
    • 미디어 쿼리 기반 (기본)
    • 클래스 기반 전환
  • CSS 커스텀 프로퍼티 기반 테마
    • 의미론적 색상 토큰
    • 다중 테마 시스템
  • next-themes와의 통합
  • 테마 전환 시 플래시 방지
    • 전환 애니메이션 제어
  • 실전 패턴: 완전한 테마 시스템
  • 정리