다크 모드 구현의 8단계: meta 태그 한 줄부터 JavaScript까지 완전 정복 (id.news.hada.io)
목차(8)
한줄 요약
meta 태그 한 줄로 시작해 JavaScript 제어까지, 다크 모드 구현을 8단계로 나눠 상황에 맞게 선택할 수 있다.
어떤 상황에서 필요한가?
다크 모드 지원은 이제 선택이 아닌 기본 요건에 가깝다. 문제는 "어디까지 구현할 것인가"다. 단순히 prefers-color-scheme에 반응하는 수준으로 충분한 프로젝트가 있는 반면, 사용자가 직접 라이트/다크/자동을 선택하고 그 설정이 세션을 넘어 유지되어야 하는 서비스도 있다.
CSS Naked Day 실험에서 출발한 이 8단계 분류는, 구현 범위를 점진적으로 확장해가면서 각 레벨이 실제로 무엇을 해결하는지 명확히 보여준다. 새 프로젝트를 시작하거나 기존 사이트에 다크 모드를 얹어야 하는 상황 모두에서 참고 기준이 된다.
핵심 구현 방법
Level 1~2: 아무것도 없어도 된다
가장 단순한 진입점은 HTML <head>에 <meta name="color-scheme" content="light dark">를 추가하는 것이다. CSS 한 줄 없이도 브라우저가 운영체제의 색상 설정을 따르기 시작한다. content 속성에서 앞에 오는 값이 기본값으로 작동하므로 순서도 의미가 있다.
CSS 레벨에서는 html { color-scheme: light dark; }로 동일한 효과를 낸다. 단, meta 태그는 CSS 파싱 전에 브라우저가 읽을 수 있어 깜박임(FOUC) 방지 측면에서 유리하다. CSS color-scheme 속성은 루트 엘리먼트 외 다른 요소에도 적용할 수 있다는 점에서 meta 태그와 차별된다.
Level 3: light-dark() 함수
비교적 최근 추가된 CSS 함수 light-dark()를 사용하면 별도의 미디어 쿼리 블록 없이 한 줄에 두 모드 색상을 선언할 수 있다.
background-color: light-dark(white, black);
color: light-dark(black, white);
첫 번째 인자가 라이트 모드, 두 번째가 다크 모드에 적용된다. CSS 커스텀 프로퍼티도 인자로 넣을 수 있어 디자인 토큰 기반 시스템과 궁합이 좋다. 다만 작성 시점 기준으로 브라우저 지원이 완전하지 않은 유일한 레벨이므로, 지원 범위를 사전에 확인해야 한다.
Level 4~5: 미디어 쿼리와 파일 분리
@media (prefers-color-scheme: dark) 블록을 활용하면 색상 변경을 넘어 이미지 채도 조정, 박스 섀도우를 아웃라인으로 교체하는 등 구조적인 변형도 가능하다. 커스터마이징 폭이 가장 넓은 방식이다.
커스터마이징 범위가 크다면 light.css와 dark.css로 파일을 분리하고 <link> 태그의 media 속성에 각각 미디어 쿼리를 지정하는 방식도 유효하다. 브라우저는 현재 조건에 맞지 않는 CSS 파일의 다운로드 우선순위를 낮추므로 성능상 이점도 생긴다. 이때 screen and 조건을 붙여 인쇄 대상을 제외하는 것이 실용적이다.
Level 6~8: JavaScript와 사용자 전환 스위처
JavaScript에서는 window.matchMedia('(prefers-color-scheme: dark)')로 현재 스킴을 감지하고, 변경 이벤트를 구독해 동적으로 반응할 수 있다. 이 레벨부터는 앞선 CSS 기법들과 조합해서 사용하는 것이 일반적이다.
사용자 직접 제어가 필요하다면 단순한 토글이 아니라 자동 / 라이트 / 다크 세 가지 옵션을 제공하는 스위처가 필요하다. "자동"은 시스템 설정을 그대로 따르는 기본값이다.
스위처 구현 시 클래스나 data-* 속성 대신, 실제 <meta name="color-scheme"> 엘리먼트의 content 값을 CSS :has() 셀렉터로 직접 참조하는 방식이 주목할 만하다.
html:has(meta[name="color-scheme"][content="dark"]) {
--color-bg: #111;
--color-text: #eee;
}
별도의 클래스나 데이터 속성 없이 meta 엘리먼트 자체가 테마의 단일 진실 공급원(source of truth)이 된다.
실전에서 주의할 점
Safari 구버전 접근성 문제는 실제로 사이트 운영 중 발견된 사례다. 비교적 최근까지 Safari는 다크 모드에서 링크 색상의 접근성을 보장하지 않았으며, 텍스트 입력 필드의 테두리가 보이지 않는 문제도 확인됐다. 시멘틱 HTML과 user agent 스타일에만 의존해서는 접근성을 완전히 담보할 수 없다는 의미다. 최소한의 커스텀 CSS로 폴백을 준비해두는 것이 안전하다.
인쇄 환경의 동작 방식도 알아둘 만하다. 실제 테스트 결과, 시스템이 다크 모드 상태여도 인쇄 미리보기에서 prefers-color-scheme는 항상 light로 보고된다. Firefox와 Chromium 모두에서 같은 동작이 확인됐다. 다크 모드 스타일이 인쇄 출력에 영향을 줄 것을 걱정할 필요는 없지만, 명시적으로 screen and 조건을 붙여두면 의도가 코드에 드러나 유지보수에 유리하다.
구현 레벨은 프로젝트 요구사항에 따라 다르다. 콘텐츠 중심 정적 사이트라면 Level 2 정도로 충분하고, 디자인 시스템을 운영하는 프로덕트라면 Level 6~8까지 고려할 필요가 있다. 무조건 높은 레벨이 좋은 게 아니라, 복잡도 대비 실질적인 사용자 경험 개선 효과를 따져야 한다.
자주 묻는 질문
Q.meta 태그와 CSS color-scheme 중 어느 것을 써야 하나?
둘 다 동일한 결과를 내지만, HTML을 직접 제어할 수 있다면 meta 태그를 권장한다. 브라우저가 CSS를 파싱하기 전에 meta 태그를 읽을 수 있어, 초기 렌더링 시 모드 전환 깜박임을 줄이는 데 유리하다. 다만 meta 태그는 문서 전체에만 적용되고, CSS `color-scheme` 속성은 특정 컴포넌트에만 개별 적용하는 것도 가능하다. 복잡한 컴포넌트 단위 제어가 필요하다면 CSS 속성을 함께 활용하는 방식이 적합하다.
Q.사용자 선택 스위처를 구현할 때 상태는 어디에 저장해야 하나?
일반적으로 `localStorage`를 사용해 페이지 새로고침이나 세션 종료 후에도 선택값을 유지한다. 중요한 점은 저장된 값을 읽어 테마를 적용하는 스크립트를 최대한 빨리 실행해야 한다는 것이다. `<body>` 렌더링 전에 실행하지 않으면 라이트 모드로 잠깐 보였다가 다크로 전환되는 깜박임이 발생한다. `<head>` 안에 인라인 스크립트로 처리하거나, blocking 스크립트로 먼저 로드하는 것이 일반적인 해결책이다.
Q.light-dark() 함수는 지금 사용해도 되나?
작성 시점 기준으로 브라우저 지원이 완전하지 않다고 언급된 유일한 레벨이다. 프로젝트의 지원 브라우저 범위를 먼저 확인해야 한다. 최신 Chromium 계열과 Firefox에서는 동작하지만, 구버전 Safari나 레거시 환경을 지원해야 한다면 `@media (prefers-color-scheme: dark)` 방식을 폴백으로 병행하거나 대신 사용하는 것이 안전하다. 📌 원문: [GeekNews](https://id.news.hada.io/topic?id=28705) 🔗 구축이나 개발이 필요하다면 → [삼태연구소에 문의하기](/contact)