# tokens — 디자인 토큰과 성능 예산 3단계에서 읽는다. **코드를 쓰기 전에 숫자를 정한다.** 구현하면서 색을 고르면 매번 다른 색이 나온다. 토큰은 취향이 아니라 **계약**이다. 한번 정하면 이후 모든 값은 여기서 나온다. 하드코딩된 값이 하나라도 있으면 수정 요청 한 번에 무너진다. --- ## 1. 타입 스케일 ### 비율을 먼저 고른다 | 비율 | 값 | 인상 | 언제 | |---|---|---|---| | Minor Third | 1.200 | 차분, 조밀 | 정보 밀도가 높은 페이지, 문서, 대시보드 | | Major Third | 1.250 | 안정 | 기본값. 대부분의 마케팅 사이트 | | Perfect Fourth | 1.333 | 또렷한 위계 | 랜딩 페이지, 제품 소개 | | Golden | 1.618 | 극적 | 포트폴리오, 에디토리얼. 중간 단계가 비어 본문이 외로워진다 | 본문 scale은 하나의 비율에서 시작하면 관리하기 쉽다. display가 다른 비율이나 별도 크기를 요구할 수 있으므로, 토큰 이름·역할·대표 폭에서의 위계 검증을 함께 기록한다. ### 실제 값으로 적는다 ```css :root { /* Perfect Fourth (1.333), 본문 16px 기준 */ --step--1: 0.75rem; /* 12 — 캡션, 레이블 */ --step-0: 1rem; /* 16 — 본문 */ --step-1: 1.333rem; /* 21 — 리드 문단, 소제목 */ --step-2: 1.777rem; /* 28 — h3 */ --step-3: 2.369rem; /* 38 — h2 */ --step-4: 3.157rem; /* 51 — h1 */ --step-5: 4.209rem; /* 67 — 디스플레이 */ } ``` ### 유동 크기와 구조 전환을 구분한다 ```css --step-4: clamp(2.25rem, 1.5rem + 3.75vw, 3.157rem); ``` `clamp(최소, 기준+vw, 최대)`는 연속적으로 변해야 하는 값에 유용하다. 최소·최대는 대표 콘텐츠로 확인한다. grid 열, 내비, 작업대처럼 구조가 바뀌는 지점에는 미디어쿼리나 container query를 쓰고, 임의 기기 폭이 아니라 실제 실패 지점에서 정한다. ### 함께 정해야 하는 것 | 토큰 | 규칙 | |---|---| | `--leading-tight` | 1.1~1.2부터 검토 — 디스플레이/헤드라인 | | `--leading-normal` | 1.5~1.6(라틴), **1.6~1.8(한글)**부터 검토 — 본문 | | `--measure` | 한 줄 길이의 시작 범위. 라틴 60~75자, **한글 25~40자** | | 자간 | 큰 라틴 디스플레이에서만 작은 음수 자간을 후보로 두고 실제 렌더로 확인. 한글은 글꼴·크기·문맥별로 시험하며, 음수값을 기본 처방이나 절대 금지로 두지 않는다 | 행간과 measure 범위는 시작값이다. 한글 서체·문자·폭·실제 문단을 렌더하고 확대 상태까지 확인해 정한다. 폰트 수는 전송량·언어 범위·위계·라이선스를 함께 보고 예산으로 정한다. 두 텍스트 패밀리와 필요 시 모노는 흔한 시작점이지만, 세 번째 패밀리가 무조건 실패라는 뜻은 아니다. > **어떤 폰트를 어떻게 고르고 싣는지는 `typography.md` 가 정본이다.** 여기서는 스케일 값만 정한다. > 로딩 전략, 폴백 메트릭 보정, OpenType 기능, 한글 서브셋, 라이선스 확인이 그 문서에 있다. --- ## 2. 색 ### 역할로 정의한다. 팔레트로 정의하지 마라 "파랑 5단계"가 아니라 **무엇에 쓰이는 색인지**로 정의한다. ```css :root { --surface: /* 페이지 바탕 */ --surface-raised: /* 카드·패널 (바탕과 구분되되 튀지 않게) */ --ink: /* 본문 텍스트 */ --ink-muted: /* 보조 텍스트 — 대비 4.5:1 유지 */ --line: /* 경계선 */ --accent: /* 강조 역할이 필요할 때의 기본 토큰 */ --accent-ink: /* 강조 위에 올라가는 글자색 */ } ``` ### 규칙 1. 강조 역할은 한 색에서 시작해 경쟁 여부를 검증한다. 여러 강조 역할이 필요할 수 있으며, 그때는 행동·상태·정보 목적을 역할 토큰으로 구분한다. 수만으로 위계 실패를 판정하지 않는다 2. 색 맥락 관찰은 [visual-design-lineage.md](./visual-design-lineage.md)를 참고한다. 이 스킬에서는 브랜드 강도·정보 위계·접근성의 근거로 색을 고르고, 실제 상태·면적·인접색에서 텍스트 대비와 판독성을 별도로 검증한다 3. **중성색도 색이다.** 순수 회색과 색조를 띤 중성색을 모두 후보로 두고, 표면·브랜드·상태색과의 관계를 비교한다 4. **대비를 측정해라.** 본문 4.5:1, 큰 글자 3:1. 눈으로 판단하지 마라 ### 다크 모드 **다크가 슬롭인 게 아니라 고르지 않은 다크가 슬롭이다.** (`antipatterns.md` §1 — bun.sh 는 다크인데도 슬롭 항목을 거의 전부 회피한다) 다크를 기본으로 하려면 **근거를 한 줄로 대라.** 정당화되는 이유의 목록은 `presets/dark-instrument.md` 에 있다(야간 운영 환경, 밝은 데이터 시각화의 대비, 제품 자체가 어두움). 댈 수 없으면 라이트로 간다. 라이트/다크 모두를 지원하기로 한 제품은 역할 토큰을 각각 정의하고 실제 상태를 검증한다. 제품이 한 테마만 제공할 수는 있지만, 사용자 환경·브리프·운영 맥락과 접근성 영향을 명시한다. 지원할 때는 색을 뒤집는 게 아니라 **역할별로 다시 정의**한다. 순수 검정·흰색과 근접한 색은 제품·디스플레이·주변광에 따라 읽기 경험이 다르므로, 대비·눈부심·브랜드 표면을 실제 기기에서 확인한다. ```css :root { --surface: #fbfaf8; --ink: #1a1917; } @media (prefers-color-scheme: dark) { :root { --surface: #14130f; --ink: #e8e4dc; } } ``` --- ## 3. 간격 ### 하나의 리듬에서 파생시킨다 ```css :root { --space-1: 0.25rem; /* 4 */ --space-2: 0.5rem; /* 8 */ --space-3: 1rem; /* 16 */ --space-4: 1.5rem; /* 24 */ --space-5: 2.5rem; /* 40 */ --space-6: 4rem; /* 64 */ --space-7: 6rem; /* 96 */ --space-8: 10rem; /* 160 — 섹션 간격 */ } ``` **중간 값을 즉석에서 만들지 마라.** `--space-4` 와 `--space-5` 사이가 필요하다면 스케일이 잘못된 것이다. ### 여백이 위계를 만든다 - 관련 있는 것끼리는 **가깝게**, 다른 그룹과는 **확실히 멀게**. 애매한 중간 간격이 가장 나쁘다 - 섹션·그룹·요소 간 간격 차이가 관계를 읽히게 하는지 실제 화면에서 본다. 4배 같은 고정 비율은 시작 가설일 뿐, 콘텐츠와 폭에 따라 조절한다. - 요소를 정렬할 때 **간격이 아니라 정렬선**을 먼저 맞춰라 ### 제목 역할이 실제로 구분되는지 확인한다 히어로 제목을 줄이다가 실측에서 이렇게 됐다. | | 크기 | | |---|---|---| | h1 (히어로) | 41.5px | `--step-4` | | h2 (섹션) | 33.2px | `--step-3` | **8px 차이.** 스케일상으로는 한 단계 위지만 화면에서는 같은 크기로 읽힌다. 페이지에서 가장 중요한 문장이 아홉 개의 섹션 제목과 구별되지 않았다. 원인은 과잉 교정이었다. 처음에 `--display`(1440px 에서 112px)를 썼다가 배경 그래픽과 싸워서 줄였는데, 줄이다가 h2 와 붙는 데까지 왔다. **한 극단에서 도망치다 반대쪽 극단에 도착한 것이다.** 넓은 화면에서 히어로와 섹션 제목이 같은 위계로 읽히지 않는지 비교한다. 2배 안팎은 한 프로젝트의 관찰값일 뿐 기준선이 아니다. 문구 길이, 구도, 여백, 브랜드 타입에 따라 같은 크기 차도 다르게 보인다. ### clamp 값을 역할 관계와 함께 확인한다 여기서 한 번 더 틀렸다. 데스크톱만 보고 고쳤더니 이렇게 됐다. ```css .hero-title { font-size: clamp(2.2rem, 1.3rem + 3.1vw, 4.4rem); } ``` | 화면 | h1 | h2 | 비율 | |---|---|---|---| | 1440px | 65.4px | 33.2px | 1.97 배 | | 1024px | 52.5px | 33.2px | 1.58 배 | | 390px | **35.2px** | 33.2px | **1.06 배** | **h1 만 `vw` 에 비례해 줄고 h2 는 고정이라, 좁아질수록 둘이 만난다.** 데스크톱에서 고친 위계가 모바일에서 그대로 사라진다. 모바일에서 h1과 h2가 거의 같은 크기로 수렴하지 않는지 확인한다. 아래 값은 해당 조합의 예시이며 모든 제목에 적용하는 비율 규칙이 아니다. ```css /* 하한 3rem = 48px = h2(33.2px)의 1.45 배 */ .hero-title { font-size: clamp(3rem, 1.9rem + 3.1vw, 4.4rem); } ``` | 화면 | 320 | 390 | 768 | 1024 | 1440 | |---|---|---|---|---|---| | 비율 | 1.45 | 1.45 | 1.63 | 1.87 | 2.12 | 좁은 화면에서 2 배를 고집할 필요는 없다 — 폭이 좁으면 큰 글자가 줄만 늘린다. 최소·중간·최대 폭에서 제목 역할이 구분되는지 확인하고, 필요한 경우 h1과 h2를 함께 유동화하거나 문구·레이아웃을 조정한다. > 확인법: 한 화면 폭에서만 재지 마라. `clamp` 를 쓴 값은 **최소 폭·중간 폭·최대 폭 > 세 곳에서 계산해 보고**, 같이 변하지 않는 값(고정 스케일의 h2 등)과의 비율을 봐라. > 그리고 눈으로 비교하지 말고 크기를 재라 — 배경이 화려하면 큰 글자도 작아 보인다. ### 제목 다음 간격은 전역에서 한 번 정한다 `h2 { margin: 0 }` 리셋과 `p { margin: 0 0 X }` 를 같이 쓰면 **제목과 본문이 붙는다.** 위쪽 여백이 양쪽 다 0 이기 때문이다. 눈에 띄게 깨져 보이지만, 섹션을 하나씩 만들다 보면 어떤 섹션에는 `.mat-lead { margin-block: var(--space-4) ... }` 처럼 손으로 붙게 되고 어떤 섹션에는 안 붙는다. 실측에서 한 페이지 안에 0px 인 섹션 둘과 24px 인 섹션 셋이 공존했다. **간격이 컴포넌트마다 다르면 그건 SSOT 가 아니다.** ```css p { margin: 0 0 var(--space-3); } /* 제목 다음에 오는 것은 반드시 떨어진다 — 여기 한 곳에서만 정한다 */ h1 + *, h2 + *, h3 + *, .head + * { margin-top: var(--space-4); } ``` 세 가지 함정이 있다. 1. **`:where()` 로 감싸면 특이도가 0 이 되어 위의 `p` 규칙에 진다.** `:where(h2) + :where(.lead)` 는 0-0-0, `p` 는 0-0-1 이다. 아무 효과 없이 조용히 무시된다. 특이도를 갖추고, 캐스케이드에서 `p` **뒤에** 둬라. 2. **제목을 `