다크 모드 완벽 대응: 시맨틱 토큰 레이어로 자동화 시스템 구축하기

다크 모드 디자인 시스템: 시맨틱 토큰 레이어로 자동화 구현하기

Dark Mode

라이트 모드에서 정교하게 설계된 화면을 다크 모드로 전환했을 때, 단순히 색상만 반전시켜서 가독성이 깨지거나 브랜드 아이덴티티가 무너지는 경험을 해보셨나요? 특히 수백 개의 화면을 가진 대규모 프로젝트에서 일일이 하드코딩된 색상 값을 찾아 다크 모드 대응 코드를 추가하는 작업은 개발자에게는 야근을, 디자이너에게는 끝없는 검수 작업을 강요하는 비효율의 극치입니다.

이 문제를 근본적으로 해결하는 열쇠는 바로 ‘시맨틱 토큰(Semantic Token)’ 레이어에 있습니다. 색상을 “어떤 색인가(What color)”가 아닌 “어디에 쓰이는가(Where/How)”로 정의하는 계층 구조를 구축하면, 테마 전환은 단순히 값의 교체가 아니라 시스템의 자연스러운 확장이 됩니다. 이번 글에서는 다크 모드를 완벽하게 자동화할 수 있는 시맨틱 토큰 설계 전략과 실무 트러블슈팅 사례를 깊이 있게 다뤄보겠습니다.

프리미티브 토큰과 시맨틱 토큰의 분리

다크 모드 자동화의 첫 단추는 토큰의 위계를 나누는 것입니다. 많은 팀이 실수하는 지점이 #FFFFFFwhite라는 이름으로 정의하고 이를 코드에서 직접 사용하는 것입니다. 다크 모드에서는 이 whiteblack으로 변해야 하는데, 이름과 값의 괴리가 발생하면서 논리가 꼬이기 시작합니다.

가장 이상적인 구조는 ‘프리미티브(Primitive)’ 레이어와 ‘시맨틱(Semantic)’ 레이어를 엄격히 분리하는 것입니다. 프리미티브 토큰은 gray-10, blue-500처럼 색상의 고유한 명칭을 가집니다. 반면 시맨틱 토큰은 bg-primary, text-subtle, border-strong처럼 역할에 따른 이름을 가집니다.

다크 모드 시스템이 가동되면, bg-primary라는 시맨틱 토큰은 라이트 모드일 때 프리미티브의 white를 참조하고, 다크 모드일 때 gray-900을 참조하도록 설정합니다. 개발자나 디자이너는 오직 bg-primary라는 이름만 기억하면 되며, 테마에 따라 어떤 물리적 색상이 뿌려질지는 시스템이 결정하게 됩니다.

[💡 에디터의 실무 팁: 다크 모드 전용 프리미티브 색상을 따로 정의하지 마세요. 하나의 공통 컬러 팔레트 안에서 테마별로 적절한 명도(Luminance) 단계를 매핑하는 것이 관리 포인트를 줄이는 비결입니다.]

트러블슈팅: 다크 모드에서의 레이어 고립 문제(Elevation)

실무에서 다크 모드를 구현할 때 가장 골치 아픈 상황은 ‘레이어의 깊이감’이 사라지는 현상입니다. 라이트 모드에서는 그림자(Drop Shadow)를 통해 요소의 높낮이를 표현하기 쉽지만, 배경이 어두운 다크 모드에서는 그림자가 거의 보이지 않습니다. 이 때문에 카드 UI나 모달이 배경색과 섞여버리는 고립 문제가 발생합니다.

저는 이 문제를 해결하기 위해 시맨틱 토큰에 ‘엘리베이션(Elevation)’ 개념을 도입합니다. 다크 모드에서는 위로 올라온 요소(예: 모달, 팝업)일수록 배경색보다 더 밝은 회색을 사용하도록 설계하는 것이 핵심입니다.

  • surface-level-0 (가장 바닥 배경): gray-950

  • surface-level-1 (기본 카드): gray-900

  • surface-level-2 (플로팅 버튼, 모달): gray-800

이렇게 설계하면 별도의 그림자 효과 없이도 사용자는 시각적으로 요소의 위계를 인지할 수 있습니다. 피그마 변수(Variables) 기능을 사용할 때, Mode를 추가하여 각 레벨에 맞는 색상을 매핑해두면 디자인 과정에서 즉시 확인이 가능합니다.

Style Dictionary를 활용한 테마 자동 생성

설계된 토큰을 코드로 옮기는 과정은 Style Dictionary가 담당합니다. 단순히 하나의 CSS 파일을 만드는 것이 아니라, 테마별로 분리된 CSS 변수 집합을 생성해야 합니다.


// style-dictionary.config.js 테마 설정 예시
const StyleDictionary = require(‘style-dictionary’);

module.exports = {
source: [‘tokens/**/*.json’],
platforms: {
css: {
transformGroup: ‘css’,
buildPath: ‘build/css/’,
files: [
{
destination: ‘tokens.css’,
format: ‘css/variables’,
filter: (token) => token.path.includes(‘semantic’) // 시맨틱 토큰만 추출
}
]
}
}
};


이때 팁은 HTML의 data-theme 속성에 따라 변수값이 스위칭되도록 CSS 선택자를 구성하는 것입니다. :root[data-theme='light']:root[data-theme='dark'] 블록 안에 동일한 이름의 시맨틱 토큰을 배치하면, 자바스크립트로 data-theme 값만 바꿔주는 것만으로 서비스 전체의 테마를 1ms 안에 전환할 수 있습니다.

[💡 에디터의 실무 팁: 다크 모드에서 순수 검정(#000000) 배경은 눈의 피로도를 높입니다. #121212#1A1A1A 정도의 짙은 무채색을 베이스로 잡는 것이 훨씬 고급스러운 사용자 경험을 제공합니다.]

접근성 검증: 다크 모드에서도 잃지 말아야 할 대비

디자인 시스템 엔지니어링의 완성은 접근성입니다. 다크 모드는 저시력자나 시각 장애를 가진 사용자에게 유용하지만, 잘못 설계된 대비는 오히려 독이 됩니다. 시맨틱 토큰을 정의할 때 반드시 WCAG 2.1 가이드라인의 대비 수치를 체크해야 합니다.

주요 텍스트는 배경 대비 4.5:1 이상, 장식 요소는 3:1 이상을 유지해야 합니다. 저는 빌드 파이프라인에 color-contrast 체크 라이브러리를 결합하여, 다크 모드 토큰 값이 이 기준을 충족하지 못할 경우 빌드 오류를 발생시키도록 설정합니다. 시스템이 인간의 실수를 원천 차단하게 만드는 것이 DesignOps의 궁극적인 목표이기 때문입니다.

자주 묻는 질문(FAQ)

Q1. 다크 모드용 이미지는 어떻게 처리하나요? 시맨틱 토큰만으로 해결되지 않는 이미지나 일러스트레이션은 CSS의 content 속성이나 HTML의 <picture> 태그를 활용하세요. 혹은 테마에 따라 filter: brightness(0.8) 등을 적용해 명도를 조절하는 시맨틱 클래스를 부여하는 방식도 효율적입니다.

Q2. 시스템 설정(OS)의 다크 모드와 연동하려면 어떻게 하나요? CSS 미디어 쿼리인 @media (prefers-color-scheme: dark)를 사용하면 됩니다. 하지만 사용자에게 선택권을 주는 것이 최선이므로, 시스템 설정을 따르되 사용자가 직접 테마를 토글할 수 있는 기능을 제공하는 것을 추천합니다.

Q3. 시맨틱 토큰 이름이 너무 많아지면 어떻게 관리하나요? 처음부터 모든 경우의 수를 다 만들지 마세요. bg, text, border, icon이라는 4대 카테고리에서 시작하여 필요한 경우에만 세분화하는 ‘점진적 확장’ 전략을 취하는 것이 관리 부하를 줄이는 방법입니다.

댓글 남기기