feat(skill): route fixes, delivery report, composition diversity, platform refresh
- route table: extension route for products with an existing visual language, feedback row, derived state tokens stay local - completion: blocking 0 plus reported unverified; hard-gate surface is the touched scope; delivery report starts with what and how to open - design.md created at step 0 with a skeleton, fingerprint line and change log; product-surface rubric reading; direction delegation states the chosen card and reason - layout: hero families, nav forms, footer forms; art-direction page convergence 6 axes; genre sections; image treatments; placeholder conventions; multi-screen order; Tailwind theme mapping - platform: scroll-driven examples, anchor positioning, canvas OT, three/R3F pins, shadcn style prefixes, Safari 26 notes, subgrid - design-gate: four result states, exit codes, cwd module resolution, --url/--out-dir/--strict, oklch contrast, dead selector word match - Korean font budget split from the Latin baseline
This commit is contained in:
parent
9bcb45cbd3
commit
88c11682b1
51 changed files with 5834 additions and 2259 deletions
|
|
@ -6,6 +6,22 @@
|
|||
|
||||
---
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| **3단계에서 토큰 전체를 정한다** | §1~§4 + "웨이트와 컨트롤"(§4 뒤, 번호 없는 절) + §6. §5는 예산을 정할 때 |
|
||||
| 타입 스케일·행간·measure를 정한다(한글 값의 정본은 [typography.md](typography.md) §6-5) | §1 |
|
||||
| 역할 색 토큰 | §2(방법은 [color.md](color.md)) |
|
||||
| 간격 스케일·수직 리듬·제목 간격 | §3 |
|
||||
| radius·선 | §3-b |
|
||||
| 모션 토큰 | §4 |
|
||||
| 웨이트·컨트롤 치수·헤더 높이 | "웨이트와 컨트롤" |
|
||||
| 성능 예산(INP·LCP·CLS·전송량) | §5 |
|
||||
| 3단계 산출물 형식(Tailwind·컴포넌트 기반이면 [component-systems.md](component-systems.md) §3·§7-1) | §6 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 타입 스케일
|
||||
|
||||
### 비율을 먼저 고른다
|
||||
|
|
@ -24,7 +40,7 @@
|
|||
```css
|
||||
:root {
|
||||
/* Perfect Fourth (1.333), 본문 16px 기준 */
|
||||
--step--1: 0.75rem; /* 12 — 캡션, 레이블 */
|
||||
--step--1: 0.75rem; /* 12 — 라틴 캡션·레이블. 한글 프로젝트는 0.8125rem(13) 이상에서 시작(typography §6-5) */
|
||||
--step-0: 1rem; /* 16 — 본문 */
|
||||
--step-1: 1.333rem; /* 21 — 리드 문단, 소제목 */
|
||||
--step-2: 1.777rem; /* 28 — h3 */
|
||||
|
|
@ -45,14 +61,16 @@
|
|||
|
||||
| 토큰 | 규칙 |
|
||||
|---|---|
|
||||
| `--leading-tight` | 1.1~1.2부터 검토 — 디스플레이/헤드라인 |
|
||||
| `--leading-normal` | 1.5~1.6(라틴), **1.6~1.8(한글)**부터 검토 — 본문 |
|
||||
| `--measure` | 한 줄 길이의 시작 범위. 라틴 60~75자, **한글 25~40자** |
|
||||
| 자간 | 큰 라틴 디스플레이에서만 작은 음수 자간을 후보로 두고 실제 렌더로 확인. 한글은 글꼴·크기·문맥별로 시험하며, 음수값을 기본 처방이나 절대 금지로 두지 않는다 |
|
||||
| `--leading-tight` | 1.1~1.2부터 검토(라틴), **한글 표제 1.2~1.35** — 디스플레이/헤드라인 |
|
||||
| `--leading-normal` | 1.5~1.6(라틴), **한글 1.7 시작·범위 1.6~1.8·하한 1.5** — 본문 |
|
||||
| `--measure` | 한 줄 길이의 시작 범위. 라틴 60~75자, **한글 32~40자(좁은 컬럼 20자대까지)**. 한글은 `em`으로 적고 `ch`로 적지 않는다 |
|
||||
| 자간 | `em` 단위로만. 큰 라틴 디스플레이에서 작은 음수 자간을 후보로 두고 실제 렌더로 확인. 한글은 본문 0~-0.01em, 큰 표제 -0.02em 시작 + 글리프 충돌 검사 |
|
||||
|
||||
**한글 값의 정본은 [typography.md](typography.md) §6-5다.** 위 표는 그 값을 토큰 자리에 옮긴 요약이다. 둘이 다르면 §6-5를 따르고 이 표를 고친다.
|
||||
|
||||
행간과 measure 범위는 시작값이다. 한글 서체·문자·폭·실제 문단을 렌더하고 확대 상태까지 확인해 정한다. 폰트 수는 전송량·언어 범위·위계·라이선스를 함께 보고 예산으로 정한다. 두 텍스트 패밀리와 필요 시 모노는 흔한 시작점이지만, 세 번째 패밀리가 무조건 실패라는 뜻은 아니다.
|
||||
|
||||
**관찰 후보 — 라틴 측정폭 60자 하한은 고정선이 아니다.** 사이드바·카드처럼 컬럼 자체가 좁을 때는 45~59자 구간도 시작값으로 허용한다. 다른 스킬은 45~75자 범위를 제시하지만, 이 스킬의 60~75자·한글 25~40자 값을 대체하는 것이 아니라 좁은 컬럼에서만 여는 예외다 [SKILL-DESIGN-REVIEW].
|
||||
**관찰 후보 — 라틴 측정폭 60자 하한은 고정선이 아니다.** 사이드바·카드처럼 컬럼 자체가 좁을 때는 45~59자 구간도 시작값으로 허용한다. 다른 스킬은 45~75자 범위를 제시하지만, 이 스킬의 60~75자·한글 32~40자 값을 대체하는 것이 아니라 좁은 컬럼에서만 여는 예외다 [SKILL-DESIGN-REVIEW]. 한글 좁은 컬럼의 예외(20자대)도 같은 성격이다.
|
||||
|
||||
> **어떤 폰트를 어떻게 고르고 싣는지는 `typography.md` 가 정본이다.** 여기서는 스케일 값만 정한다.
|
||||
> 로딩 전략, 폴백 메트릭 보정, OpenType 기능, 한글 서브셋, 라이선스 확인이 그 문서에 있다.
|
||||
|
|
@ -90,19 +108,21 @@
|
|||
|
||||
**다크가 슬롭인 게 아니라 고르지 않은 다크가 슬롭이다.** (`antipatterns.md` §1 — bun.sh 는 다크인데도 슬롭 항목을 거의 전부 회피한다)
|
||||
|
||||
다크를 기본으로 하려면 **근거를 한 줄로 대라.** 정당화되는 이유의 목록은 `presets/dark-instrument.md` 에 있다(야간 운영 환경, 밝은 데이터 시각화의 대비, 제품 자체가 어두움). 댈 수 없으면 라이트로 간다.
|
||||
다크를 기본으로 하려면 **근거를 한 줄로 대라.** 정당화되는 이유의 목록은 `presets/dark-instrument.md` 에 있다(야간 운영 환경, 밝은 데이터 시각화의 대비, 제품 자체가 어두움). 댈 수 없으면 다크를 기본으로 삼지 않는다. 그렇다고 밝은 오프화이트가 자동으로 답이 되는 것은 아니다 — 배경 명도는 프리셋의 범위([presets/README.md](presets/README.md), 중간 명도·유채색 면 포함)에서 브리프 근거로 고른다.
|
||||
|
||||
라이트/다크 모두를 지원하기로 한 제품은 역할 토큰을 각각 정의하고 실제 상태를 검증한다. 제품이 한 테마만 제공할 수는 있지만, 사용자 환경·브리프·운영 맥락과 접근성 영향을 명시한다.
|
||||
|
||||
지원할 때는 색을 뒤집는 게 아니라 **역할별로 다시 정의**한다. 순수 검정·흰색과 근접한 색은 제품·디스플레이·주변광에 따라 읽기 경험이 다르므로, 대비·눈부심·브랜드 표면을 실제 기기에서 확인한다.
|
||||
|
||||
```css
|
||||
:root { --surface: #fbfaf8; --ink: #1a1917; }
|
||||
:root { --surface: <밝은 테마 표면>; --ink: <그 위에서 대비를 잰 본문색>; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --surface: #14130f; --ink: #e8e4dc; }
|
||||
:root { --surface: <어두운 테마 표면 — 뒤집지 않고 다시 정한 값>; --ink: <다시 잰 본문색>; }
|
||||
}
|
||||
```
|
||||
|
||||
값을 자리표시로 둔 것은 의도다. 이 자리에 있던 웜 오프화이트·니어블랙 한 쌍이 쇼케이스 표면색의 대부분이 됐다. 성격이 다른 세 쌍의 예는 [color.md](color.md) §5에 있다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 간격
|
||||
|
|
@ -111,18 +131,27 @@
|
|||
|
||||
```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 — 섹션 간격 */
|
||||
/* 4px 기반. 토큰 번호 = px ÷ 4 라서 나중에 값을 끼워 넣어도 번호가 밀리지 않는다 */
|
||||
--space-1: 0.25rem; /* 4 */
|
||||
--space-2: 0.5rem; /* 8 */
|
||||
--space-3: 0.75rem; /* 12 — 컨트롤 내부 gap, 아이콘-라벨 */
|
||||
--space-4: 1rem; /* 16 */
|
||||
--space-6: 1.5rem; /* 24 */
|
||||
--space-8: 2rem; /* 32 — 카드 패딩, 그룹 간격 */
|
||||
--space-12: 3rem; /* 48 */
|
||||
--space-16: 4rem; /* 64 */
|
||||
--space-24: 6rem; /* 96 */
|
||||
--space-32: 8rem; /* 128 — 섹션 간격 */
|
||||
}
|
||||
```
|
||||
|
||||
**중간 값을 즉석에서 만들지 마라.** `--space-4` 와 `--space-5` 사이가 필요하다면 스케일이 잘못된 것이다.
|
||||
**같은 값을 즉석에서 만들지 마라.** 스케일에 없는 값이 반복해서 필요해지면 그 자리에서 리터럴을 쓰지 말고 스케일에 추가(예: 40px이면 `--space-10`)하고 `design.md`에 추가한 이유를 적는다. 한 번만 필요한 값이면 가장 가까운 스케일 값을 쓴다.
|
||||
|
||||
### 수직 리듬
|
||||
|
||||
- 행 높이를 4px 격자로 반올림한다. 16px × 1.5 = 24px처럼 떨어지게 행간을 고르거나, 떨어지지 않으면 `line-height`를 rem 값으로 준다(한글 16px × 1.7 = 27.2px → 28px이면 `1.75rem`).
|
||||
- 컴포넌트 높이(`--control-h`)·간격·행 높이를 같은 4px 격자에 둔다. 그래야 카드 두 개를 나란히 놓았을 때 첫 줄과 버튼 높이가 맞는다.
|
||||
- 엄격한 베이스라인 정렬(모든 텍스트가 한 격자선에 앉는 것)은 에디토리얼·잡지형에서만 연다. 그때는 `text-box: trim-both cap alphabetic`([typography.md](typography.md) §12-2, 점진적 향상)으로 글자 위아래 여백을 깎아 광학적으로 맞춘다. 일반 UI에서는 4px 격자로 충분하다.
|
||||
|
||||
### 여백이 위계를 만든다
|
||||
|
||||
|
|
@ -192,13 +221,13 @@
|
|||
공존했다. **간격이 컴포넌트마다 다르면 그건 SSOT 가 아니다.**
|
||||
|
||||
```css
|
||||
p { margin: 0 0 var(--space-3); }
|
||||
p { margin: 0 0 var(--space-4); }
|
||||
|
||||
/* 제목 다음에 오는 것은 반드시 떨어진다 — 여기 한 곳에서만 정한다 */
|
||||
h1 + *,
|
||||
h2 + *,
|
||||
h3 + *,
|
||||
.head + * { margin-top: var(--space-4); }
|
||||
.head + * { margin-top: var(--space-6); }
|
||||
```
|
||||
|
||||
세 가지 함정이 있다.
|
||||
|
|
@ -214,13 +243,13 @@ h3 + *,
|
|||
### 반복되는 리듬은 값이 아니라 토큰 하나로
|
||||
|
||||
같은 리듬을 세 곳에서 쓰면 세 곳이 따로 논다. 실측 사례 — 타임라인에서
|
||||
정거장 패딩은 `--space-5`, 마커 원의 `top` 은 `calc(var(--space-4) + 0.55em)`,
|
||||
연결선의 `top/bottom` 은 `--space-4` 로 각자 적혀 있었다. 패딩만 한 단계 올리자
|
||||
정거장 패딩, 마커 원의 `top`(`calc(간격 토큰 + 0.55em)`), 연결선의 `top/bottom` 이
|
||||
각자 다른 간격 토큰으로 적혀 있었다. 패딩만 한 단계 올리자
|
||||
**원이 제목보다 24px 위에 떠 버렸다.**
|
||||
|
||||
```css
|
||||
.spine {
|
||||
--stop-pad: var(--space-5); /* 리듬의 출처는 한 곳 */
|
||||
--stop-pad: var(--space-12); /* 리듬의 출처는 한 곳 */
|
||||
}
|
||||
.spine::before { top: var(--stop-pad); bottom: var(--stop-pad); }
|
||||
.stop { padding-block: var(--stop-pad); }
|
||||
|
|
@ -233,19 +262,21 @@ h3 + *,
|
|||
|
||||
## 3-b. 형태 (radius·선)
|
||||
|
||||
'스타일 휴리스틱 — radius 어휘를 3종 미만으로 제한한다'가 검사하는 대상이다. **정의하지 않으면 검사할 수 없다.**
|
||||
radius 스타일 휴리스틱이 검사하는 대상이다. **정의하지 않으면 검사할 수 없다.** 값은 프리셋의 radius 범위([presets/README.md](presets/README.md))에서 고른다. 아래는 이름의 형식이고 값은 자리표시다 — 이 자리에 있던 `2px`·`8px` 한 벌이 쇼케이스 여러 종의 토큰에 그대로 들어갔다.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--radius-sm: 2px; /* 입력, 배지 */
|
||||
--radius-md: 8px; /* 카드, 패널 */
|
||||
--radius-pill: 999px;
|
||||
--radius-control: <프리셋 범위에서>; /* 입력, 버튼, 배지 */
|
||||
--radius-surface: <역할이 다르면 다른 값>; /* 카드, 패널, 모달 */
|
||||
--radius-pill: 999px; /* 필, 토글 */
|
||||
--line-width: 1px;
|
||||
}
|
||||
```
|
||||
|
||||
**서로 다른 radius 값은 3종 미만으로.** 어휘가 많을수록 우연히 결정된 것으로 보인다.
|
||||
1종만 쓰는 것도 정당한 선택이다 — `swiss-minimal`·`anti-grid` 에서는 오히려 그쪽이 맞다.
|
||||
성격이 다른 세 벌의 예(컨트롤 / 표면): 각진 인쇄물 `0 / 0`, 중간 `6px / 12px`, 둥근 소비재 `12px / 24px`. 셋 중 하나를 고르라는 뜻이 아니다.
|
||||
|
||||
**radius 값은 역할 토큰으로만 쓰고, 역할당 하나다.** 기본 어휘는 위처럼 3종 안팎이다. 입력·카드·필·모달은 서로 다른 반경을 갖는 것이 정상이며, 역할이 늘면 토큰도 늘 수 있다(M3의 shape scale은 7단계다). 문제는 종류 수가 아니라 **역할 없이 즉석에서 만든 값**이다 — 같은 역할의 두 컴포넌트가 다른 radius를 쓰면 우연히 결정된 것으로 보인다. 동심 계산으로 파생한 값(아래)은 세지 않는다.
|
||||
1종만 쓰는 것도 선택일 수 있다(예: radius 0 전면). 프리셋이 역할별 차등을 요구하면 그쪽을 따른다.
|
||||
|
||||
### 동심 radius — 중첩된 둥근 요소의 계산식
|
||||
|
||||
|
|
@ -255,13 +286,13 @@ h3 + *,
|
|||
outer-radius = inner-radius + padding
|
||||
```
|
||||
|
||||
예: 안쪽 배지가 `--radius-sm`(2px)이고 카드 패딩이 `--space-3`(16px)이면 카드는 지역 토큰으로 이렇게 계산한다.
|
||||
예: 안쪽 배지가 `--radius-control`(이를테면 6px)이고 카드 패딩이 `--space-4`(16px)이면 카드는 22px가 되도록 지역 토큰으로 계산한다.
|
||||
|
||||
```css
|
||||
.card { --card-radius: calc(var(--radius-sm) + var(--space-3)); border-radius: var(--card-radius); }
|
||||
.card { --card-radius: calc(var(--radius-control) + var(--space-4)); border-radius: var(--card-radius); }
|
||||
```
|
||||
|
||||
동심 계산으로 파생한 지역 값은 radius 어휘(3종 미만)에 새 종류로 세지 않는다 — 기준 radius에서 계산으로 나온 값이다. 패딩이 24px를 넘으면 이 관계가 시각적으로 느슨해지므로, 그 지점부터는 바깥 radius를 독립적으로 다시 고른다 [SKILL-BETTER-UI].
|
||||
동심 계산으로 파생한 지역 값은 radius 어휘에 새 종류로 세지 않는다 — 기준 radius에서 계산으로 나온 값이다. 패딩이 24px를 넘으면 이 관계가 시각적으로 느슨해지므로, 그 지점부터는 바깥 radius를 독립적으로 다시 고른다 [SKILL-BETTER-UI].
|
||||
|
||||
그림자·깊이 토큰과 radius를 함께 짝짓는 규칙은 [elevation.md](elevation.md) §3(radius·표면과 짝짓기)을 따른다.
|
||||
|
||||
|
|
@ -297,23 +328,24 @@ outer-radius = inner-radius + padding
|
|||
타입 스케일·색·간격은 대부분 토큰으로 잡는다. **웨이트와 컨트롤 치수는 거의 안 잡는다.**
|
||||
그래서 SSOT 가 거기서 먼저 깨진다.
|
||||
|
||||
### 웨이트는 세 개다
|
||||
### 웨이트는 역할 토큰 셋에서 시작한다
|
||||
|
||||
```css
|
||||
--weight-body: 400;
|
||||
--weight-medium: 500;
|
||||
--weight-strong: 600;
|
||||
--weight-body: 400;
|
||||
--weight-medium: 500;
|
||||
--weight-strong: 600;
|
||||
--weight-display: 750; /* 선택. 브리프 근거가 있을 때만 — 300 라이트 디스플레이, 800 블랙 헤드라인 등 */
|
||||
```
|
||||
|
||||
본문·UI는 역할 토큰 3개에서 시작하고, 디스플레이용 `--weight-display` 하나를 브리프 근거가 있으면 더 둔다. 한글은 같은 숫자에서 더 무겁게 보이므로 실제 값은 [typography.md](typography.md) §6-2를 보고 정한다.
|
||||
|
||||
컴포넌트마다 `font-weight: 560` 같은 값을 직접 쓰기 시작하면 페이지에 웨이트가 여섯 종이 된다.
|
||||
실측 사례: designpaca 소개 페이지가 `400 / 500 / 560 / 600 / 620 / 660` 을 동시에 쓰고 있었고,
|
||||
그중 셋은 토큰에 없는 값이었다.
|
||||
그중 셋은 토큰에 없는 값이었다. 문제는 값의 개수가 아니라 **역할 없이 즉석에서 만든 값**이다.
|
||||
|
||||
가변폰트는 660 같은 중간값도 그려준다. 그래서 더 위험하다 — **표준 축(400/500/600/700)을
|
||||
벗어나면 힌팅이 흐려지고, 폴백 폰트에서는 아예 다른 굵기로 떨어진다.**
|
||||
가변 폰트는 660 같은 비정수 웨이트도 그린다. 쓸 수 있지만 **토큰으로만** 쓴다. 폴백 폰트에는 그 웨이트가 없어 가장 가까운 정적 웨이트로 떨어지므로, 폰트를 받지 못한 상태에서 위계가 유지되는지 확인한다.
|
||||
|
||||
**웨이트로 위계를 만들려 들지 마라.** R1 실측(zed.dev 모바일)에서 `font-weight: 400` 이 832회,
|
||||
나머지 전부 합쳐 12회였다. 위계는 크기와 색이 만든다.
|
||||
**위계는 크기·굵기·색·여백 중 둘 이상의 차이로 만든다.** 굵기 하나만으로 만든 위계는 약하고, 굵기를 배제할 이유도 없다. 참고 관찰: R1 실측(zed.dev 모바일)은 `font-weight: 400` 이 832회, 나머지 전부 합쳐 12회로 크기와 색이 위계를 거의 다 만들었다 — 한 사이트의 선택이지 규범이 아니다.
|
||||
|
||||
### 버튼처럼 생긴 것은 전부 같은 치수를 쓴다
|
||||
|
||||
|
|
@ -323,7 +355,7 @@ outer-radius = inner-radius + padding
|
|||
--control-pad-x: var(--space-3);
|
||||
```
|
||||
|
||||
같은 역할의 컴포넌트는 치수 토큰을 공유한다. 다른 밀도나 위험도는 별도 역할 토큰으로 설명한다. WCAG 2.2 AA의 포인터 target 최소 기준은 예외를 포함해 24×24 CSS px이며, 44px는 많은 터치 UI에서 쓰는 편안한 시작값이지 보편 적합성 수치는 아니다 (`evidence-ledger.md`의 `WCAG-TARGET`).
|
||||
같은 역할의 컴포넌트는 치수 토큰을 공유한다. 다른 밀도나 위험도는 별도 역할 토큰으로 설명한다. 포인터 target의 최소 크기와 44px 계약의 적용 범위는 [accessibility.md](accessibility.md) §8이 정본이다.
|
||||
실측 사례: 같은 페이지의 두 버튼이 높이 36 vs 44, 패딩 16 vs 8, 테두리 1px vs 0,
|
||||
웨이트 400 vs 500 이었다. 규칙이 없으니 전부 달랐다.
|
||||
반대로 R1 의 두 버튼은 `h36 · pad-x10 · r4 · w400 · 14px` 로 **픽셀 단위까지 같았다.**
|
||||
|
|
@ -331,7 +363,7 @@ outer-radius = inner-radius + padding
|
|||
### 떠 있는 헤더는 높이를 토큰으로 내놔야 한다
|
||||
|
||||
```css
|
||||
--header-h: calc(var(--space-3) + var(--space-2) * 2 + var(--control-h-sm));
|
||||
--header-h: calc(var(--space-4) + var(--space-2) * 2 + var(--control-h-sm));
|
||||
```
|
||||
|
||||
`position: fixed` 헤더는 문서 흐름에서 빠져 있어서, 첫 섹션의 상단 여백과 앵커 목적지
|
||||
|
|
@ -344,8 +376,8 @@ outer-radius = inner-radius + padding
|
|||
작은 값은 고정값부터, 큰 공간은 `clamp()`부터 검토할 수 있다. 어느 쪽이든 콘텐츠·확대·컨테이너 조건에서 관계가 유지되는지 확인한다.
|
||||
|
||||
```css
|
||||
--space-6: clamp(2.5rem, 1.6rem + 2.6vw, 4rem);
|
||||
--space-7: clamp(3.5rem, 1.9rem + 4.6vw, 7rem);
|
||||
--space-block: clamp(2.5rem, 1.6rem + 2.6vw, 4rem); /* 블록 사이 */
|
||||
--space-section: clamp(3.5rem, 1.9rem + 4.6vw, 7rem); /* 섹션 상하 */
|
||||
--gutter: clamp(2rem, 1.4rem + 1.8vw, 2.5rem);
|
||||
```
|
||||
|
||||
|
|
@ -365,11 +397,12 @@ outer-radius = inner-radius + padding
|
|||
| 히어로까지 JS (gzip) | 150KB | 400KB | 측정 |
|
||||
| WebGL/3D 추가분 | +200KB | +600KB | 측정 |
|
||||
| 총 전송량 (첫 화면) | 1MB | 2MB | 측정 |
|
||||
| 첫 인터랙션 (모바일 4G) | 3초 | 5초 | 측정 |
|
||||
| INP | field p75 200ms 이하 | 브리프에 명시 | RUM/CrUX. lab에서는 TBT 200ms 이하(모바일 CPU 4배 스로틀)를 proxy로 표기 |
|
||||
| LCP | field p75 2.5초 이하를 목표로 검토 | 브리프에 명시 | RUM/CrUX 또는 local proxy 표기 |
|
||||
| CLS | field p75 0.1 이하를 목표로 검토 | 브리프에 명시 | RUM/CrUX 또는 local proxy 표기 |
|
||||
| LCP 이미지 우선순위 | LCP 이미지는 lazy 금지, `fetchpriority="high"`(CSS 배경이면 preload) | 같음 | 실제 LCP 요소가 무엇인지 확인. 상세는 [images.md](images.md) §3-c |
|
||||
| 폰트 **패밀리** | 프로젝트별 전송량·언어 범위 예산 | 브리프에 명시 | 실제 요청·전송량 확인 |
|
||||
| 폰트 전송량(첫 화면) | 100KB | 200KB | 측정 |
|
||||
| 폰트 전송량(첫 화면) | 100KB(라틴) · 한글 1패밀리 200KB · 2패밀리 350KB | 한글 2패밀리 450KB | 측정 — 한글 웹폰트는 유니코드 범위 슬라이스로 받아도 첫 화면에 흔히 150~300KB가 든다(쇼케이스 19종 실측 144~480KB). 넘기면 웨이트 수·`text=` 서브셋·시스템 서체 폴백으로 줄이고, 못 줄이면 이유를 적는다 |
|
||||
| 애니메이션 속성 | compositing 친화 속성을 우선 검토 | 브리프에 명시 | 성능·reduced-motion·상태 피드백 확인 |
|
||||
|
||||
예산을 올렸다면 **올렸다는 사실과 이유를 명시**해라. 조용히 넘기는 것이 가장 나쁘다.
|
||||
|
|
@ -381,7 +414,7 @@ outer-radius = inner-radius + padding
|
|||
|
||||
### LCP·CLS 를 어떻게 재나
|
||||
|
||||
`preflight.md` 가 이 값을 요구한다. 재는 법이 없으면 "미측정"으로 남고, 미측정은 통과가 아니다.
|
||||
`preflight.md` 가 이 값을 요구한다. INP는 실제 사용자 입력이 있어야 생기는 field 지표다. 로컬에서는 대표 상호작용을 직접 조작하며 `PerformanceObserver`의 `event` 항목으로 지연을 보거나, lab의 TBT를 proxy로 표기한다. 재는 법이 없으면 "미측정"으로 남고, 미측정은 통과가 아니다.
|
||||
|
||||
```html
|
||||
<!-- 페이지에 임시로 넣고 콘솔을 본다. 측정 후 반드시 제거한다 -->
|
||||
|
|
@ -401,11 +434,21 @@ new PerformanceObserver((l) => {
|
|||
|
||||
프로덕션 빌드에서 재는 편이 개발 서버보다 배포 조건에 가깝다. 다만 PerformanceObserver·Lighthouse 한 번은 **lab proxy**다. CWV의 적합성 주장은 실제 사용자의 field data에서 모바일·데스크톱별 75th percentile로 판단한다. local 값은 회귀 탐지에 쓰고 field 수치와 혼동하지 않는다 (`evidence-ledger.md`의 `CWV`, `CWV-METHOD`).
|
||||
|
||||
LCP와 상호작용 지연(`event`) 엔트리는 2025-12부터 Chrome·Firefox·Safari 모두에서 관측된다 — WebKit에서도 같은 스니펫으로 잰다. `layout-shift`는 Chromium에서만 나오므로(확인 2026-10-06) CLS 수치는 Chromium 값이고, Safari에서는 폰트 교체·이미지 로드 전후 캡처 비교로 대신 본다. 클라이언트 라우팅 SPA에서 이 스니펫은 **첫 문서 로드만** 잰다 — 라우트 전환 뒤의 LCP·CLS는 대표 전환을 직접 조작한 트레이스로 따로 잰다. [WEB-BASELINE]
|
||||
|
||||
### 예산을 지키는 기본 수단
|
||||
|
||||
- 폰트: 상세는 `typography.md` §3~§4. 요약하면 `font-display` 를 의식적으로 고르고, preload 는 첫 화면에 실제로 쓰는 파일만, 폴백 메트릭을 보정하거나 `optional` 을 쓴다
|
||||
- 이미지: 실제 표시 크기의 2배까지만, AVIF/WebP, 첫 화면 밖은 `loading="lazy"`
|
||||
- 이미지: `srcset`·`sizes`로 DPR 2까지의 후보를 주되 실제 표시 크기의 2배를 넘는 파일은 보내지 않는다. 포맷은 AVIF 시작(폴백 판단은 images §3-c), 첫 화면 밖은 `loading="lazy"`, LCP 이미지는 lazy 금지·`fetchpriority="high"`(상세는 [images.md](images.md) §3-c)
|
||||
- JS: 첫 화면에 필요 없는 것은 전부 지연 로드. 3D는 뷰포트 진입 시 동적 import
|
||||
- 페이지 이동(다중 페이지 사이트): 같은 출처 링크에 Speculation Rules를 건다. 지원 브라우저(Chromium)는 링크에 포인터가 머물 때 다음 문서를 미리 렌더하고, 나머지는 이 스크립트를 무시한다(Baseline Limited, 해 없는 점진 향상 — 확인 2026-10-06 [WEB-BASELINE]).
|
||||
```html
|
||||
<script type="speculationrules">
|
||||
{ "prerender": [{ "where": { "and": [ { "href_matches": "/*" },
|
||||
{ "not": { "href_matches": "/logout" } } ] }, "eagerness": "moderate" }] }
|
||||
</script>
|
||||
```
|
||||
로그아웃·장바구니 담기처럼 GET으로 상태를 바꾸는 URL은 `not`으로 뺀다. 분석·광고 스크립트가 프리렌더를 조회로 세지 않는지 확인한다. `eager`로 올리지 않는다 — 방문자의 데이터와 메모리를 쓴다. 클라이언트 라우팅 SPA에는 해당 없다.
|
||||
- **측정하지 않은 최적화는 하지 마라.** 대신 예산을 넘겼는지는 반드시 측정해라
|
||||
|
||||
---
|
||||
|
|
@ -421,16 +464,20 @@ new PerformanceObserver((l) => {
|
|||
/* 색 — 역할별. 다크 테마도 함께 */
|
||||
--surface, --surface-raised, --ink, --ink-muted, --line, --accent, --accent-ink
|
||||
/* 간격 */
|
||||
--space-1 ~ --space-8
|
||||
/* 형태 — '스타일 휴리스틱 — radius 어휘를 3종 미만으로 제한한다'가 이걸 검사한다 */
|
||||
--radius-*, --line-width
|
||||
/* 모션 */
|
||||
--space-1 ~ --space-32 (4px 기반, 번호 = px ÷ 4), 필요 시 --space-block·--space-section
|
||||
/* 형태 — 역할당 radius 하나(기본 3종 안팎). radius 스타일 휴리스틱이 이걸 검사한다 */
|
||||
--radius-control, --radius-surface, --radius-pill, --line-width
|
||||
/* 웨이트 */
|
||||
--weight-body, --weight-medium, --weight-strong, (선택) --weight-display
|
||||
/* 모션 — 모션을 쓰면 --stagger·--shift-sm·--shift-lg 도(motion.md §1) */
|
||||
--dur-*, --ease-*
|
||||
}
|
||||
```
|
||||
|
||||
그리고 한 줄로: **성능 예산 = JS ___KB / 첫 인터랙션 ___초 / 폰트 패밀리 ___개**
|
||||
컴포넌트 기반·Tailwind 프로젝트에서는 이 블록을 새 파일로 만들지 않는다. 같은 역할을 프로젝트의 테마 자리에 넣는다([component-systems.md](component-systems.md) §3·§7-1).
|
||||
|
||||
그리고 한 줄로: **성능 예산 = JS ___KB / INP ___ms / LCP ___초 / 폰트 패밀리 ___개**
|
||||
|
||||
**정의하지 않은 토큰은 5단계에서 검사할 수 없다.** 쓰지 않을 항목은 "쓰지 않음"이라고 적어라 — 빈칸과 "없음"은 다르다.
|
||||
|
||||
> 근거: research/references/03-trends-2026.md, 04-ai-slop-signatures.md (조사일 2026-08-20)
|
||||
> 근거: 저장소 조사 문서 03-trends-2026, 04-ai-slop-signatures (저장소 전용, 설치본 미포함, 조사일 2026-08-20)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue