색 — 4장
--primary, --background, --destructive …
짝(배경/글자)으로 관리하는 것이 규칙이다.
테마는 색 설정이 아니다. 앱의 시각적 결정 전체를 담는 목록이다.
리소스Resource의미 토큰Semantic Token--zinc-900(무슨 색인가)이 아니라 --primary(무슨 역할인가).원시 토큰Primitive Token--zinc-900: #18181b. 팔레트에 해당한다.하드코딩Hardcodingbg-[#18181b] 처럼. 나중에 바꾸려면 전부 찾아야 한다.프로젝트 6개월 차, 디자이너가 말한다. “브랜드 색을 남색에서 초록으로 바꿉시다.”
<button className="bg-blue-600 text-white">저장</button><div className="border-blue-600">…</div><a className="text-blue-600">더 보기</a><span className="bg-blue-50 text-blue-900">진행 중</span>blue-600이 몇 군데 있는지 아무도 모른다. 전체 검색하면 214군데가 나온다blue-50, blue-900처럼 같은 결정에서 파생된 것들도 따로 찾아야 한다dark:bg-blue-400도 따로 있었기 때문작업 견적: 2~3일 + 회귀 테스트.
<button className="bg-primary text-primary-foreground">저장</button><div className="border-primary">…</div><a className="text-primary">더 보기</a><span className="bg-accent text-accent-foreground">진행 중</span>/* app/globals.css — 여기 한 곳 */:root { --primary: oklch(0.55 0.15 155); }.dark { --primary: oklch(0.72 0.14 155); }.dark 값만 같이 바꾼다작업 견적: 10분.
아래 데모에서 버튼을 눌러보자. 카드·버튼·탭·진행바가 전부 바뀐다.
하지만 HTML은 한 글자도 바뀌지 않는다. <button class="ui-btn ui-btn--default">저장</button> 그대로다.
Material Design과 사내 SI 스타일이 이렇게 다른데도 마크업이 같다는 게 요점이다. 바뀐 것은 이름에 매달린 값뿐이다.
“테마 = 색”이라고 생각하면 절반만 관리하게 된다. 실제로는 다섯 종류다.
색 — 4장
--primary, --background, --destructive …
짝(배경/글자)으로 관리하는 것이 규칙이다.
형태 — 5장
--radius 하나에서 전체 둥글기가 파생된다.
간격·테두리·그림자도 여기 속한다.
글자 — 6장
폰트 패밀리, 크기 스케일, 줄 간격. 한글은 영문과 규칙이 다르다.
컴포넌트 — 7장
button.tsx 안의 “outline이란 이런 것” 같은 결정.
코드지만 명백히 테마의 일부다.
아이콘 · 폰트 파일 — 8장
아이콘 세트 선택과 굵기, 폰트 파일을 어떻게 내려받게 할 것인가.
그리고 두 벌 — 9장
위 다섯 개를 라이트/다크 두 벌로 갖는 것. 이름은 그대로, 값만 다르다.
토큰에는 두 층이 있다. 이 구분을 놓치면 이름을 붙여도 소용이 없다.
| 원시 토큰 | 의미 토큰 | |
|---|---|---|
| 예 | --zinc-900, --blue-500 | --primary, --muted, --destructive |
| 답하는 질문 | 무슨 색인가 | 무슨 역할인가 |
| 개수 | 많다 (팔레트 전체) | 적다 (20개 안팎) |
| 테마마다 | 대체로 같다 | 바뀐다 |
| 컴포넌트가 쓰는 것 | ❌ | ✅ |
자료를 찾다 보면 원시 → 의미 → 컴포넌트 3층 구조를 보게 된다.
--button-bg: var(--primary) 같은 층을 하나 더 두는 것이다.
대부분의 프로젝트는 2층이면 충분하다. 3층은 컴포넌트가 수백 개고 팀이 여러 개로 나뉘어 서로 다른 속도로 움직일 때 의미가 생긴다. 처음부터 만들면 관리할 이름만 3배가 된다.
아래 넷은 완전히 같은 마크업이다. 값만 다르다.
여기서 읽어야 할 것은 “예쁘다”가 아니라 —
“테마를 만든다”는 말을 실무 동작으로 풀면 이렇다.
| 동작 | 무슨 뜻인가 | 어디서 |
|---|---|---|
| 정의한다 | 리소스마다 이름과 값을 정한다 | globals.css의 :root |
| 두 벌로 만든다 | 같은 이름에 다크용 값을 하나 더 준다 | globals.css의 .dark |
| 연결한다 | Tailwind가 bg-primary 클래스를 만들게 한다 | @theme inline 블록 |
| 강제한다 | 컴포넌트가 이름만 쓰도록 규칙을 세운다 | 코드 리뷰 · 린트 |
| 배포한다 | 다른 앱·팀이 같은 테마를 받게 한다 | 레지스트리 (11장) |
세 번째 “연결한다”가 자주 빠진다. CSS 변수를 정의하는 것만으로는
bg-primary 유틸리티 클래스가 생기지 않는다 — 4장에서 자세히 본다.