07 — Dark Mode & Theming
한 줄 답: 모던 다크모드는 3가지 결정으로 압축된다 — (1) 어떻게 감지할 것인가(
prefers-color-schemevs 사용자 토글), (2) 어떻게 전환할 것인가(color-scheme+ 시맨틱 토큰), (3) 어디서 분기할 것인가(light-dark()vs:root[data-theme]). 2024년의light-dark()함수는 수백 줄의 분기를 한 줄로 줄였다.
Why — 다크모드는 왜 어려운가
단순해 보이지만 5가지 함정
.card { background: white; color: black; }
.card.dark { background: black; color: white; }— 작동하지만 곧 다음 문제가 보인다:
- 수백 개 컴포넌트에
.dark변형 → 유지보수 폭발. <select>·<input>같은 폼 컨트롤은 OS가 그린다 → 다크모드에서 흰 배경으로 튐.- 이미지·그림자는 다크 배경에서 다르게 보여야 함.
- 사용자 선호(OS 설정)와 수동 토글이 충돌 — 어느 게 이기나?
- SSR/SSG에서 첫 페인트에 깜빡임 (FOUC, Flash of Unstyled Content).
제대로 된 다크모드는 디자인 시스템 전체를 다시 본다.
How — 3가지 전략의 지도
What — 전략 1: System Only (가장 단순)
OS 다크모드 설정을 그대로 따른다. 사용자 토글 없음.
:root {
--color-bg: white;
--color-text: black;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0a0a0a;
--color-text: #e5e5e5;
}
}
body {
background: var(--color-bg);
color: var(--color-text);
}장점:
- 코드가 짧다.
- JS 0줄.
- SSR FOUC 없음 (CSS 자체가 처리).
단점:
- 사용자가 밝게/어둡게 토글할 수 없음.
- 사이트 안에서 부분 라이트 같은 미세 조정 불가.
적합: 블로그, 문서 사이트, 작은 앱.
What — 전략 2: Manual Override (사실상 표준)
OS 설정을 기본값으로 두고, 사용자 토글로 덮어쓴다.
1) HTML 속성으로 분기
<html data-theme="auto">
<!-- "auto" | "light" | "dark" -->
</html>:root {
--color-bg: white;
--color-text: black;
}
/* auto: OS 설정 따라감 */
:root[data-theme="auto"] {
@media (prefers-color-scheme: dark) {
--color-bg: #0a0a0a;
--color-text: #e5e5e5;
}
}
/* dark: 강제 다크 */
:root[data-theme="dark"] {
--color-bg: #0a0a0a;
--color-text: #e5e5e5;
}
/* light는 :root 기본값으로 충분 */2) JS로 토글
function setTheme(theme) {
document.documentElement.dataset.theme = theme;
localStorage.setItem('theme', theme);
}
// 페이지 로드 시 (head에 인라인 권장 — FOUC 방지)
const saved = localStorage.getItem('theme') || 'auto';
document.documentElement.dataset.theme = saved;FOUC 방지가 핵심: 위 JS를 <head>에 최우선 인라인 스크립트로. body 렌더 전에 data-theme 설정.
What — color-scheme (OS 폼 컨트롤까지 다크화)
CSS 명세의 작지만 중요한 속성. OS가 그리는 UI까지 다크 모드로 전환.
:root { color-scheme: light dark; }
:root[data-theme="dark"] { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }효과:
<input>·<select>·<textarea>기본 색상 자동 적응.- 스크롤바 색상 자동.
- 폼 자동완성 배경색 자동.
<details>토글 아이콘 색 자동.
color-scheme 없으면 → 다크 페이지에 흰 스크롤바·흰 폼 컨트롤이 튀어나옴.
<!-- 작은 한 줄로 큰 차이 -->
<meta name="color-scheme" content="light dark">What — light-dark() 함수 (2024년 Baseline)
CSS Color Module Level 5의 새 함수. 두 값을 받아 현재 color-scheme에 맞는 쪽을 반환.
이전 (분기 6줄)
:root {
--color-text: black;
--color-bg: white;
}
@media (prefers-color-scheme: dark) {
:root {
--color-text: #e5e5e5;
--color-bg: #0a0a0a;
}
}이후 (한 줄씩)
:root {
color-scheme: light dark;
--color-text: light-dark(black, #e5e5e5);
--color-bg: light-dark(white, #0a0a0a);
}light-dark()는:
- 현재
color-scheme값을 보고 한쪽 선택. :root[data-theme="dark"] { color-scheme: dark }와 결합하면 수동 토글도 자동 작동.- 모든 색상 속성에서 사용 가능.
브라우저: Chrome 123+, Safari 17.5+, Firefox 120+ (Baseline 2024).
What — 토큰 시스템과의 통합
다크모드의 핵심은 semantic 토큰 층에서 일어난다 (01-design-tokens 참고).
:root {
color-scheme: light dark;
/* primitive (변하지 않음) */
--gray-50: #f9fafb;
--gray-900: #111827;
--blue-500: #2563eb;
--blue-300: #93c5fd;
/* semantic (다크모드에서 재매핑) */
--color-bg: light-dark(white, var(--gray-900));
--color-surface: light-dark(var(--gray-50), #1f2937);
--color-text: light-dark(var(--gray-900), var(--gray-50));
--color-text-muted: light-dark(#6b7280, #9ca3af);
--color-action: light-dark(var(--blue-500), var(--blue-300));
--color-border: light-dark(#e5e7eb, #374151);
}
.card {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
}→ 컴포넌트는 다크모드를 모른다. semantic 토큰만 보고 자동 적응.
What — 전략 3: Per-Component (드물게 사용)
특정 컴포넌트만 영구 다크로 (예: 코드 블록은 항상 다크).
.code-block {
--code-bg: #0d1117;
--code-text: #c9d1d9;
/* 사이트 테마와 무관하게 항상 다크 */
color-scheme: dark; /* 내부 스크롤바도 다크 */
background: var(--code-bg);
color: var(--code-text);
}color-scheme을 요소 단위로 줄 수 있다는 게 강력하다. 예: 헤더만 다크, 푸터만 라이트.
언제 쓰나: 코드 블록, 다이얼로그, 인앱 임베디드 비디오 플레이어 등 맥락이 강한 부분.
What — Tailwind v4의 다크모드
Tailwind v4는 3가지 모드:
// tailwind.config (v3) — 미디어 쿼리 모드
darkMode: 'media',
// 또는 클래스 모드
darkMode: 'class',
// 또는 selector 모드 (v3.4+)
darkMode: ['selector', '[data-theme="dark"]'],/* v4 CSS-first */
@import "tailwindcss";
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));<div class="bg-white dark:bg-gray-900 text-black dark:text-white">→ 모든 유틸리티에 dark: variant 적용.
팁: Tailwind의 dark: 유틸을 너무 많이 쓰지 마라. semantic 토큰으로 추상화하면:
@theme {
--color-bg: light-dark(white, var(--color-gray-900));
}<div class="bg-bg"> <!-- 한 번에 둘 다 처리 -->What-if — 다크모드의 5가지 함정
1) FOUC (Flash of Unstyled Content)
SSR/SSG 시 서버는 다크인지 모름. 첫 페인트는 라이트, 그 후 JS가 다크로 전환 → 깜빡임.
해결: <head> 최상단 인라인 스크립트:
<script>
document.documentElement.dataset.theme =
localStorage.theme ||
(matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
</script>2) 이미지·로고가 다크에서 안 보임
/* 다크 배경에 어두운 로고 */
img.logo { filter: brightness(0) invert(1); }
/* 또는 자동 반전 */
@media (prefers-color-scheme: dark) {
img.logo { filter: invert(1) hue-rotate(180deg); }
}
/* 또는 다크 전용 이미지 */
<picture>
<source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)">
<img src="logo-light.svg" alt="">
</picture>3) 그림자가 다크에서 사라짐
라이트 모드의 box-shadow: 0 4px 8px rgba(0,0,0,0.1)는 다크에서 안 보임 (검은 위에 검정).
:root {
--shadow-card: light-dark(
0 4px 8px rgba(0,0,0,0.1),
0 4px 8px rgba(0,0,0,0.4) /* 더 진하게 또는 outline으로 대체 */
);
}4) color-scheme 누락 → 폼 컨트롤 흰색
다크 페이지에 흰 input·select가 튀어나온다. :root { color-scheme: light dark } 반드시.
5) 너무 극단적 대비
#000과 #fff는 눈이 아프다. 모던 다크는 보통:
- 배경:
#0a0a0a~#1a1a1a - 텍스트:
#e5e5e5~#f0f0f0
WCAG 대비 4.5:1만 만족하면, 순수 검정/순수 흰색은 피하라. iOS·Material 가이드 모두 이 패턴.
What-if — Light-Dark 함수 미지원 브라우저
2024 Baseline이지만 구형 브라우저 대응:
:root {
/* fallback */
--color-text: black;
}
@supports (color: light-dark(black, white)) {
:root {
--color-text: light-dark(black, #e5e5e5);
}
}
/* 또는 점진적 향상 */
@media (prefers-color-scheme: dark) {
:root { --color-text: #e5e5e5; }
}
:root {
--color-text: light-dark(black, #e5e5e5);
}후자는 모던 브라우저는 light-dark(), 구형은 미디어 쿼리.
Insight — 다크모드의 3단 진화
다크모드는 기능에서 디자인 시스템의 일부로, 다시 언어 기능으로 진화했다.
2014-2018: 수동 시대
- .dark 클래스를 컴포넌트마다 분기.
- JS로 토글, localStorage 저장.
- FOUC 끔찍.
- 500개 컴포넌트면 500번 분기.
2018-2024: 토큰 시대
- CSS Variables가 표준화.
- semantic 토큰 층에서 한 번만 재매핑.
prefers-color-scheme미디어 쿼리.- 코드 양 10배 감소.
2024~: 함수 시대
light-dark()함수.color-scheme속성.- 컴포넌트는 다크모드를 모른다.
- 다크모드 추가에 수십 줄.
흥미로운 점: 다크모드는 디자인 시스템의 시험대다. 토큰이 잘 설계됐다면 다크모드는 몇 시간에 끝난다. 안 됐다면 몇 주가 든다.
Robin Rendle은 “Dark mode is a stress test for your design system” 이라고 했다. 토큰 없는 디자인 시스템은 다크모드에서 드러난다.
2024년 light-dark()가 표준이 된 것은 CSS가 디자인 시스템의 일부 결정을 언어로 흡수한 사례다. 함수 하나에 의미가 응축됐다 — “두 가지 모드 사이의 선택”.
오늘날의 다크모드는 더 이상 *“다크모드를 추가하자”*는 프로젝트가 아니다. 디자인 시스템이 잘 설계됐다면 이미 거기 있다 .
요약 + Mermaid
- 3 전략: System Only / Manual Override(표준) / Per-Component.
color-scheme: light dark— OS 폼 컨트롤·스크롤바까지 다크.light-dark(a, b)— 한 줄로 두 모드 표현 (2024 Baseline).- semantic 토큰 층이 다크모드의 유일한 분기 지점.
- FOUC 방지: head 최상단 인라인 스크립트.
- WCAG 대비 유지하면서 순수 검정·흰색은 피한다.
다음: 08-perf-and-shipping — Critical CSS·content-visibility·container query 비용.