'폰트 관련된 것도 스킬이 좀 잘 해줘야 하는데 그렇지 못한 것 같다'는 지적. 점검해보니 size-adjust, ascent-override, unicode-range, OpenType 기능, FOUT/FOIT, 폰트 라이선스가 스킬 어디에도 없었다. font-display 와 서브셋은 tokens.md 에 한 줄씩뿐이었다. references/typography.md 신설 - 고르는 절차: 개수를 먼저 정하고, 성격을 브리프에서 끌어내고, x-height·대비·웨이트 범위·숫자 형태를 실제로 재서 검증한다 - 가변 폰트: 축, 표준 속성 우선, 가변 축 애니메이션이 왜 게이트 8 위반인지 - 로딩: font-display 값별 선택 기준, preload 조건, unicode-range - 폴백 메트릭: size-adjust 와 override 3종. 재는 코드 포함 - OpenType: tabular-nums 등. font-variant-* 를 font-feature-settings 보다 먼저 - 한글: 동적 서브셋 원리, 웨이트가 라틴과 다르게 보이는 이유, 스택 순서 - 라이선스: 웹 임베딩 허용 여부가 상업 이용 허용과 다르다는 것 그리고 이 페이지가 그 규칙을 어기고 있었다 - 실측: 맑은 고딕이 Pretendard 보다 13.0% 넓다. 교체 순간 줄바꿈이 다시 잡힌다 - fonts.css 신설. OS 별로 폴백을 따로 정의하고 size-adjust 로 맞췄다 - 실측 차이 13.0% -> 0.0% preflight.md 에 폰트 검사 6개를 추가했다. 지금까지 하나도 없었다.
248 lines
10 KiB
Markdown
248 lines
10 KiB
Markdown
# 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)
|