CSS 다크 모드 자동 감지 & Storybook 테마 연동

규돌·2026년 4월 14일

이 글은 Claude Code로 요약하였습니다

사전 정보

prefers-color-scheme 미디어 쿼리

CSS 미디어 쿼리의 일종으로, 사용자의 OS 또는 브라우저 설정이 다크 모드인지 라이트 모드인지를 감지한다.

@media (prefers-color-scheme: dark) {
  /* OS가 다크 모드일 때 적용 */
}

JS 없이 순수 CSS만으로 OS 테마를 감지할 수 있어, 초기 렌더링 시 깜빡임(FOUC) 없이 테마를 적용할 수 있다.


data-theme 속성 패턴

<html> 또는 최상위 요소에 data-theme="dark" 같은 속성을 붙이고, CSS에서 이를 셀렉터로 활용해 테마를 전환하는 패턴이다.

[data-theme="dark"] {
  --color-bg-main: #242424;
}

JS로 속성값만 바꾸면 전체 CSS 변수가 일괄 교체되어, 컴포넌트 코드는 건드리지 않아도 된다.


@storybook/addon-themes

Storybook 툴바에 테마 토글 버튼을 추가해주는 공식 애드온. withThemeByDataAttribute 데코레이터를 제공하며, 토글 클릭 시 story iframe의 <html> 요소에 지정한 data-* 속성을 자동으로 적용한다.

  • 목적: Storybook에서 라이트/다크 테마를 수동으로 전환하며 컴포넌트를 확인
  • 사용 방법: preview.ts에 데코레이터 등록 후 main.ts의 addons 배열에 추가

Storybook manager.ts

Storybook UI(사이드바, 툴바 등 shell 영역)의 테마를 설정하는 파일. story 콘텐츠가 렌더링되는 iframe과는 별개의 영역이다.

  • 목적: Storybook shell 자체를 다크 테마로 변경
  • 사용 방법: addons.setConfig({ theme: themes.dark }) 호출

작업 흐름

tokens.css — prefers-color-scheme 미디어 쿼리 추가
    ↓
Storybook 애드온 설치 및 preview.ts 설정
    ↓
preview.css — docs 배경 다크 모드 오버라이드
    ↓
manager.ts — Storybook UI 다크 테마 적용


1. tokens.css — prefers-color-scheme 미디어 쿼리 추가

기존에는 [data-theme="dark"] 셀렉터만 있어서, JS로 속성을 직접 세팅해야만 다크 모드가 적용됐다. OS 테마를 자동으로 따라가게 하려면 prefers-color-scheme 미디어 쿼리를 추가해야 한다.

셀렉터를 :root:not([data-theme="light"])로 구성한 게 핵심이다. 이렇게 하면 세 가지 경우를 모두 처리할 수 있다.

상황결과
OS 다크 모드, data-theme 없음다크 모드 적용
OS 다크 모드, data-theme="light"라이트 모드 강제 유지
data-theme="dark" 명시기존 셀렉터가 처리
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --color-bg-main: #242424;
    --color-bg-sub:  #1a1a1a;
    --color-contents-default: #ffffff;
    /* ... 나머지 다크 모드 변수들 */
  }
}

/* 명시적 다크 모드 (JS로 토글하는 경우) */
[data-theme='dark'] {
  --color-bg-main: #242424;
  /* ... 동일한 변수들 */
}

두 블록의 변수값은 동일하다. OS 자동 감지와 수동 토글 모두 같은 다크 테마를 적용한다.


2. Storybook 애드온 설치 및 preview.ts 설정

npm install --save-dev @storybook/addon-themes

main.ts의 addons 배열에 등록하고, preview.ts에 withThemeByDataAttribute 데코레이터를 추가한다.

// .storybook/preview.ts
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import type { Preview, ReactRenderer } from '@storybook/react-vite';

const preview: Preview = {
  decorators: [
    withThemeByDataAttribute<ReactRenderer>({
      themes: {
        light: 'light',
        dark: 'dark',
      },
      defaultTheme: 'light',
      attributeName: 'data-theme',  // <html data-theme="light|dark">
    }),
  ],
  // ...
};

이 데코레이터는 Storybook 툴바에 Light / Dark 토글을 추가하고, 선택된 테마값을 story iframe의 <html> 요소에 data-theme 속성으로 주입한다. tokens.css의 [data-theme="dark"] 셀렉터가 그대로 반응한다.


3. preview.css — docs 배경 다크 모드 오버라이드

Storybook docs 모드에서는 Storybook 자체 스타일이 배경색을 덮어씌운다. data-theme="dark" 가 <html>에 적용돼도 docs 페이지 배경이 흰색으로 남는 문제가 있어, !important로 직접 오버라이드했다.

/* .storybook/preview.css */

/* 툴바 토글로 Dark 선택 시 */
html[data-theme='dark'] body,
html[data-theme='dark'] #storybook-docs,
html[data-theme='dark'] .sbdocs-wrapper,
html[data-theme='dark'] .sbdocs-content {
  background-color: #242424 !important;
  color: #ffffff;
}

html[data-theme='dark'] .docs-story {
  background-color: #1a1a1a !important;
}

/* OS 다크 모드 자동 감지 */
@media (prefers-color-scheme: dark) {
  html:not([data-theme='light']) body,
  html:not([data-theme='light']) #storybook-docs,
  html:not([data-theme='light']) .sbdocs-wrapper {
    background-color: #242424 !important;
  }
}

이 파일은 preview.ts에서 import해 story iframe에 주입된다.


4. manager.ts — Storybook UI 다크 테마 적용

docs 배경과 달리, Storybook 사이드바·툴바 영역(manager)은 story iframe 외부에 있어 CSS 오버라이드가 통하지 않는다. storybook/theming의 빌트인 테마를 사용해야 한다.

// .storybook/manager.ts
import { addons } from 'storybook/manager-api';
import { themes } from 'storybook/theming/create';

const isDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;

addons.setConfig({
  theme: isDark ? themes.dark : themes.light,
});

@storybook/theming 패키지를 별도 설치할 필요 없이 storybook 패키지에 포함된 storybook/theming/create에서 가져올 수 있다. OS 테마를 window.matchMedia로 감지해 초기 테마를 결정한다.

manager는 story 툴바 토글과 연동되지 않는다. OS 설정 기준으로만 동작한다.

profile
회고하기 위한 저장소

0개의 댓글