05 — Variable Fonts & Axes Tokens
이 문서가 답하는 질문:
font-weight: 400 / 700둘만 토큰화하던 시대는 끝났다. Variable font의 연속 축(weight 100900, width 75125, optical size 8~144 등)을 디자인 시스템에 어떻게 토큰화할까? 한 줄 답 (Pyramid Top): Variable font는 “하나의 .woff2 파일이 weight·width·optical size 같은 연속 축을 노출” 한다 — 토큰은 고정 정수가 아니라font-weight-bold: 600또는font-weight-bold: var(--weight-bold)처럼 축 위의 의미적 위치로 정의한다.wght / wdth / opsz / slnt의 4개 표준 축과 폰트별 custom 축을 구분해서 다뤄야 한다.
Why — Static font의 한계
1) “Regular / Medium / SemiBold / Bold 4개 파일”
전통 워크플로:
@font-face { font-family: "Inter"; font-weight: 400; src: url("Inter-Regular.woff2"); }
@font-face { font-family: "Inter"; font-weight: 500; src: url("Inter-Medium.woff2"); }
@font-face { font-family: "Inter"; font-weight: 600; src: url("Inter-SemiBold.woff2"); }
@font-face { font-family: "Inter"; font-weight: 700; src: url("Inter-Bold.woff2"); }- 4개 파일 × 평균 80KB = 320KB.
- Italic까지 하면 8개. 640KB.
- weight 450·550 같은 중간값은 불가.
2) Variable font 도입
@font-face {
font-family: "Inter";
src: url("Inter-VariableFont.woff2") format("woff2-variations");
font-weight: 100 900; /* 가변 범위 */
}
h1 { font-weight: 450; } /* 정수 아닌 값도 OK */
.bold { font-variation-settings: "wght" 720; } /* 직접 축 제어 */- 단일 파일 220KB — 4개 static의 60% 수준.
- 모든 정수 + 소수 weight 가능.
font-variation-settings로 weight·width·optical size를 동시에 제어.
| 풀려는 문제 | static font | variable font |
|---|---|---|
| 파일 크기 | 큼 (× weight 수) | 작음 (단일) |
| 중간 weight | 불가 | 가능 |
| Animation | 불가 (weight 점프) | 부드러운 transition |
| 축의 다양성 | weight·italic만 | weight·width·optical·slant + custom |
How — 4개의 표준 축 (registered axes)
OpenType 명세는 4개의 등록된 축 이름을 정의한다 — 모든 variable font가 이 이름을 따라야 함.
각 축의 의미
| 축 | 이름 | 범위 | CSS 속성 | 의미 |
|---|---|---|---|---|
wght | weight | 1~1000 | font-weight | 굵기. 400=Regular, 700=Bold |
wdth | width | 50~200 | font-stretch | 자형 폭. 100=normal, 75=condensed |
opsz | optical size | 8~144 | font-optical-sizing | 작은 글자는 더 굵게, 큰 글자는 더 가늘게 (시각 보정) |
slnt | slant | -90~90 | font-style: oblique <angle> | 기울기 (각도). italic과 별개 |
ital | italic | 0 / 1 | font-style | 진짜 italic (별도 자형) |
Custom axes 예시
폰트 디자이너가 정의한 비표준 축은 대문자 4자로 명명한다 (registered axes는 소문자).
| 폰트 | 축 | 범위 | 효과 |
|---|---|---|---|
| Recursive | MONO | 0~1 | mono ↔ proportional |
| Recursive | CASL | 0~1 | 기하학적 ↔ casual |
| Roboto Flex | GRAD | -200~150 | width를 안 바꾸고 굵기만 |
| Roboto Flex | XOPQ | 27~175 | 가로 획 두께 |
| Fraunces | SOFT | 0~100 | 모서리 부드러움 |
What — 축의 토큰화 전략
1) Static 매핑 (가장 흔함)
// panda.config.ts
theme: {
tokens: {
fontWeights: {
thin: { value: "100" },
extralight: { value: "200" },
light: { value: "300" },
regular: { value: "400" },
medium: { value: "500" },
semibold: { value: "600" },
bold: { value: "700" },
extrabold: { value: "800" },
black: { value: "900" }
}
}
}- 토큰 = 정수. 단순.
- 한계: 연속 축의 장점을 못 살림.
2) 의미적 alias
semanticTokens: {
fontWeights: {
"body": { value: "{fontWeights.regular}" }, // 400
"body.strong": { value: "{fontWeights.semibold}" }, // 600
"heading": { value: "{fontWeights.bold}" }, // 700
"heading.display": { value: "{fontWeights.black}" } // 900
}
}- 컴포넌트는 의미로만 호출 (
fontWeight: "heading"). - 브랜드 톤 변경 →
heading = bold→heading = extrabold한 줄로.
3) 축의 범위 토큰 (advanced)
variable font의 진짜 장점을 살리려면 범위도 토큰화:
@theme {
--weight-bold-min: 600;
--weight-bold-max: 720;
--weight-bold: var(--weight-bold-max);
}
.button {
font-weight: var(--weight-bold);
}
.button:hover {
/* hover에서 *살짝 더 굵게* — variable font의 진짜 효과 */
font-weight: calc(var(--weight-bold) + 40);
transition: font-weight 200ms;
}4) font-variation-settings 토큰
여러 축을 동시에 제어할 때:
theme: {
tokens: {
fontVariationSettings: {
"heading.compressed": {
value: "'wght' 700, 'wdth' 85, 'opsz' 72"
},
"body.optimal": {
value: "'wght' 400, 'wdth' 100, 'opsz' 14"
}
}
}
}
font-weight: 700보다font-variation-settings: 'wght' 700이 우선되지만, 코드 가독성을 위해 가능하면 표준 CSS 속성을 쓰고 custom 축만font-variation-settings로.
What — font-display (로딩 전략)
variable font는 파일이 크다 (200~400KB). 로딩 중 FOUT/FOIT 관리가 필수.
@font-face {
font-family: "Inter";
src: url("Inter-Variable.woff2") format("woff2-variations");
font-weight: 100 900;
font-display: swap; /* 추천 */
}| 값 | 동작 | 사용처 |
|---|---|---|
auto | 브라우저 default (보통 block) | 비추천 |
block | 3s 동안 글자 안 보임 | logo·대형 헤드라인 |
swap | 바로 fallback 보여주고 교체 | 본문 default |
fallback | 100ms block + 3s swap | 짧은 깜빡임 허용 |
optional | 100ms block, 그 후엔 교체 안 함 | 최적화 우선 |
추가 최적화
@font-face {
font-family: "Inter";
src: url("Inter-Variable.woff2") format("woff2-variations");
font-weight: 100 900;
font-display: swap;
/* fallback 폰트 메트릭 보정 — CLS 0 */
size-adjust: 100.06%;
ascent-override: 90%;
descent-override: 22.43%;
line-gap-override: 0%;
}→ Capsize와 결합하면 FOUT 시 글자 위치 안 튐 (04-line-height-and-leading-trim 참고).
What — font-feature-settings
OpenType 기능(ligature, tabular numbers, stylistic sets 등)을 축과 별개로 토큰화.
theme: {
tokens: {
fontFeatureSettings: {
"numeric.tabular": { value: "'tnum' on" }, // 표·코드용 등폭 숫자
"numeric.oldstyle": { value: "'onum' on" }, // 본문용 옛 양식 숫자
"ligature.disabled": { value: "'liga' off, 'clig' off" }, // 글자별 정확도
"stylistic.alt1": { value: "'ss01' on" } // 폰트별 alt 자형
}
}
}자주 쓰는 features
| Tag | 의미 | 권장 사용처 |
|---|---|---|
liga | 표준 리거처 (fi, fl) | default on (대부분 폰트) |
dlig | 추가 리거처 | 마케팅·디스플레이 |
tnum | 등폭 숫자 | 표·금액·차트 |
pnum | 비례 숫자 | 본문 |
lnum | lining (대문자형 숫자) | 헤딩 |
onum | oldstyle (소문자형 숫자) | 본문 (글자 사이에 자연스러움) |
ss01..ss20 | stylistic set | 폰트마다 다름 (Inter ss01 = single-storey g 등) |
kern | 커닝 | default on |
frac | 분수 (1/2 → ½) | 레시피·기술 문서 |
.code-block {
font-feature-settings: "tnum" on, "ss01" on, "liga" off;
}
.stat-number {
font-feature-settings: "tnum" on, "lnum" on;
}What-if — 잘못 쓰면 어떻게 깨지는가
- 함정 1: variable font 파일에 모든 축이 다 있다고 가정 — 어떤 폰트는
wght만, 어떤 폰트는wght + wdth + opsz. 대응: 폰트마다 지원 축을 Wakamai Fondue로 확인 후 토큰 설계. - 함정 2:
font-variation-settings+font-weight같이 사용 —font-variation-settings가 모든 축을 override. 즉font-variation-settings: 'wght' 400을 설정한 element는font-weight: 700이 무시. 대응: variable font 사용 시 한 가지 방식만 일관되게. - 함정 3:
font-display: block본문 사용 — 폰트 로드 전 3초 동안 글자 안 보임. 대응: 본문은 항상swap. - 함정 4: optical size 직접 안 줌 —
font-optical-sizing: auto가 default이긴 하나, Safari 일부 버전에서 값을 명시 안 하면 적용 안 됨. 대응: 큰 헤딩에는font-variation-settings: 'opsz' 72직접 명시. - 함정 5: custom 축 이름 충돌 — Recursive의
MONO와 다른 폰트의MONO가 다른 의미일 수 있음. 대응: custom 축은 폰트별 prefix로 토큰화 (recursive.mono,roboto-flex.grad). - 함정 6: 폰트 라이선스 미준수 — 일부 상용 폰트는 variable 버전 별도 라이선스. 대응: foundry 약관 확인.
Insight — 왜 2024년이 variable font의 변곡점인가
Variable font 표준은 2016년 OpenType 1.8 에서 발표됐다. 그런데 실무 도입은 2024년에야 변곡점을 맞이했다. 8년이 걸린 이유:
1) 브라우저 지원의 완성
- 2018년 Chrome/Safari/Firefox 모두 기본 지원.
- 하지만
font-variation-settings가font-weight를 무시하는 상호작용 버그가 여러 브라우저에서 2022년까지 잔존.
2) 폰트 공급의 부족
- 2018~2020년: Inter, Roboto Flex 외에 변수 폰트가 별로 없었음.
- 2022년 Google Fonts가 주요 폰트 100여 종을 variable로 재배포.
- 2024년 Apple System Font(SF Pro)도 variable 공개 → 디자이너 입장에서 원하는 폰트가 있음.
3) 디자인 시스템 도구의 지원
- Figma는 2023년에야 variable font 슬라이더 추가.
- 토큰 도구(Style Dictionary, Tokens Studio)는 2024년에야 축 토큰을 1급 시민으로.
4) 실용 케이스의 발견
Roboto Flex의 GRAD 축(2022)이 결정적이었다 — 글자 폭은 고정한 채 굵기만 늘리는 축이 등장하자, hover/focus 시 살짝 더 굵게 하는 마이크로 인터랙션이 디자인 시스템 사례로 폭발. layout shift 없이 가중치만 애니메이션되는 첫 사례.
.button {
font-variation-settings: "wght" 500, "GRAD" 0;
transition: font-variation-settings 200ms;
}
.button:hover {
font-variation-settings: "wght" 500, "GRAD" 100; /* 굵기↑, width 변화 0 */
}이게 “variable font는 단지 파일 절약이 아니라 애니메이션 가능한 타이포” 라는 패러다임을 만들었다.
요약
- Variable font는 연속 축(
wght / wdth / opsz / slnt / ital)을 가진 단일 파일. - 토큰은 정수(
bold: 700) → 의미 alias(heading: bold) → 범위·custom까지 3단계. font-display: swap+size-adjust로 FOUT 관리.font-feature-settings로 OpenType 기능 (tnum,ss01)도 토큰화.- 2024년 이후가 진짜 실무 변곡점 — hover 시 굵기 애니메이션이 사실상 표준.