들어가며: 다크모드 토글, 왜 이렇게 복잡해졌나

사이트 헤더에 다크모드 토글 하나 붙이려는데 선택지가 세 개입니다. 라이트, 다크, 시스템. 처음엔 친절해 보이지만, 실상은 사용자의 본래 목적(콘텐츠 소비)을 방해하는 UI가 되어버리곤 하죠.

여기서 던지는 질문은 단순합니다. "시스템 옵션을 굳이 보여줘야 할까?" 좋은 2단계 컨트롤 하나면 세 가지 상태를 모두 표현할 수 있습니다. 사용자는 오버라이드가 필요해지는 순간에만 한 번 클릭하면 되니까요.

핵심 원칙은 이렇습니다. 선호도가 없으면 시스템 설정을 따르고, 오버라이드가 필요하면 그때 저장하라. localStorage에 저장된 값이 없으면 시스템을 따르고, 있으면 그 값을 우선하는 식이죠.

Web developer comparing two-state vs three-state dark mode toggle UI on a laptop screen Technical Structure Concept

핵심 구현: 2단계 토글이 3가지 상태를 표현하는 법

아래 코드는 시스템 설정을 기본값으로 따르면서, 사용자가 명시적으로 오버라이드한 경우에만 localStorage에 저장하는 패턴입니다.

// 시스템 다크모드 여부 확인
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

// 저장된 오버라이드 값 (없으면 null → 시스템 따름)
const storedOverride = localStorage.getItem('theme-override');

// 실제 적용할 테마 결정
const effectiveTheme = storedOverride ?? (systemPrefersDark ? 'dark' : 'light');

document.documentElement.dataset.theme = effectiveTheme;

// 토글 버튼: 현재 테마의 '반대'만 노출
function renderToggle() {
  const button = document.getElementById('theme-toggle');
  // 현재 다크면 '라이트로' 버튼, 라이트면 '다크로' 버튼
  button.textContent = effectiveTheme === 'dark' ? '☀️ 라이트' : '🌙 다크';
}
renderToggle();

// 클릭 시: 시스템과 다를 때만 오버라이드 저장
function onToggle() {
  const next = effectiveTheme === 'dark' ? 'light' : 'dark';
  const systemTheme = systemPrefersDark ? 'dark' : 'light';

  if (next === systemTheme) {
    // 시스템 값과 같아지면 오버라이드 삭제 → 시스템 복귀
    localStorage.removeItem('theme-override');
  } else {
    localStorage.setItem('theme-override', next);
  }
  location.reload(); // 또는 상태 갱신 로직
}

포인트는 토글 버튼이 항상 '반대 상태'만 노출한다는 점입니다. 다크 상태면 '라이트로' 버튼만, 라이트 상태면 '다크로' 버튼만 보입니다. 시스템과 같은 값으로 돌아가면 오버라이드를 지워 자연스럽게 시스템 설정으로 복귀하죠.

시스템 설정 변경 감지

사용자가 OS에서 다크모드를 켜고 끄면 실시간으로 반영되어야 합니다.

window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', (e) => {
    // 오버라이드가 없을 때만 시스템 변경 반영
    if (!localStorage.getItem('theme-override')) {
      document.documentElement.dataset.theme = e.matches ? 'dark' : 'light';
      renderToggle();
    }
  });

이 정도면 3단계 토글 없이도 사용자 경험이 완결됩니다.

UI designer sketching minimal theme toggle switch with system preference fallback Programming Illustration

주의사항과 3단계가 정당한 예외 상황

1. 이 패턴은 '상시 노출 토글'에 한정된다

헤더나 내비게이션에 항상 떠 있는 토글이라면 2단계가 정답입니다. 하지만 별도 설정 패널에 들어가는 옵션이라면 3단계도 괜찮습니다. 사용자가 이미 '설정을 바꾸러 왔다'는 명확한 의도를 가지고 있으니까요.

2. 색상 스킴이 3개 이상일 때

라이트/다크 외에 '고대비 모드', '세피아' 같은 추가 스킴이 있다면 시스템 설정만으로는 부족합니다. 이 경우엔 명시적 선택 UI가 필요하죠.

3. 토글 상태의 '모호함' 문제

라이트 시스템 설정에서 다크로 토글했다가 다시 라이트로 돌아오면, 이게 '오버라이드로 라이트'인지 '시스템 복귀'인지 모호해집니다. 위 코드처럼 시스템 값과 같아지면 오버라이드를 삭제하는 방식으로 처리하면 이 모호함이 사라집니다. 사용자가 알아채기 어려운 엣지 케이스지만, 코드 한 줄로 정리됩니다.

4. 국내 SI/엔터프라이즈 환경에서의 주의

국내 관공서나 대기업 SI 프로젝트에서는 '시스템 설정'이라는 개념 자체가 낯선 사용자가 많습니다. 이 경우 시스템 설정을 기본으로 따르되, 첫 방문 시 한 번만 안내 문구를 노출하는 것을 권장합니다. "현재 OS 설정에 맞춰 다크모드가 적용됩니다. 변경하려면 우측 상단 버튼을 눌러주세요" 정도의 툴팁이면 충분합니다.

Laptop displaying CSS media query prefers-color-scheme code for dark mode implementation Developer Related Image

결론: UI는 적을수록 낫다

정리하면 이렇습니다.

  • 상시 노출 토글 → 2단계(현재 상태의 반대만 노출), 시스템 설정 기본 따름
  • 설정 패널 내 옵션 → 3단계 허용
  • 색상 스킴 3개 이상 → 명시적 선택 UI 필요

토글은 사용자의 본래 목적과 완전히 무관한 컨트롤입니다. 콘텐츠를 읽으러 온 사람에게 선택지 3개를 강요하는 대신, 시스템에 맡기고 필요할 때 한 번의 클릭으로 해결하게 하세요. UI가 줄어들면 인지 부하도 줄고, 코드도 단순해집니다.

이 주제와 함께 보면 좋은 글들이에요. 최신 CSS 기능을 실무에 어떻게 적용할지 고민된다면 2025년 CSS 최신 기능 총정리 가이드를, 인증 레이어에서 제로 트러스트를 고민 중이라면 Cloudflare Sandbox로 에이전트 인증 해결하기를 참고해 보세요.

근거자료: CSS-Tricks - Dark mode toggles: two states are enough

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.