🧩 Design System3. Typography & Spacing05 — Variable Fonts & Axes Tokens

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 fontvariable font
파일 크기큼 (× weight 수)작음 (단일)
중간 weight불가가능
Animation불가 (weight 점프)부드러운 transition
축의 다양성weight·italic만weight·width·optical·slant + custom

How — 4개의 표준 축 (registered axes)

OpenType 명세는 4개의 등록된 축 이름을 정의한다 — 모든 variable font가 이 이름을 따라야 함.

각 축의 의미

이름범위CSS 속성의미
wghtweight1~1000font-weight굵기. 400=Regular, 700=Bold
wdthwidth50~200font-stretch자형 폭. 100=normal, 75=condensed
opszoptical size8~144font-optical-sizing작은 글자는 더 굵게, 큰 글자는 더 가늘게 (시각 보정)
slntslant-90~90font-style: oblique <angle>기울기 (각도). italic과 별개
italitalic0 / 1font-style진짜 italic (별도 자형)

Custom axes 예시

폰트 디자이너가 정의한 비표준 축대문자 4자로 명명한다 (registered axes는 소문자).

폰트범위효과
RecursiveMONO0~1mono ↔ proportional
RecursiveCASL0~1기하학적 ↔ casual
Roboto FlexGRAD-200~150width를 안 바꾸고 굵기만
Roboto FlexXOPQ27~175가로 획 두께
FrauncesSOFT0~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 = boldheading = 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)비추천
block3s 동안 글자 안 보임logo·대형 헤드라인
swap바로 fallback 보여주고 교체본문 default
fallback100ms block + 3s swap짧은 깜빡임 허용
optional100ms 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비례 숫자본문
lnumlining (대문자형 숫자)헤딩
onumoldstyle (소문자형 숫자)본문 (글자 사이에 자연스러움)
ss01..ss20stylistic 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-settingsfont-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 시 굵기 애니메이션이 사실상 표준.