🌙 CSS · JavaScript · UX

다크모드 대응 완전 정복

CSS 변수와 prefers-color-scheme으로 다크모드를 지원하고,
토글 버튼으로 수동 전환까지 — 복붙 한 번에 내 사이트에 바로 적용해요.

⏱️ 약 10분 📅 2026년 4월 🏷️ CSS · JS · 다크모드 👶 입문 난이도
① 개념

다크모드란 무엇인가요?

배경을 어둡게, 텍스트를 밝게 — 눈의 피로를 줄이고 배터리를 아끼는 UI 테마

다크모드(Dark Mode)는 화면 배경을 어두운 색으로, 텍스트와 아이콘을 밝은 색으로 표시하는 UI 테마예요. iOS 13과 Android 10이 시스템 차원에서 다크모드를 도입한 이후 웹에서도 필수 기능으로 자리 잡았어요. 사용자의 절반 이상이 다크모드를 선호하거나 야간에 사용하기 때문에, 대응하지 않으면 사용성이 크게 떨어질 수 있어요.

웹에서 다크모드를 감지하는 핵심 도구는 CSS 미디어 쿼리 prefers-color-scheme: dark예요. 사용자의 OS 설정이 다크모드일 때 이 쿼리가 true가 되어, 별도의 JavaScript 없이도 자동으로 다크 스타일을 적용할 수 있어요.

👁️

눈의 피로 감소

어두운 환경에서 밝은 화면은 눈을 피로하게 해요. 다크모드는 빛 방출을 줄여 장시간 사용에 편해요.

🔋

배터리 절약

OLED 디스플레이에서는 검정 픽셀이 꺼져 있어 다크모드가 배터리를 최대 30%까지 아낄 수 있어요.

사용자 경험 향상

사용자가 원하는 테마로 볼 수 있다는 것 자체가 배려예요. 체류 시간과 만족도가 올라가요.

🎨

세련된 디자인

다크모드를 지원하는 사이트는 전문적이고 완성도 높은 인상을 줘요. 브랜드 신뢰도에도 도움이 돼요.

💡

prefers-color-scheme이란? 사용자의 OS 또는 브라우저 설정에서 선호하는 색상 테마를 감지하는 CSS 미디어 쿼리예요. @media (prefers-color-scheme: dark) { ... } 안에 작성한 스타일은 다크모드 사용자에게만 적용돼요. Chrome, Firefox, Safari, Edge 모두 지원하는 표준 기능이에요.

다크모드 적용 전후 비교

항목 다크모드 미적용 다크모드 적용
야간 사용성눈부심 심함편안한 시청
OLED 배터리빠르게 소모최대 30% 절약
사용자 만족도낮을 수 있음선택권 제공
브랜드 이미지기본 수준완성도 높음
② 구현 방법 3가지

다크모드 구현 방법 비교

프로젝트 규모와 요구사항에 따라 3가지 방법 중 선택하세요

방법 ①
미디어쿼리만 사용
JS 없이 CSS만으로 구현
코드가 간단하고 빠름
자동으로 OS 설정 따라감
수동 전환 버튼 추가 불가
사용자 설정 저장 불가
방법 ②
CSS 변수 + 미디어쿼리
색상 관리가 체계적
유지보수가 쉬움
나중에 JS 토글 추가 용이
변수 체계 설계 필요
초기 설정에 시간 필요
방법 ③
JS 토글 버튼 추가
사용자가 직접 전환 가능
localStorage로 설정 저장
가장 완성도 높은 UX
JS 코드 추가 필요
구현 복잡도 약간 높음

추천: 바이브 코딩으로 사이트를 만든다면 방법 ③ (CSS 변수 + JS 토글)을 추천해요. 처음엔 코드가 조금 많아 보이지만, 아래 완성 코드를 통째로 복붙하면 돼요. 사용자 경험이 훨씬 좋아지고, 나중에 색상 변경도 변수 하나만 바꾸면 돼요.

CSS 변수 방식의 핵심 원리

CSS 변수(Custom Properties)를 사용하면 색상을 한 곳에서만 정의하고, 다크모드에서는 그 변수값만 바꾸면 전체 스타일이 자동으로 바뀌어요.

CSS 변수 방식 원리
/* 라이트모드 기본값 */
:root {
  --bg: #ffffff;
  --text: #111827;
  --surface: #f9fafb;
  --border: #e5e7eb;
}

/* OS가 다크모드일 때 변수값만 교체 */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1e1e2e;
    --text: #cdd6f4;
    --surface: #282a36;
    --border: #2d2d3f;
  }
}

/* 나머지 스타일은 변수를 참조 — 테마 바뀌어도 안 건드려도 됨 */
body {
  background: var(--bg);
  color: var(--text);
}
.card {
  background: var(--surface);
  border: 1px solid var(--border);
}
③ 완성 코드

복붙하면 바로 되는 완성 코드

CSS 변수 + 미디어쿼리 + JS 토글 버튼까지 한 번에 적용되는 완전한 코드예요

style.css + index.html (통합)
/* ① CSS 변수 정의 — <head> 안 <style> 태그에 추가 */

:root {
  --bg:          #ffffff;
  --bg-surface:  #f9fafb;
  --text:        #111827;
  --text-muted:  #6b7280;
  --border:      #e5e7eb;
  --accent:      #4f46e5;
}

/* ② OS 다크모드 자동 대응 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:          #1e1e2e;
    --bg-surface:  #282a36;
    --text:        #cdd6f4;
    --text-muted:  #a6adc8;
    --border:      #2d2d3f;
    --accent:      #7c6dfa;
  }
}

/* ③ JS 토글로 강제 다크모드 */
[data-theme="dark"] {
  --bg:          #1e1e2e;
  --bg-surface:  #282a36;
  --text:        #cdd6f4;
  --text-muted:  #a6adc8;
  --border:      #2d2d3f;
  --accent:      #7c6dfa;
}

/* ④ 전환 애니메이션 (선택) */
*, *::before, *::after {
  transition: background-color 0.25s, color 0.25s, border-color 0.25s;
}

/* ⑤ 기본 스타일 — 변수만 참조 */
body { background: var(--bg); color: var(--text); }
.card { background: var(--bg-surface); border: 1px solid var(--border); }

/* ⑥ 토글 버튼 스타일 */
#theme-toggle {
  position: fixed;
  bottom: 24px; right: 24px;
  width: 44px; height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-surface);
  color: var(--text);
  font-size: 1.2rem;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0,0,0,.15);
  transition: transform .2s, box-shadow .2s;
  display: flex; align-items: center; justify-content: center;
}
#theme-toggle:hover { transform: scale(1.1); }


<!-- ⑦ HTML — </body> 바로 위에 추가 -->

<button id="theme-toggle" aria-label="다크모드 전환">🌙</button>

<script>
// 저장된 테마 불러오기
const saved = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = saved === 'dark' || (!saved && prefersDark);

if (isDark) document.documentElement.setAttribute('data-theme', 'dark');

const btn = document.getElementById('theme-toggle');
btn.textContent = isDark ? '☀️' : '🌙';

btn.addEventListener('click', () => {
  const current = document.documentElement.getAttribute('data-theme');
  if (current === 'dark') {
    document.documentElement.removeAttribute('data-theme');
    document.documentElement.setAttribute('data-theme', 'light');
    localStorage.setItem('theme', 'light');
    btn.textContent = '🌙';
  } else {
    document.documentElement.setAttribute('data-theme', 'dark');
    localStorage.setItem('theme', 'dark');
    btn.textContent = '☀️';
  }
});
</script>
⚠️

트랜지션 주의: transition* 전체에 적용하면 페이지 로드 시 깜빡임이 생길 수 있어요. <script><head> 안에서 가장 먼저 실행하거나, transition을 특정 클래스에만 적용하는 방식으로 해결할 수 있어요.

④ 만들어보기

테마 전환 미리보기

라이트 / 다크 / 시스템 버튼을 눌러서 카드 미리보기가 실시간으로 바뀌는 걸 확인해보세요

LIVE PREVIEW
새 도구
다크모드를 지원해요

이 카드는 라이트모드와 다크모드에서 각각 다른 색상을 보여줘요. CSS 변수 하나만 바꾸면 전체가 자동으로 전환돼요.

💡

시스템을 누르면 현재 여러분의 OS 다크모드 설정을 따라가요. OS가 다크모드라면 카드가 어둡게, 라이트모드라면 밝게 표시돼요.

⑤ FAQ

자주 묻는 질문

다크모드 구현 중 가장 많이 막히는 부분들을 모았어요

사용자가 선택한 테마를 다음 방문에도 유지하려면?

localStorage를 사용하면 돼요. 사용자가 다크모드를 선택했을 때 localStorage.setItem('theme', 'dark')로 저장하고, 페이지 로드 시 localStorage.getItem('theme')으로 읽어와서 적용하면 돼요.

중요한 점은 이 코드를 <head> 안에서 최대한 일찍 실행해야 한다는 거예요. </body> 바로 위에 두면 페이지가 잠깐 라이트모드로 보였다가 전환되는 "깜빡임(FOUC)" 현상이 생겨요. <head> 안에서 인라인 스크립트로 실행하세요.

세션이 끝나도 유지하려면 localStorage, 세션 동안만 유지하려면 sessionStorage를 쓰면 돼요. 바이브 코딩 프로젝트라면 localStorage가 더 적합해요.

이미지도 다크모드에 맞게 바꿀 수 있나요?

네, HTML <picture> 태그와 media 속성을 조합하면 돼요. 다크모드용 이미지와 라이트모드용 이미지를 각각 준비하고 아래처럼 작성하면 자동으로 전환돼요.

<picture><source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)"><img src="logo-light.svg" alt="로고"></picture>

CSS로도 가능해요. @media (prefers-color-scheme: dark) { img.logo { content: url('logo-dark.svg'); } } — 단, CSS content 속성은 일부 브라우저에서 지원이 불안정해서 <picture> 방식이 더 안전해요.

이미지에 filter: invert(1) hue-rotate(180deg)를 다크모드에서 적용하면 색상을 반전시키는 효과를 줄 수도 있어요. 단순 아이콘이나 도식에 유용해요.

다크모드에서 텍스트가 너무 밝아서 불편해요 (color: inherit 문제)

color: inherit을 쓴 요소는 부모의 --text 변수를 제대로 받아오지 못할 수 있어요. 명시적으로 color: var(--text)를 지정해주는 게 가장 확실한 해결책이에요.

다크모드에서는 순수한 흰색(#ffffff)보다 약간 회색빛이 도는 색상(#cdd6f4, #e2e8f0 등)이 눈에 훨씬 편해요. 순백색 텍스트는 어두운 배경과 대비가 너무 강해 오히려 피로감을 줄 수 있어요.

접근성 가이드라인(WCAG AA)에서는 배경과 텍스트의 명도 대비가 최소 4.5:1 이상이어야 해요. WebAIM 대비 검사기로 선택한 색상 조합이 기준을 통과하는지 꼭 확인하세요.

다크모드 전환 시 부드러운 트랜지션을 추가하려면?

*, *::before, *::after { transition: background-color 0.25s, color 0.25s, border-color 0.25s; } 를 CSS에 추가하면 돼요. 모든 요소에 0.25초 트랜지션이 적용돼서 테마 전환이 부드러워요.

단, transition: all은 피하세요. 레이아웃 관련 속성(width, height, transform 등)까지 포함되어 성능 문제가 생길 수 있어요. 색상 관련 속성만 명시적으로 지정하세요.

페이지 첫 로드 때 트랜지션이 적용되면 깜빡임 현상이 생길 수 있어요. body.ready { transition: ... }처럼 클래스를 조건부로 붙이거나, JS에서 테마 적용 후 약간의 딜레이를 주고 트랜지션 클래스를 추가하는 방법으로 해결할 수 있어요.

OS 설정과 토글 버튼의 우선순위는 어떻게 되나요?

일반적으로 사용자가 직접 누른 토글 버튼이 OS 설정보다 우선해야 해요. 사용자의 명시적 선택을 더 존중하는 것이 좋은 UX예요.

권장 우선순위: localStorage 저장값 → OS 설정 → 라이트모드(기본값) 순서로 적용해요. 즉, 사용자가 직접 토글하면 localStorage에 저장하고, 다음 방문 때 그 값을 먼저 읽어와요. localStorage 값이 없으면 OS 설정을 따라가요.

data-theme 속성이 없을 때는 미디어쿼리가 작동하고, data-theme="dark"data-theme="light"가 있으면 미디어쿼리를 무시하고 해당 테마를 적용해요. 이 패턴이 가장 깔끔한 구현이에요.

⑥ 다음 단계

다음에 배워볼 것들

다크모드를 마스터했다면 이 도구들도 도전해보세요

🎨
CSS 변수 완전 정복
CSS Custom Properties로 디자인 시스템을 만드는 방법
CSS
📐
Flexbox 레이아웃
CSS Flexbox로 반응형 레이아웃을 쉽게 만드는 법
CSS · 레이아웃
💡
플로팅 버튼
화면에 고정되는 플로팅 버튼 만들기
CSS · JS