designpaca/packages/skill/references/tokens.md
Yun Chan 6805fb2be7 feat(skill): absorb external design skills, restore interview gate, add review route
- Restore the step-0 interview as a mechanical gate the skill explicitly
  depends on; add harness.md (per-harness question tools, limits,
  fallbacks) and brief-interview.md (slots, question cards, rounds).
- Add 10 reference docs absorbed from external design skills
  (accessibility, interaction-feel, elevation, color, icons, product-copy,
  component-systems, critique, change-review, print-email) and extend
  existing references.
- Add a review-only route and two hard-gate clauses (truncated content
  reachability, three-flashes limit).
- design-gate: split tap targets into WCAG 2.5.8 and 44px contract layers,
  run axe-core when available, and fix false positives found on a real
  site (decorative alt="", stacked wordmark line count, url-only pages).
- lint-skill: fail if the interview gate section or its links disappear.
- Ship agents/openai.yaml and THIRD_PARTY_NOTICES.md.
2026-09-24 13:26:03 +09:00

436 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 범위는 시작값이다. 한글 서체·문자·폭·실제 문단을 렌더하고 확대 상태까지 확인해 정한다. 폰트 수는 전송량·언어 범위·위계·라이선스를 함께 보고 예산으로 정한다. 두 텍스트 패밀리와 필요 시 모노는 흔한 시작점이지만, 세 번째 패밀리가 무조건 실패라는 뜻은 아니다.
**관찰 후보 — 라틴 측정폭 60자 하한은 고정선이 아니다.** 사이드바·카드처럼 컬럼 자체가 좁을 때는 45~59자 구간도 시작값으로 허용한다. 다른 스킬은 45~75자 범위를 제시하지만, 이 스킬의 60~75자·한글 25~40자 값을 대체하는 것이 아니라 좁은 컬럼에서만 여는 예외다 [SKILL-DESIGN-REVIEW].
> **어떤 폰트를 어떻게 고르고 싣는지는 `typography.md` 가 정본이다.** 여기서는 스케일 값만 정한다.
> 로딩 전략, 폴백 메트릭 보정, OpenType 기능, 한글 서브셋, 라이선스 확인이 그 문서에 있다.
---
## 2. 색
### 역할로 정의한다. 팔레트로 정의하지 마라
"파랑 5단계"가 아니라 **무엇에 쓰이는 색인지**로 정의한다.
```css
:root {
--surface: /* 페이지 바탕 */
--surface-raised: /* 카드·패널 (바탕과 구분되되 튀지 않게) */
--ink: /* 본문 텍스트 */
--ink-muted: /* 보조 텍스트 — 대비 4.5:1 유지 */
--line: /* 경계선 */
--accent: /* 강조 역할이 필요할 때의 기본 토큰 */
--accent-ink: /* 강조 위에 올라가는 글자색 */
}
```
> **색 체계의 램프 생성·OKLCH 파생·그라디언트 보간·다크모드 재조정·대비 수정 절차는 [color.md](color.md) 가 정본이다.** 여기서는 역할 토큰 7개만 정한다. 여기 없는 역할이 필요할 때(상태색을 더 세분하거나 채색 액션을 늘릴 때) 확장하는 방법은 color.md §8을 본다.
### 규칙
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. **제목을 `<div class="head">` 로 감싼 섹션에서는 인접 형제가 끊긴다.**
그 섹션만 혼자 0px 이 된다. 제목 묶음 클래스도 같은 규칙에 넣어라.
3. **섹션이 각자 `margin-top` 을 또 붙이면 전역이 무력해진다.**
위쪽은 전역이, 아래쪽만 섹션이 정한다 — `margin-block: A B` 가 아니라 `margin-bottom: B`.
### 반복되는 리듬은 값이 아니라 토큰 하나로
같은 리듬을 세 곳에서 쓰면 세 곳이 따로 논다. 실측 사례 — 타임라인에서
정거장 패딩은 `--space-5`, 마커 원의 `top` 은 `calc(var(--space-4) + 0.55em)`,
연결선의 `top/bottom` 은 `--space-4` 로 각자 적혀 있었다. 패딩만 한 단계 올리자
**원이 제목보다 24px 위에 떠 버렸다.**
```css
.spine {
--stop-pad: var(--space-5); /* 리듬의 출처는 한 곳 */
}
.spine::before { top: var(--stop-pad); bottom: var(--stop-pad); }
.stop { padding-block: var(--stop-pad); }
.stop::before { top: calc(var(--stop-pad) + 0.55em); }
```
규칙: **같은 값을 두 번 이상 적게 되면 그 순간 지역 토큰으로 올려라.**
---
## 3-b. 형태 (radius·선)
'스타일 휴리스틱 — radius 어휘를 3종 미만으로 제한한다'가 검사하는 대상이다. **정의하지 않으면 검사할 수 없다.**
```css
:root {
--radius-sm: 2px; /* 입력, 배지 */
--radius-md: 8px; /* 카드, 패널 */
--radius-pill: 999px;
--line-width: 1px;
}
```
**서로 다른 radius 값은 3종 미만으로.** 어휘가 많을수록 우연히 결정된 것으로 보인다.
1종만 쓰는 것도 정당한 선택이다 — `swiss-minimal`·`anti-grid` 에서는 오히려 그쪽이 맞다.
### 동심 radius — 중첩된 둥근 요소의 계산식
**관찰 후보 — 취향이 아니라 기하학이다.** 둥근 요소 안에 다른 둥근 요소를 넣을 때(배지를 담은 카드, 아이콘을 담은 버튼) 바깥 radius를 안쪽 radius와 무관하게 고르면 두 모서리의 곡률이 어긋나 인터페이스가 어색해 보인다. 바깥 radius는 **안쪽 radius + 안쪽 요소까지의 패딩**으로 계산한다.
```
outer-radius = inner-radius + padding
```
예: 안쪽 배지가 `--radius-sm`(2px)이고 카드 패딩이 `--space-3`(16px)이면 카드는 지역 토큰으로 이렇게 계산한다.
```css
.card { --card-radius: calc(var(--radius-sm) + var(--space-3)); border-radius: var(--card-radius); }
```
동심 계산으로 파생한 지역 값은 radius 어휘(3종 미만)에 새 종류로 세지 않는다 — 기준 radius에서 계산으로 나온 값이다. 패딩이 24px를 넘으면 이 관계가 시각적으로 느슨해지므로, 그 지점부터는 바깥 radius를 독립적으로 다시 고른다 [SKILL-BETTER-UI].
그림자·깊이 토큰과 radius를 함께 짝짓는 규칙은 [elevation.md](elevation.md) §3(radius·표면과 짝짓기)을 따른다.
---
## 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 을 쓰면 페이지가 불안해 보인다.
**프로젝트 계약 — 새 duration 리터럴을 만들지 않는다.** 외부 레퍼런스나 라이브러리 프리셋이 다른 수치를 제시해도(예: 프레스 피드백 `scale(0.96)`~`scale(0.98)`) 그 값을 위 4단계 duration 토큰 중 가장 가까운 것에 매핑해 쓴다 — 프레스처럼 즉시 반응해야 하는 상태 변화는 `--dur-instant`가 기본 후보다 [SKILL-APPLE-DESIGN][SKILL-BETTER-UI]. 값 자체(0.96 대 0.98)의 차이는 실질적이지 않으므로 번호를 맞추려 하지 마라.
**관찰 후보 — 드로어류에는 별도 이징 곡선을 조건부로 둘 수 있다.** iOS 스타일 드로어가 브리프의 요구일 때만 위 3단계 이징에 추가하지 않고 드로어 컴포넌트 스코프에 로컬로 `--ease-drawer`를 정의해 쓴다. 값과 적용 범위는 `references/motion.md`(§1 이징 각주)와 일관되게 유지한다 [SKILL-EMIL-DESIGN-ENG].
**프로젝트 계약 — `prefers-contrast`에도 반응한다.** WCAG 성공 기준은 아니지만, `prefers-reduced-motion`만 처리하고 대비를 더 원하는 사용자 설정을 무시하면 색 토큰이 그 사용자에게는 그대로 방치된다. `prefers-contrast`의 지원 상태와 판정 위계는 [accessibility.md](accessibility.md) §12가 정본이고, 램프를 실제로 재조정할 때 쓰는 색 쪽 기법(명도 우선 조정·재측정)은 [color.md](color.md) §6을 따른다 [SKILL-APPLE-DESIGN].
---
## 웨이트와 컨트롤 — 자주 빠지는 두 축
타입 스케일·색·간격은 대부분 토큰으로 잡는다. **웨이트와 컨트롤 치수는 거의 안 잡는다.**
그래서 SSOT 가 거기서 먼저 깨진다.
### 웨이트는 세 개다
```css
--weight-body: 400;
--weight-medium: 500;
--weight-strong: 600;
```
컴포넌트마다 `font-weight: 560` 같은 값을 직접 쓰기 시작하면 페이지에 웨이트가 여섯 종이 된다.
실측 사례: designpaca 소개 페이지가 `400 / 500 / 560 / 600 / 620 / 660` 을 동시에 쓰고 있었고,
그중 셋은 토큰에 없는 값이었다.
가변폰트는 660 같은 중간값도 그려준다. 그래서 더 위험하다 — **표준 축(400/500/600/700)을
벗어나면 힌팅이 흐려지고, 폴백 폰트에서는 아예 다른 굵기로 떨어진다.**
**웨이트로 위계를 만들려 들지 마라.** R1 실측(zed.dev 모바일)에서 `font-weight: 400` 이 832회,
나머지 전부 합쳐 12회였다. 위계는 크기와 색이 만든다.
### 버튼처럼 생긴 것은 전부 같은 치수를 쓴다
```css
--control-h: 2.75rem; /* 프로젝트의 편안한 기본 높이 예시 */
--control-h-sm: 2.25rem; /* 36px — 헤더 등 조밀한 자리 */
--control-pad-x: var(--space-3);
```
같은 역할의 컴포넌트는 치수 토큰을 공유한다. 다른 밀도나 위험도는 별도 역할 토큰으로 설명한다. WCAG 2.2 AA의 포인터 target 최소 기준은 예외를 포함해 24×24 CSS px이며, 44px는 많은 터치 UI에서 쓰는 편안한 시작값이지 보편 적합성 수치는 아니다 (`evidence-ledger.md`의 `WCAG-TARGET`).
실측 사례: 같은 페이지의 두 버튼이 높이 36 vs 44, 패딩 16 vs 8, 테두리 1px vs 0,
웨이트 400 vs 500 이었다. 규칙이 없으니 전부 달랐다.
반대로 R1 의 두 버튼은 `h36 · pad-x10 · r4 · w400 · 14px` 로 **픽셀 단위까지 같았다.**
### 떠 있는 헤더는 높이를 토큰으로 내놔야 한다
```css
--header-h: calc(var(--space-3) + var(--space-2) * 2 + var(--control-h-sm));
```
`position: fixed` 헤더는 문서 흐름에서 빠져 있어서, 첫 섹션의 상단 여백과 앵커 목적지
(`scroll-margin-top`)가 그 높이를 알아야 한다. 값을 각자 손으로 맞춰두면
**간격 토큰을 `clamp()` 로 바꾸는 순간 관계가 끊어져 제목이 헤더 뒤로 들어간다.**
값이 아니라 관계를 토큰으로 둬라.
### 큰 간격은 화면에 반응해야 한다
작은 값은 고정값부터, 큰 공간은 `clamp()`부터 검토할 수 있다. 어느 쪽이든 콘텐츠·확대·컨테이너 조건에서 관계가 유지되는지 확인한다.
```css
--space-6: clamp(2.5rem, 1.6rem + 2.6vw, 4rem);
--space-7: clamp(3.5rem, 1.9rem + 4.6vw, 7rem);
--gutter: clamp(2rem, 1.4rem + 1.8vw, 2.5rem);
```
고정으로 두면 모바일에서 섹션 상하 여백 112px 이 그대로 들어가 **화면 높이의 26% 를 여백이 먹는다.**
좌우 여백도 마찬가지다 — R1 모바일 실측은 32px 이었다.
---
## 5. 성능 예산
**여기서 정한다. 구현 후에 재면 이미 늦었다.**
**이 표가 성능 예산의 유일한 원본이다.** SKILL.md 도 `preflight.md` 도 여기를 가리킨다. 다른 문서에 예산 표를 만들면 값이 갈라지고, 갈라진 순간 아무도 어느 쪽이 맞는지 모른다.
| 항목 | 기본 | 데모/포트폴리오 | 5단계에서 |
|---|---|---|---|
| 히어로까지 JS (gzip) | 150KB | 400KB | 측정 |
| WebGL/3D 추가분 | +200KB | +600KB | 측정 |
| 총 전송량 (첫 화면) | 1MB | 2MB | 측정 |
| 첫 인터랙션 (모바일 4G) | 3초 | 5초 | 측정 |
| LCP | field p75 2.5초 이하를 목표로 검토 | 브리프에 명시 | RUM/CrUX 또는 local proxy 표기 |
| CLS | field p75 0.1 이하를 목표로 검토 | 브리프에 명시 | RUM/CrUX 또는 local proxy 표기 |
| 폰트 **패밀리** | 프로젝트별 전송량·언어 범위 예산 | 브리프에 명시 | 실제 요청·전송량 확인 |
| 폰트 전송량(첫 화면) | 100KB | 200KB | 측정 |
| 애니메이션 속성 | compositing 친화 속성을 우선 검토 | 브리프에 명시 | 성능·reduced-motion·상태 피드백 확인 |
예산을 올렸다면 **올렸다는 사실과 이유를 명시**해라. 조용히 넘기는 것이 가장 나쁘다.
> **폰트는 파일 개수가 아니라 패밀리 수와 전송량으로 센다.** 한글 웹폰트를 유니코드 범위별로
> 수십 개 파일로 쪼개는 방식은 실제 사용 문자 범위·캐시·전송 waterfall에 따라 유효할 수 있다. 파일 개수 자체로 최적화를 판정하지 않는다.
> 브라우저는 페이지에 실제로 쓰인 글자 범위만 받는다. 파일 개수를 줄이라고 요구하면
> 한글 프로젝트를 단일 대용량 파일이라는 잘못된 방향으로 몬다.
### 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>
```
프로덕션 빌드에서 재는 편이 개발 서버보다 배포 조건에 가깝다. 다만 PerformanceObserver·Lighthouse 한 번은 **lab proxy**다. CWV의 적합성 주장은 실제 사용자의 field data에서 모바일·데스크톱별 75th percentile로 판단한다. local 값은 회귀 탐지에 쓰고 field 수치와 혼동하지 않는다 (`evidence-ledger.md`의 `CWV`, `CWV-METHOD`).
### 예산을 지키는 기본 수단
- 폰트: 상세는 `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
/* 형태 — '스타일 휴리스틱 — radius 어휘를 3종 미만으로 제한한다'가 이걸 검사한다 */
--radius-*, --line-width
/* 모션 */
--dur-*, --ease-*
}
```
그리고 한 줄로: **성능 예산 = JS ___KB / 첫 인터랙션 ___초 / 폰트 패밀리 ___개**
**정의하지 않은 토큰은 5단계에서 검사할 수 없다.** 쓰지 않을 항목은 "쓰지 않음"이라고 적어라 — 빈칸과 "없음"은 다르다.
> 근거: research/references/03-trends-2026.md, 04-ai-slop-signatures.md (조사일 2026-08-20)