# tokens — 디자인 토큰과 성능 예산 3단계에서 읽는다. **코드를 쓰기 전에 숫자를 정한다.** 구현하면서 색을 고르면 매번 다른 색이 나온다. 토큰은 취향이 아니라 **계약**이다. 한번 정하면 이후 모든 값은 여기서 나온다. 하드코딩된 값이 하나라도 있으면 수정 요청 한 번에 무너진다. --- ## 1. 타입 스케일 ### 비율을 먼저 고른다 | 비율 | 값 | 인상 | 언제 | |---|---|---|---| | Minor Third | 1.200 | 차분, 조밀 | 정보 밀도가 높은 페이지, 문서, 대시보드 | | Major Third | 1.250 | 안정 | 기본값. 대부분의 마케팅 사이트 | | Perfect Fourth | 1.333 | 또렷한 위계 | 랜딩 페이지, 제품 소개 | | Golden | 1.618 | 극적 | 포트폴리오, 에디토리얼. 중간 단계가 비어 본문이 외로워진다 | **한 페이지에 비율은 하나다.** 헤드라인만 다른 비율을 쓰고 싶으면 그건 비율이 아니라 **디스플레이 사이즈를 따로 정의**하는 것이다. ### 실제 값으로 적는다 ```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 — 디스플레이 */ } ``` ### 반응형은 clamp 로, 미디어쿼리로 하지 마라 ```css --step-4: clamp(2.25rem, 1.5rem + 3.75vw, 3.157rem); ``` `clamp(최소, 기준+vw, 최대)`. 최소값은 **모바일에서 읽히는 크기**, 최대값은 데스크톱 기준. 중간이 매끄럽게 이어져 중간 뷰포트에서 깨지지 않는다. ### 함께 정해야 하는 것 | 토큰 | 규칙 | |---|---| | `--leading-tight` | 1.1~1.2 — 디스플레이/헤드라인 | | `--leading-normal` | 1.5~1.6 — 본문 (라틴) / **1.6~1.8 (한글)** | | `--measure` | 한 줄 길이. 라틴 60~75자, **한글 25~40자** | | 자간 | 큰 글자에만 음수(`-0.02em` 정도). **한글에는 음수 자간 금지** | **폰트는 최대 2종**(+ 코드용 모노 1). 세 번째 텍스트 폰트를 넣고 싶다면 위계를 웨이트로 못 만들고 있다는 신호다. > **어떤 폰트를 어떻게 고르고 싣는지는 `typography.md` 가 정본이다.** 여기서는 스케일 값만 정한다. > 로딩 전략, 폴백 메트릭 보정, OpenType 기능, 한글 서브셋, 라이선스 확인이 그 문서에 있다. --- ## 2. 색 ### 역할로 정의한다. 팔레트로 정의하지 마라 "파랑 5단계"가 아니라 **무엇에 쓰이는 색인지**로 정의한다. ```css :root { --surface: /* 페이지 바탕 */ --surface-raised: /* 카드·패널 (바탕과 구분되되 튀지 않게) */ --ink: /* 본문 텍스트 */ --ink-muted: /* 보조 텍스트 — 대비 4.5:1 유지 */ --line: /* 경계선 */ --accent: /* 강조 — 페이지당 하나 */ --accent-ink: /* 강조 위에 올라가는 글자색 */ } ``` ### 규칙 1. **강조색은 하나다.** 두 개가 필요하다고 느끼면 위계 설계가 실패한 것이다. 예외: 상태색(성공/경고/오류)은 강조색이 아니라 기능색이다 2. **채도가 높은 색은 면적을 좁게.** 넓은 면적에 쓰면 눈이 피로하고 싸구려로 보인다 3. **중성색도 색이다.** 순수 회색(`#808080`) 대신 강조색 쪽으로 약간 기운 중성색을 쓰면 화면 전체가 하나로 묶인다 4. **대비를 측정해라.** 본문 4.5:1, 큰 글자 3:1. 눈으로 판단하지 마라 ### 다크 모드 **다크가 슬롭인 게 아니라 고르지 않은 다크가 슬롭이다.** (`antipatterns.md` §1 — bun.sh 는 다크인데도 슬롭 항목을 거의 전부 회피한다) 다크를 기본으로 하려면 **근거를 한 줄로 대라.** 정당화되는 이유의 목록은 `presets/dark-instrument.md` 에 있다(야간 운영 환경, 밝은 데이터 시각화의 대비, 제품 자체가 어두움). 댈 수 없으면 라이트로 간다. 어느 쪽을 기본으로 하든 **두 테마를 동등하게 정의한다.** 다크만 만들고 라이트를 빼는 것은 사용자 선택권을 뺏는 것이다. 지원할 때는 색을 뒤집는 게 아니라 **역할별로 다시 정의**한다. 다크에서 순수 검정(`#000`)은 대비가 너무 세서 눈이 아프고, 순수 흰색 텍스트도 마찬가지다. ```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 이 h2 보다 한 단계만 크면 위계가 아니다 히어로 제목을 줄이다가 실측에서 이렇게 됐다. | | 크기 | | |---|---|---| | h1 (히어로) | 41.5px | `--step-4` | | h2 (섹션) | 33.2px | `--step-3` | **8px 차이.** 스케일상으로는 한 단계 위지만 화면에서는 같은 크기로 읽힌다. 페이지에서 가장 중요한 문장이 아홉 개의 섹션 제목과 구별되지 않았다. 원인은 과잉 교정이었다. 처음에 `--display`(1440px 에서 112px)를 썼다가 배경 그래픽과 싸워서 줄였는데, 줄이다가 h2 와 붙는 데까지 왔다. **한 극단에서 도망치다 반대쪽 극단에 도착한 것이다.** 기준: **넓은 화면에서 히어로 h1 은 섹션 h2 의 2 배 안팎.** 그 아래면 위계가 없고, 그 위는 배경·여백과 싸우기 시작한다. ### clamp 의 하한은 h2 를 기준으로 정한다 여기서 한 번 더 틀렸다. 데스크톱만 보고 고쳤더니 이렇게 됐다. ```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 는 고정이라, 좁아질수록 둘이 만난다.** 데스크톱에서 고친 위계가 모바일에서 그대로 사라진다. 하한을 h2 의 1.4 배 이상으로 잡으면 전 구간이 유지된다. ```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 배를 고집할 필요는 없다 — 폭이 좁으면 큰 글자가 줄만 늘린다. **1.4 배가 하한, 2 배 안팎이 상한**이고 그 사이를 `vw` 가 잇는다. > 확인법: 한 화면 폭에서만 재지 마라. `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. **제목을 `