디자인 토큰은 디자인 결정 하나에 이름을 붙인 것입니다. 잘 지은 토큰은 한 겹이 아니라 세 겹으로 쌓입니다. 원시 값에서 시맨틱 이름을 거쳐 컴포넌트 토큰으로 이어지는 이 체인을 CSS 변수로 직접 만들어 보겠습니다.

35.1 1계층: 원시 토큰

가장 밑바닥에는 생김새만 담은 팔레트가 깔립니다. 색의 이름은 그저 무슨 색 몇 단계일 뿐, 어디에 쓰는지는 아직 말하지 않습니다.


:root {
--blue-400: #60a5fa;
--blue-500: #3b82f6;
--blue-600: #2563eb;
--gray-100: #f3f4f6;
--gray-500: #6b7280;
--gray-900: #111827;
}


이 층은 화면이 직접 바라보지 않습니다. 그저 우리가 쓸 수 있는 원재료가 무엇인지 늘어놓은 물감통에 가깝습니다.

35.2 2계층: 시맨틱 토큰

원시 토큰 위에 역할을 입힙니다. 시맨틱 토큰은 값을 직접 갖지 않고 밑층 원시 토큰을 가리킵니다. 여기서부터 이름이 쓰임새를 말하기 시작합니다.


:root {
--color-primary: var(--blue-600);
--color-text: var(--gray-900);
--color-text-muted: var(--gray-500);
--color-surface: var(--gray-100);
}


--color-primary 는 자기 안에 색값을 품지 않고 --blue-600 을 참조합니다. 이 참조가 바로 토큰 체인입니다. 화면은 파랑이 무엇인지 몰라도 되고 강조색이라는 역할만 알면 됩니다.

35.3 3계층: 컴포넌트 토큰

맨 위에는 특정 컴포넌트만을 위한 토큰이 놓입니다. 시맨틱 토큰을 다시 한 겹 가리켜, 그 컴포넌트의 국소적인 결정을 담습니다.


:root {
--btn-bg: var(--color-primary);
--btn-fg: #ffffff;
--btn-radius: 6px;
}
.btn {
background: var(--btn-bg);
color: var(--btn-fg);
border-radius: var(--btn-radius);
padding: 10px 16px;
border: none;
cursor: pointer;
}


이제 참조가 세 겹입니다. --btn-bg--color-primary 를 보고, 그것은 --blue-600 을 봅니다. 버튼 색만 따로 바꾸고 싶으면 맨 위 --btn-bg 만 건드리고, 브랜드 전체를 바꾸려면 밑층을 건드립니다. 층마다 바꾸는 범위가 다릅니다.

35.4 체인이 주는 유연함

세 겹으로 나눈 보람은 값을 바꿀 때 드러납니다. 강조색을 파랑에서 보라로 옮기는 일을 봅시다.


:root {
--violet-600: #7c3aed;
--color-primary: var(--violet-600);
}


시맨틱 토큰이 가리키는 곳만 바꿨을 뿐인데 --btn-bg 도 그것을 참조하는 모든 컴포넌트도 함께 보라가 됩니다. 원시 팔레트와 버튼 규칙은 한 글자도 손대지 않았습니다.

35.5 나쁜 이름 vs 좋은 이름

토큰의 힘은 이름에서 나옵니다. 생김새로 지은 이름은 값이 바뀌는 순간 거짓말이 됩니다.


/* 나쁜 예: 생김새를 이름에 박음 */
--blue: #2563eb;
--big-text: 20px;
.title { color: var(--blue); }


강조색이 초록으로 바뀌면 --blue 라는 이름에 초록이 담기는 우스운 꼴이 됩니다. 역할로 이름을 지으면 값이 바뀌어도 이름은 살아남습니다.


/* 좋은 예: 역할을 이름에 담음 */
--color-accent: #2563eb;
--font-size-heading: 20px;
.title { color: var(--color-accent); }


이제 강조색이 무슨 색이 되든 --color-accent 라는 이름은 그대로입니다. 화면은 색이 아니라 역할을 부르고 있어서 흔들리지 않습니다.

35.6 토큰 네이밍 규칙

이름은 큰 갈래에서 작은 갈래로 좁혀 가며 짓습니다. 무엇의 무슨 역할, 어떤 상태인지를 순서대로 이어 붙이면 규칙이 눈에 보입니다.


:root {
--color-bg-default: var(--gray-100);
--color-bg-hover: #e5e7eb;
--color-border-strong: var(--gray-900);
--color-border-subtle: #e5e7eb;
--space-inline-sm: 8px;
--space-inline-md: 12px;
}


범주와 역할과 상태가 이름 안에 순서대로 들어 있으니, 처음 보는 토큰도 뜻을 짐작할 수 있습니다. 새 토큰을 만들 때도 이름을 두고 헤매지 않습니다.

35.7 calc로 토큰 파생시키기

모든 토큰을 손으로 적을 필요는 없습니다. 기준값 하나를 두고 calc 로 나머지를 계산하면, 스케일 전체가 한 값에 묶입니다.


:root {
--space-unit: 4px;
--space-2: calc(var(--space-unit) * 2);
--space-4: calc(var(--space-unit) * 4);
--space-6: calc(var(--space-unit) * 6);
}
.stack {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-6);
}


기준을 4px 에서 8px 로 바꾸면 파생된 간격 전체가 비율을 지킨 채 함께 커집니다. 배수 규칙이 코드 안에 박혀 있어 어긋난 값이 끼어들 틈이 없습니다.

35.8 체인 끝에서 테마 갈아 끼우기

세 계층 구조의 진가는 테마를 바꿀 때 나옵니다. 시맨틱 층만 다시 가리키게 하면 컴포넌트 코드를 건드리지 않고 어두운 화면을 얻습니다.


:root {
--color-surface: var(--gray-100);
--color-text: var(--gray-900);
}
:root[data-theme="dark"] {
--color-surface: #1f2937;
--color-text: #f9fafb;
--color-primary: var(--blue-400);
}
.card {
background: var(--color-surface);
color: var(--color-text);
}


테마 속성 하나가 바뀌면 시맨틱 토큰이 다른 원시 값을 가리키고, 그 위의 컴포넌트가 전부 따라옵니다. 원시 팔레트도 .card 규칙도 그대로입니다. 층을 나눠 둔 덕분에 옷만 갈아입힐 수 있습니다.

35.9 다른 컴포넌트도 같은 밑층을 공유한다

컴포넌트 토큰의 이점은 여러 컴포넌트가 같은 시맨틱 층을 나눠 쓸 때 또렷해집니다. 입력 칸도 버튼과 같은 밑층을 바라봅니다.


:root {
--field-border: var(--color-border-subtle);
--field-border-focus: var(--color-primary);
--field-radius: var(--btn-radius);
}
.field {
border: 1px solid var(--field-border);
border-radius: var(--field-radius);
padding: 8px 12px;
}
.field:focus {
outline: none;
border-color: var(--field-border-focus);
}


입력 칸의 포커스 테두리와 버튼의 배경이 같은 --color-primary 를 바라봅니다. 브랜드 색을 바꾸면 버튼과 입력 칸이 한 몸처럼 함께 움직입니다. 컴포넌트는 달라도 뿌리는 하나입니다.

35.10 정리

디자인 토큰은 원시 토큰과 시맨틱 토큰과 컴포넌트 토큰의 세 계층으로 쌓입니다. 원시 층은 물감을 늘어놓고, 시맨틱 층은 역할을 입히고, 컴포넌트 층은 그 역할을 국소적으로 소비합니다. 이름은 생김새가 아니라 역할로 짓고, 파생은 calc 에 맡기고, 참조 체인은 위를 향하게 둡니다. 그러면 값 하나를 어느 층에서 바꾸느냐로 파급 범위를 손끝에서 조절할 수 있습니다.