한 줄 요약 — 다크 모드는 페이지를 새로 만드는 게 아니라, 색만 갈아 끼우는 일이다. 색 값을 CSS 변수 한곳에 모아 두면, prefers-color-scheme로 시스템이 밤인지 낮인지 감지해 변수 값만 바꿔 라이트·다크 두 모습을 만들 수 있다. 오늘은 같은 마크업을 색 변수만으로 두 테마로 전환해 본다.

학습 목표#

  • 다크 모드의 색 값을 CSS 변수에 모아 한곳에서 관리할 수 있다.
  • prefers-color-scheme로 시스템의 라이트/다크 설정을 감지할 수 있다.
  • color-scheme로 입력창·스크롤바 같은 브라우저 기본 UI까지 함께 맞출 수 있다.
  • [data-theme] 속성으로 직접 만드는 토글의 구조를 이해할 수 있다.
  • 다크 모드에서 초보자가 자주 빠지는 실수를 피할 수 있다.

오늘의 비유 — 같은 옷장, 조명만 바꾸기#

옷장 안의 옷은 그대로인데, 낮에 창문으로 들어오는 햇빛 아래에서 보는 색과 밤에 노란 전등 아래에서 보는 색은 사뭇 다르다. 같은 회색 셔츠도 밝은 조명에서는 또렷한 회색이지만, 어두운 방의 은은한 조명 아래서는 차분하게 가라앉아 보인다. 우리가 바꾼 것은 옷이 아니라 조명 하나다.

다크 모드도 똑같다. 페이지의 글과 버튼, 카드 같은 내용물은 그대로 두고, 색이라는 조명만 갈아 끼운다. 그래서 색 값을 마크업 곳곳에 흩어 두면 조명을 한 번에 못 바꾼다. 옷장의 조명 스위치를 한곳에 모아 두듯, 색도 CSS 변수 한곳에 모아 둬야 스위치 한 번으로 방 전체 분위기를 바꿀 수 있다.

핵심 개념#

색을 변수 한곳에 모으기#

먼저 색을 :root의 변수로 모은다. 배경, 글자, 카드, 테두리처럼 역할별 이름으로 둔다. --blue 같은 색 이름이 아니라 --bg, --text처럼 쓰임으로 이름을 지어야 조명을 바꿔도 이름이 그대로 들어맞는다.

:root {
  --bg: #ffffff;
  --text: #1a1a1a;
  --card: #f4f4f5;
  --border: #e0e0e0;
}

body {
  background: var(--bg);
  color: var(--text);
}

이제 본문 어디서도 #ffffff 같은 날색을 직접 쓰지 않고 var(--bg)만 쓴다. 다크 모드는 이 변수 네 개의 값만 바꾸면 끝난다.

prefers-color-scheme — 시스템이 밤인지 낮인지 묻기#

prefers-color-scheme는 사용자가 운영체제나 브라우저에서 고른 라이트/다크 설정을 알려 주는 미디어 쿼리다. 27회에서 화면 너비를 묻던 @media (min-width: ...)와 같은 문법이고, 묻는 대상만 "지금 다크 모드인가요?"로 바뀐 것이다.

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #18181b;
    --text: #f4f4f5;
    --card: #27272a;
    --border: #3f3f46;
  }
}

시스템이 다크로 설정돼 있으면 이 블록 안의 변수 값이 적용돼, 같은 body { background: var(--bg) }가 자동으로 어두운 배경이 된다. 마크업도, 본문 CSS도 하나 안 고쳤다. 조명 스위치(변수)만 내린 셈이다.

color-scheme — 브라우저 기본 UI까지 함께#

배경과 글자는 다크로 바꿨는데 입력창, 스크롤바, 기본 버튼만 여전히 하얗게 떠 보일 때가 있다. 이건 브라우저가 그리는 기본 UI라 우리 변수가 닿지 않기 때문이다. color-scheme로 "이 페이지는 다크도 지원한다"고 알려 주면 브라우저가 그 부품들까지 어두운 톤으로 그려 준다.

:root {
  color-scheme: light dark;
}

문서 첫 페인트부터 적용되도록 HTML(HyperText Markup Language) <head>에 메타 태그로 같이 선언해 두기도 한다.

<meta name="color-scheme" content="light dark" />

[data-theme] — 직접 스위치를 다는 경우#

prefers-color-scheme는 시스템 설정을 따라갈 뿐, 사용자가 페이지 안에서 누르는 토글 버튼과는 다르다. 직접 스위치를 달려면 보통 <html>data-theme 속성을 두고, 그 값에 따라 변수를 바꾼다.

[data-theme="dark"] {
  --bg: #18181b;
  --text: #f4f4f5;
  --card: #27272a;
  --border: #3f3f46;
}

<html data-theme="dark">가 되는 순간 페이지가 다크로 바뀐다. 다만 버튼을 눌러 이 속성값을 light/dark로 바꾸는 동작 자체는 CSS만으로는 안 되고 JavaScript 한 줄이 필요하다. 그 부분은 뒤 시리즈에서 다루고, 오늘은 시스템 설정을 따라가는 prefers-color-scheme 방식까지만 손으로 만들어 본다.

함께 따라하기 — 변수만 바꿔 두 테마 전환하기#

카드 하나를 두고, 시스템 다크 모드를 켜고 끌 때 색이 통째로 바뀌는지 확인한다.

<main class="page">
  <article class="card">
    <h2>오늘의 메모</h2>
    <p>시스템 설정을 바꾸면 이 카드의 색이 함께 바뀝니다.</p>
  </article>
</main>
:root {
  color-scheme: light dark;
  --bg: #ffffff;
  --text: #1a1a1a;
  --card: #f4f4f5;
  --border: #e0e0e0;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #18181b;
    --text: #f4f4f5;
    --card: #27272a;
    --border: #3f3f46;
  }
}

.page {
  min-height: 100vh;
  padding: 40px;
  background: var(--bg);
  color: var(--text);
}

.card {
  max-width: 360px;
  padding: 20px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
}

저장하고 열어 보면, 라이트 설정에서는 흰 배경에 옅은 회색 카드가 보인다. 운영체제의 다크 모드를 켜면(맥은 시스템 설정의 화면, 윈도우는 개인 설정의 색) 새로고침 없이도 배경이 어두운 먹색으로, 글자는 밝은 회색으로, 카드는 한 톤 밝은 짙은 회색으로 한꺼번에 바뀐다. 바꾼 코드는 변수 네 개뿐이다.

흔한 실수 3가지#

1. 시스템 설정을 무시하고 강제 라이트만 둔다#

방문자가 밤에 다크 모드를 켜 둔 채로 들어왔는데, 페이지만 혼자 새하얗게 빛나면 눈이 부시다. prefers-color-scheme를 아예 안 쓰고 흰 배경만 박아 두면 이런 일이 생긴다.

/* (다크 모드를 켠 사용자에게도 흰 화면이 그대로 노출됩니다) */
body {
  background: #ffffff;
  color: #000000;
}

색을 변수로 빼고 @media (prefers-color-scheme: dark)로 다크 값을 한 벌 더 준비해 두면, 시스템 설정을 켠 사용자는 자동으로 어두운 화면을 받는다. 라이트 한 벌만 만들고 끝내지 말고, 다크 한 벌을 같이 둔다.

2. 다크에서 안 보이는 이미지·아이콘을 invert로 강제한다#

검은 로고가 다크 배경에서 안 보인다고 페이지 전체에 filter: invert(1)을 거는 경우가 있다. 글자와 배경은 어찌어찌 뒤집히지만, 사진은 색이 기괴하게 반전되고 파란 로고가 주황으로 변한다.

/* (사진까지 색이 반전돼 인물 얼굴이 음화처럼 보입니다) */
@media (prefers-color-scheme: dark) {
  img {
    filter: invert(1);
  }
}

invert는 모든 색을 일괄로 뒤집는 무딘 칼이라 사진에는 못 쓴다. 로고처럼 단색 아이콘은 라이트용·다크용 이미지를 따로 두거나, SVG라면 fill: var(--text)로 글자색을 따라가게 한다. 사진은 보통 그대로 둬도 괜찮고, 너무 밝아 튀면 살짝만 어둡게 낮춘다.

@media (prefers-color-scheme: dark) {
  .photo {
    filter: brightness(0.85);
  }
}

3. 색만 바꾸고 그림자·테두리는 라이트 그대로 둔다#

배경과 글자색만 다크로 바꾸고 끝내면, 35회에서 만든 옅은 검정 그림자가 어두운 배경 위에서 사라져 카드가 납작해 보인다. 라이트에서 쓰던 옅은 회색 테두리도 다크 배경에선 거의 안 보인다.

/* (어두운 배경에서는 검은 그림자가 묻혀 카드가 떠 보이지 않습니다) */
.card {
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}

밝은 방에선 옅은 그림자로 입체감을 주지만, 어두운 방에선 같은 그림자가 배경에 묻힌다. 다크에서는 그림자를 더 진하게 주거나, 그림자 대신 살짝 밝은 테두리로 카드 경계를 살린다. 테두리 색도 변수로 빼 뒀다면 --border 값만 바꿔 두 테마에 맞출 수 있다.

@media (prefers-color-scheme: dark) {
  .card {
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5);
  }
}

오늘 배운 것 체크리스트#

  • 색 값을 --bg, --text처럼 역할별 CSS 변수로 모을 수 있다.
  • @media (prefers-color-scheme: dark)로 시스템 다크 설정을 감지해 변수만 바꿀 수 있다.
  • color-scheme로 입력창·스크롤바 같은 브라우저 기본 UI까지 다크로 맞춘다.
  • 다크에서 이미지는 invert 대신 별도 이미지나 밝기 조정으로 처리한다.
  • 색뿐 아니라 그림자·테두리도 테마에 맞춰 함께 바꾼다.

자주 묻는 질문#

Q. 다크모드 토글을 CSS만으로 만들 수 있나요?

A. 시스템 설정을 그대로 따라가는 방식은 prefers-color-scheme만으로 CSS로 충분합니다. 다만 페이지 안에 버튼을 두고 사용자가 직접 라이트/다크를 누르는 토글은, [data-theme] 속성값을 바꾸는 동작에 JavaScript 한 줄이 필요해 CSS만으로는 완성되지 않습니다. CSS는 색을 정의하는 일까지, 스위치를 누르는 일은 JS의 몫입니다.

Q. 시스템 다크모드 감지는 어떻게 하나요?

A. @media (prefers-color-scheme: dark) 미디어 쿼리를 쓰면 사용자가 운영체제나 브라우저에서 고른 다크 설정을 감지할 수 있습니다. 이 블록 안에서 CSS 변수 값만 다크용으로 바꿔 두면, 시스템이 다크일 때 자동으로 어두운 화면이 적용됩니다.

Q. color-scheme 메타 태그는 왜 필요한가요?

A. 배경과 글자는 우리 CSS로 바꿀 수 있지만, 입력창·스크롤바·기본 버튼처럼 브라우저가 직접 그리는 UI는 우리 변수가 닿지 않습니다. <meta name="color-scheme" content="light dark" />로 이 페이지가 다크도 지원한다고 알려 주면, 브라우저가 그 기본 부품들까지 어두운 톤으로 그려 흰 입력창만 떠 보이는 문제를 막아 줍니다.

다음 시간 예고#

내일은 웹 접근성 기초 — 시맨틱·ARIA·색 대비·포커스를 다룬다. 오늘 맞춘 색이 모두에게 잘 보이는지(색 대비), 키보드만으로도 페이지를 끝까지 쓸 수 있는지(포커스 표시)까지 한 번에 챙겨 본다.

더 알아보기#