콘텐츠로 이동

토큰 및 변수 전략

반복되는 값을 임의로 작성하지 않고, 토큰과 변수를 통해 일관되게 관리하는 것을 원칙으로 합니다. 이 전략은 스타일 변경을 구조 변경으로 번지지 않게 하기 위한 기준입니다.


토큰과 변수는 다음 역할을 담당합니다.

  • 디자인 값의 단일 출처 역할을 합니다.
  • 프로젝트 전반의 일관성을 유지합니다.
  • 수정 범위를 최소화합니다.
  • 스타일 변경의 영향을 예측 가능하게 합니다.

토큰은 구조를 대체하지 않으며, 구조 위에서만 작동합니다.


토큰은 다음 원칙을 따릅니다.

  • 반복되는 값은 반드시 토큰으로 관리합니다.
  • 임의의 숫자나 색상 값 사용을 지양합니다.
  • 의미 없는 값 나열을 허용하지 않습니다.
  • 토큰은 역할과 목적이 드러나야 합니다.

토큰은 값 자체가 아니라 의미를 담은 단위입니다.

의미 기반 토큰은 :root에 선언하며, 이름에 역할과 목적이 드러나야 합니다. rem 값에는 px 환산 주석을 함께 답니다.

:root { --color_primary: #2563eb; --color_text: #1a1a1a; --color_border: #e5e7eb; }
:root { --space_sm: 0.5rem; /* 8 */ --space_md: 1rem; /* 16 */ --space_lg: 1.5rem; /* 24 */ }
:root { --radius_md: 8px; --radius_lg: 16px; }

토큰은 실제 사용처가 생길 때만 추가합니다.

  • 완비된 스케일(모든 단계를 미리 채운 토큰 세트)을 만들지 않습니다.
  • 사용되지 않는 토큰은 삭제 대상입니다.
  • 반복되는 임의값은 가장 가까운 스케일 값으로 스냅해 토큰으로 흡수합니다. (예: 18px → var(--space_md))
  • 임의값이 반복되고 있다면 토큰 부재의 신호로 판단합니다.

2계층 토큰(색상의 원료 → 역할)에서는 “사용”의 정의가 계층마다 다릅니다.

  • 원료(프리미티브)의 사용 = 역할 레이어가 참조하는 것. 라이트·다크 두 테마가 같은 사다리를 반대편에서 소비하므로, 컴포넌트 코드에 보이지 않아도 사용 중일 수 있습니다.
  • 역할(시맨틱)의 사용 = 컴포넌트가 참조하는 것.

어느 계층이든 참조가 0회인 토큰은 삭제 대상입니다.

예외는 z-index입니다. z-index는 개별 값이 아니라 레이어 간 순서 계약이므로, 아직 사용되지 않는 중간 레이어도 유지합니다.


토큰의 가치는 이름과 값 사이의 간격에서 나옵니다.

  • --color_text는 이름(역할)과 값이 분리되어 있어 테마에 따라 값을 바꿀 수 있습니다.
  • --space_8은 이름이 곧 값입니다. 값을 바꾸는 순간 이름이 거짓말을 합니다.

그래서 이름이 곧 값인 토큰(눈금 토큰)은 값을 절대 바꾸지 않습니다.

눈금 토큰은 치환 장치가 아니라 자의 눈금입니다.

  • 고를 수 있는 값의 선택지를 좁히고 (임의값 유입 차단)
  • 단위 정책(rem)을 표기 자체로 집행합니다.

“값을 바꾸고 싶다”는 요구는 둘 중 하나이며, 어느 쪽도 눈금을 옮기지 않습니다.

특정 자리의 간격을 키우고 싶은 경우 값 변경이 아니라 선택 변경입니다. 해당 사용처만 다른 눈금으로 재배정합니다.

/* 이 자리가 답답하다면 — 눈금이 아니라 선택을 바꾼다 */
.card .i_foot { gap: var(--space_8); } /* 이전 */
.card .i_foot { gap: var(--space_12); } /* 이후 */

시스템 전역의 간격을 함께 조정하고 싶은 경우 눈금 위에 역할(시맨틱) 별칭 레이어를 얹습니다. 색상의 원료 → 역할 2계층과 같은 구조입니다.

/* 역할 토큰만 재배선하면 전역에 반영된다 */
:root { --control_pad_x: var(--space_16); }
[data-density="compact"] { --control_pad_x: var(--space_12); }

단, 역할 레이어는 전역 연동 수요가 실증될 때 추가합니다. 미리 만들면 미사용 토큰이 됩니다.

간격에 생값(8px, 0.5rem)을 직접 쓰지 않습니다. 유일한 예외는 단위 기준에서 정한 레이아웃 구조 간격(px 직접 사용 영역)입니다.


단위는 취향이 아니라 다음 질문으로 판정합니다.

사용자가 브라우저 글자 크기를 키웠을 때, 이 값이 함께 커져야 정보 전달이 유지되는가?

rem을 사용하는 값 — 글자 선호에 반응해야 하는 값

  • 글자 크기
  • 텍스트를 담는 컨트롤의 높이와 패딩
  • 텍스트 주변 간격 (space 토큰)

글자만 커지고 그릇이 커지지 않으면 텍스트가 잘리고, 정보 전달이 실패합니다.

px를 사용하는 값 — 글자 선호와 무관한 값

  • 라디우스, 1px 헤어라인 보더, 그림자
  • 브레이크포인트
  • 레이아웃 구조 간격 (그리드 갭, 섹션 여백)
  • 비주얼 타이포그래피 (히어로, 랜딩)
  • 애니메이션 측정값

이 값들까지 rem이면 글자 크기만 키웠을 때 레이아웃 전체가 거대해지는 다른 종류의 파손이 생깁니다.

em을 사용하는 값 — 부모 텍스트에 비례해야 하는 값

  • 텍스트와 나란히 의미를 전달하는 아이콘 크기
.btn svg { width: 1.125em; height: 1.125em; }

아이콘이 em이면 글자 확대와 크기 변형(m_small/m_large)에 자동으로 연동됩니다. SVG의 width/height 속성으로 크기를 고정하지 않습니다.

전면 rem도, 전면 px도 아닙니다. 기준 없는 혼용이 문제이며, 위 질문이 그 기준입니다.

루트 폰트 크기는 100%(16px)를 유지합니다. 62.5% 같은 환산 트릭은 브라우저 최소 폰트 크기 설정에서 배율이 붕괴하고, 미디어쿼리의 rem과 이중 기준이 생기므로 사용하지 않습니다. 환산은 px 주석으로 해결합니다.


CSS 변수는 다음 기준에 따라 사용합니다.

  • 색상, 여백, 크기, 레이아웃 값에 사용합니다.
  • 구조를 표현하는 용도로 사용하지 않습니다.
  • 상태 표현을 직접 담당하지 않습니다.

변수는 스타일 구현 수단이며, 구조 설계 도구가 아닙니다.

컴포넌트에서는 값을 직접 쓰지 않고 var()로 토큰을 참조합니다.

/* ❌ 나쁜 예 — 하드코딩된 값이 흩어져 유지보수가 어려움 */
.card { padding: 16px; border: 1px solid #e5e7eb; border-radius: 8px; }
.card .i_title { color: #1a1a1a; }
/* ✅ 좋은 예 — 의미 기반 토큰을 var()로 참조 */
.card { padding: var(--space_md); border: 1px solid var(--color_border); border-radius: var(--radius_md); }
.card .i_title { color: var(--color_text); }

구조와 토큰의 역할은 명확히 분리합니다.

  • 구조는 클래스와 네이밍으로 정의합니다.
  • 값은 토큰과 변수로 관리합니다.
  • 구조 변경 없이 값만 교체할 수 있어야 합니다.

토큰이 구조를 침범하기 시작하면 설계가 잘못되었다고 판단합니다.


토큰 소스는 카테고리별 파일로 나눠 관리합니다. 이것은 저작 구조 — 사람이 편집하기 위한 조직입니다.

단, @import 구문이 브라우저에 그대로 도달하게 하지 않습니다. 브라우저는 진입 파일을 파싱한 뒤에야 임포트 대상을 발견하므로 순차 폭포(waterfall)가 생겨 렌더링을 지연시킵니다.

  • 빌드(번들)를 거치는 배포 — 진입점과 @import를 사용해도 됩니다. 빌드 타임에 한 파일로 합쳐져 런타임 비용이 없습니다.
  • 빌드 없이 서빙@import 대신 개별 <link>를 나열합니다. HTML 파싱 시점에 전부 발견되어 병렬로 로드됩니다.

웹폰트는 시스템 CSS가 강제 로드하지 않습니다. 소비자 페이지가 <link>로 직접 넣습니다.


토큰 및 변수 전략은 UI 스타일을 통제하기 위한 규칙이 아니라, UI 구조를 안정적으로 유지하기 위한 보호 장치입니다.

토큰이 정리될수록 UI 구조는 더 단순하고 유지보수하기 쉬워집니다.