버튼 하나 만들 때마다 색과 크기를 새로 정하다 보면 화면마다 미묘하게 다른 버튼이 쌓입니다. 디자인 시스템은 그 흩어진 값을 한곳에 모아 두고 재사용하는 방식입니다. 하드코딩된 스타일이 어떻게 시스템으로 정리되는지 코드로 보겠습니다.
33.1 하드코딩이 쌓이는 방식
처음엔 별문제가 없어 보입니다. 버튼이 필요할 때마다 그 자리에서 색과 여백을 적어 넣으면 되니까요.
.save-btn {
background: #2563eb;
color: #ffffff;
padding: 10px 16px;
border-radius: 6px;
font-size: 14px;
}
.submit-btn {
background: #2f6bed;
color: #fff;
padding: 9px 18px;
border-radius: 5px;
font-size: 15px;
}
두 버튼은 같은 파란 버튼이 되려던 것인데 색도 여백도 모서리도 다 다릅니다. 이런 파일이 수십 개 쌓이면 어느 것이 진짜 우리 파랑인지 아무도 답하지 못합니다.
33.2 토큰으로 값을 한곳에 모으기
흩어진 값을 이름 붙은 변수로 끌어모으는 것이 첫걸음입니다. CSS 커스텀 프로퍼티를 :root 에 선언하면 문서 전체에서 그 이름을 씁니다.
:root {
--color-primary: #2563eb;
--color-on-primary: #ffffff;
--space-2: 8px;
--space-3: 12px;
--radius-md: 6px;
--font-size-body: 14px;
}
이제 파랑은 --color-primary 한 곳에서만 정의됩니다. 이 값의 묶음을 디자인 토큰이라고 부르고, 색과 간격과 모서리 같은 결정을 이름으로 바꿔 놓은 것입니다.
33.3 공용 컴포넌트로 형태를 통일하기
토큰을 만들었으면 그 토큰을 소비하는 공용 클래스를 하나 둡니다. 버튼의 모양은 이 클래스 한 곳에서만 결정됩니다.
.btn {
background: var(--color-primary);
color: var(--color-on-primary);
padding: var(--space-2) var(--space-3);
border: none;
border-radius: var(--radius-md);
font-size: var(--font-size-body);
cursor: pointer;
}
이제 파란 버튼이 필요하면 새 CSS를 쓰지 않고 마크업에서 클래스만 붙입니다. 형태의 정의가 딱 한 벌만 존재합니다.
<button class="btn">저장</button>
<button class="btn">제출</button>
두 버튼은 이제 한 치의 오차도 없이 똑같습니다. 33.1의 두 버튼이 제각각이던 문제가 클래스 재사용만으로 사라집니다.
33.4 변형은 규칙으로 파생시키기
모든 버튼이 똑같을 수는 없습니다. 강조 버튼과 보조 버튼처럼 갈래가 필요할 때는 공용 클래스에 변형을 얹습니다. 이름 짓는 방식으로 BEM 규칙을 쓰면 관계가 눈에 보입니다.
.btn--primary {
background: var(--color-primary);
color: var(--color-on-primary);
}
.btn--ghost {
background: transparent;
color: var(--color-primary);
border: 1px solid var(--color-primary);
}
.btn--sm {
padding: 4px 10px;
font-size: 12px;
}
기본 .btn 은 공통 뼈대를 담고, --primary 와 --ghost 는 색의 갈래를, --sm 은 크기의 갈래를 담습니다. 클래스를 겹쳐 붙이면 조합이 만들어집니다.
<button class="btn btn--primary">결제</button>
<button class="btn btn--ghost btn--sm">취소</button>
변형이 늘어도 색과 간격은 여전히 토큰을 참조합니다. 갈래는 많아지되 근원이 되는 값은 하나로 유지됩니다.
33.5 단일 소스 오브 트루스
시스템의 핵심은 이 한 문장입니다. 하나의 결정은 하나의 자리에서만 산다. 브랜드 파랑을 바꿔야 한다면 이제 파일을 뒤질 필요가 없습니다.
:root {
--color-primary: #7c3aed;
}
이 한 줄만 고치면 .btn 도 .btn--ghost 의 테두리도 전부 보라색으로 따라옵니다. 값이 한 자리에 있으니 고칠 자리도 한 곳입니다. 이것이 단일 소스 오브 트루스이고, 디자인 시스템이 주는 가장 큰 안심입니다.
33.6 상태까지 시스템 안으로
버튼은 가만히 있는 그림이 아니라 눌리고 비활성화되는 물건입니다. 이런 상태도 공용 클래스 안에서 토큰으로 다뤄야 흩어지지 않습니다.
:root {
--color-primary-hover: #1d4ed8;
}
.btn {
transition: background .15s ease;
}
.btn:hover {
background: var(--color-primary-hover);
}
.btn:disabled {
opacity: .5;
cursor: not-allowed;
}
.btn:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
마우스를 올렸을 때와 못 누를 때, 키보드로 짚었을 때의 모습이 전부 이 한 벌에 담깁니다. 새 버튼을 만들 때마다 상태를 다시 고민하지 않아도 됩니다.
33.7 다크 모드도 토큰 교체로
값을 토큰으로 모아 두면 배경 테마를 바꾸는 일이 놀랄 만큼 간단해집니다. 컴포넌트 코드는 그대로 두고 토큰 값만 다른 조건에서 덮어씁니다.
:root {
--color-surface: #ffffff;
--color-text: #111827;
}
@media (prefers-color-scheme: dark) {
:root {
--color-surface: #1f2937;
--color-text: #f9fafb;
--color-primary: #60a5fa;
}
}
.card {
background: var(--color-surface);
color: var(--color-text);
padding: var(--space-3);
border-radius: var(--radius-md);
}
어두운 환경을 선호하는 기기에서는 토큰 값이 갈리면서 모든 카드와 버튼이 함께 어두워집니다. .card 규칙은 한 줄도 건드리지 않았습니다. 컴포넌트가 값을 직접 품지 않고 토큰을 바라보기 때문에 가능한 일입니다.
33.8 접근성을 시스템에 내장하기
좋은 시스템은 규칙을 사람의 기억에 맡기지 않고 코드에 새겨 둡니다. 움직임을 줄이려는 사용자의 설정 같은 배려도 공용 코드에 넣어 두면 모든 컴포넌트가 알아서 따릅니다.
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: .01ms;
animation-duration: .01ms;
}
}
이 한 블록이 시스템 안의 모든 전환과 애니메이션을 사용자의 뜻에 맞춰 잦아들게 합니다. 접근성이 각 컴포넌트가 따로 챙길 숙제가 아니라 시스템의 기본 성격이 되는 것입니다.
33.9 정리
디자인 시스템은 거창한 도구가 아니라 재사용 가능한 토큰과 컴포넌트와 규칙을 한데 묶어 둔 것입니다. 값은 :root 토큰에 모으고, 형태는 공용 클래스에 담고, 갈래는 변형으로 파생시키면 화면이 늘어도 결정은 흩어지지 않습니다. 손이 기억하던 색과 여백을 이름으로 옮겨 두는 순간, 고칠 곳은 언제나 한 자리로 줄어듭니다.