# 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배 이상**. 좁으면 페이지가 뭉개진다
- 요소를 정렬할 때 **간격이 아니라 정렬선**을 먼저 맞춰라

---

## 3-b. 형태 (radius·선)

하드 게이트 #3 이 검사하는 대상이다. **정의하지 않으면 검사할 수 없다.**

```css
:root {
  --radius-sm: 2px;   /* 입력, 배지 */
  --radius-md: 8px;   /* 카드, 패널 */
  --radius-pill: 999px;
  --line-width: 1px;
}
```

**서로 다른 radius 값은 3종 미만으로.** 어휘가 많을수록 우연히 결정된 것으로 보인다.
1종만 쓰는 것도 정당한 선택이다 — `swiss-minimal`·`anti-grid` 에서는 오히려 그쪽이 맞다.

---

## 4. 모션 토큰

```css
:root {
  --dur-instant: 100ms;  /* 상태 변화 — 호버, 포커스 */
  --dur-quick:   200ms;  /* 작은 요소 등장/퇴장 */
  --dur-normal:  350ms;  /* 패널, 모달 */
  --dur-slow:    600ms;  /* 페이지 전환, 큰 이동 */

  --ease-out:  cubic-bezier(0.22, 1, 0.36, 1);   /* 들어오는 것 — 기본값 */
  --ease-in:   cubic-bezier(0.64, 0, 0.78, 0);   /* 나가는 것 */
  --ease-soft: cubic-bezier(0.4, 0, 0.2, 1);     /* 위치 이동 */
}
```

상세는 `references/motion.md`. 여기서는 **값을 고정**하는 것이 목적이다. 컴포넌트마다 다른 duration 을 쓰면 페이지가 불안해 보인다.

---

## 5. 성능 예산

**여기서 정한다. 구현 후에 재면 이미 늦었다.**

**이 표가 성능 예산의 유일한 원본이다.** SKILL.md 도 `preflight.md` 도 여기를 가리킨다. 다른 문서에 예산 표를 만들면 값이 갈라지고, 갈라진 순간 아무도 어느 쪽이 맞는지 모른다.

| 항목 | 기본 | 데모/포트폴리오 | 5단계에서 |
|---|---|---|---|
| 히어로까지 JS (gzip) | 150KB | 400KB | 측정 |
| WebGL/3D 추가분 | +200KB | +600KB | 측정 |
| 총 전송량 (첫 화면) | 1MB | 2MB | 측정 |
| 첫 인터랙션 (모바일 4G) | 3초 | 5초 | 측정 |
| LCP | 2.5초 | 3.5초 | 측정 |
| CLS | 0.1 | 0.1 | 측정 |
| 폰트 **패밀리** | 2개 이하 | 3개 | 개수 확인 |
| 폰트 전송량(첫 화면) | 100KB | 200KB | 측정 |
| 애니메이션 속성 | transform/opacity 만 | 동일 (타협 없음) | grep |

예산을 올렸다면 **올렸다는 사실과 이유를 명시**해라. 조용히 넘기는 것이 가장 나쁘다.

> **폰트는 파일 개수가 아니라 패밀리 수와 전송량으로 센다.** 한글 웹폰트를 유니코드 범위별로
> 수십 개 파일로 쪼개는 것은 **올바른 최적화**다(Pretendard `dynamic-subset` 은 14개 이상).
> 브라우저는 페이지에 실제로 쓰인 글자 범위만 받는다. 파일 개수를 줄이라고 요구하면
> 한글 프로젝트를 단일 대용량 파일이라는 잘못된 방향으로 몬다.

### LCP·CLS 를 어떻게 재나

`preflight.md` 가 이 값을 요구한다. 재는 법이 없으면 "미측정"으로 남고, 미측정은 통과가 아니다.

```html
<!-- 페이지에 임시로 넣고 콘솔을 본다. 측정 후 반드시 제거한다 -->
<script>
new PerformanceObserver((l) => {
  const e = l.getEntries().at(-1);
  console.log('LCP', Math.round(e.startTime), e.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });

let cls = 0;
new PerformanceObserver((l) => {
  for (const e of l.getEntries()) if (!e.hadRecentInput) cls += e.value;
  console.log('CLS', cls.toFixed(3));
}).observe({ type: 'layout-shift', buffered: true });
</script>
```

**프로덕션 빌드에 돌려라.** 개발 서버는 번들이 다르고 HMR 스크립트가 섞여 값이 의미 없다.
Lighthouse 를 쓸 수 있으면 그쪽이 더 정확하다 — 단 모바일 프로파일로.

### 예산을 지키는 기본 수단

- 폰트: 상세는 `typography.md` §3~§4. 요약하면 `font-display` 를 의식적으로 고르고, preload 는 첫 화면에 실제로 쓰는 파일만, 폴백 메트릭을 보정하거나 `optional` 을 쓴다
- 이미지: 실제 표시 크기의 2배까지만, AVIF/WebP, 첫 화면 밖은 `loading="lazy"`
- JS: 첫 화면에 필요 없는 것은 전부 지연 로드. 3D는 뷰포트 진입 시 동적 import
- **측정하지 않은 최적화는 하지 마라.** 대신 예산을 넘겼는지는 반드시 측정해라

---

## 6. 산출물 형식

3단계를 마치면 아래가 실제 값으로 채워져 있어야 한다. 이것이 4단계의 입력이다.

```css
:root {
  /* 타입 — 비율 ____ , 디스플레이는 스케일 밖 별도 정의 여부 ____ */
  --step--1 ~ --step-5, --leading-*, --measure
  /* 색 — 역할별. 다크 테마도 함께 */
  --surface, --surface-raised, --ink, --ink-muted, --line, --accent, --accent-ink
  /* 간격 */
  --space-1 ~ --space-8
  /* 형태 — 하드 게이트 #3 이 이걸 검사한다 */
  --radius-*, --line-width
  /* 모션 */
  --dur-*, --ease-*
}
```

그리고 한 줄로: **성능 예산 = JS ___KB / 첫 인터랙션 ___초 / 폰트 패밀리 ___개**

**정의하지 않은 토큰은 5단계에서 검사할 수 없다.** 쓰지 않을 항목은 "쓰지 않음"이라고 적어라 — 빈칸과 "없음"은 다르다.

> 근거: research/references/03-trends-2026.md, 04-ai-slop-signatures.md (조사일 2026-08-20)
