🎨 Frontend CSS4. 타이포그래피03 — Font Metrics & Line Height

03 — Font Metrics & Line Height

“같은 font-size: 16px인데 왜 폰트마다 줄 높이가 다른가” — 답은 폰트 파일 안에 박혀 있는 metric 숫자들에 있다. 그 숫자들을 모르면 line-height추측하게 된다.


한 문장 답 (Pyramid Top)

한 줄의 높이는 font-size가 아니라 폰트 파일의 ascent + descent + line-gap으로 결정된다. CSS의 line-height는 그 위에 추가 공간을 더하거나 덮어쓰는 도구이고, size-adjust·ascent-override폰트 자체의 metric을 외부에서 조정할 수 있다.


Why — 왜 이걸 알아야 하나

세 가지 흔한 버그가 모두 metric 이해 부족에서 온다.

  1. 버튼 안의 글자가 위로 치우침 — 폰트의 ascent가 descent보다 훨씬 커서 라인박스 중앙이 글자 중앙과 다르다.
  2. 카드 높이가 폰트 로드 후 점프 — fallback 폰트와 web font의 metric이 달라 줄 높이 변동.
  3. 디자이너의 Figma 8px이 CSS에선 12px — Figma는 글자 시각 높이(cap height), CSS는 라인박스 전체를 측정.

How — 폰트 파일 안의 metric

폰트 파일(WOFF2 등)에는 OS/2 테이블, hhea 테이블, head 테이블 등에 여러 종류의 metric이 박혀 있다.

핵심 4개 값

┌─────────────────────┐  ← Ascent (윗변)
│                     │
│    H g    pâ        │  ← Cap Height (대문자 H 높이)
│           xÔ        │  ← x-height (x 높이)
│                     │  ← Baseline (기준선)
│                     │  ← Descent (아랫변, g/p의 꼬리)
└─────────────────────┘
└── line-gap ──┘      ← 다음 줄까지 여백
의미
units-per-em (UPM)폰트 좌표계 단위 — 보통 1000 또는 2048
ascentbaseline에서 위쪽 한계까지 (UPM 단위)
descentbaseline에서 아래쪽 한계까지 (UPM 단위, 음수일 수도)
line-gap줄 간 여백 (UPM 단위)
cap-height대문자 H의 위쪽
x-height소문자 x의 위쪽

실제 폰트 값 예 (UPM 1000 기준)

폰트ascentdescentline-gapx-height
Inter9682320540
Roboto19005000528 (UPM 2048 기준)
Pretendard10002000555
Times New Roman182544387916 (UPM 2048)
Helvetica157747101099 (UPM 2048)

한 줄의 높이 계산

font-size: 16px, UPM 1000인 폰트의 기본 줄 높이:

한 줄 높이 = font-size × (ascent + descent + line-gap) / UPM
          = 16 × (968 + 232 + 0) / 1000
          = 16 × 1.2
          = 19.2px

이게 line-height: normal 의 실제 값이다. 폰트마다 이 비율이 다르다 — Times는 1.15, Inter는 1.20, 한글 폰트는 종종 1.3 이상.


How — CSS line-height 의 시맨틱

1) 단위 없는 숫자 vs em vs px

.a { line-height: 1.5; }    /* 권장 */
.b { line-height: 1.5em; }  /* 위험 — 상속 시 누적 안 됨 */
.c { line-height: 24px; }   /* 위험 — 폰트 크기 변경 시 깨짐 */

1.5 (단위 없음):

  • 자식이 자기 폰트 크기에 1.5를 곱한다 → 자식 폰트 크기가 12px이면 line-height 18px.
  • 상속이 multiplier 자체라 어디서나 올바르게 계산된다.

1.5em:

  • 부모 폰트 크기로 계산된 값이 상속된다. 부모가 16px면 24px이 상속됨 — 자식이 12px여도 line-height는 그대로 24px (의도와 다름).

24px:

  • 고정값. 폰트가 커져도 line-height 그대로 → 폰트가 잘림.

line-height는 항상 단위 없는 숫자.

2) line-height: normal vs 명시값

.a { line-height: normal; }  /* 폰트 metric의 기본값 — 폰트마다 다름 */
.b { line-height: 1.5; }     /* 명시 — 폰트 무관 일정 */

디자인 시스템에서는 항상 명시한다 — 폰트가 바뀌어도 줄 높이 일관성.

3) 라인박스의 반-Leading

CSS는 line-height가 폰트의 metric보다 크면, 그 차이를 위아래 절반씩 분배한다 — half-leading.

font-size: 16px
폰트 metric 줄 높이 = 19.2px
line-height: 1.6 (= 25.6px)
추가 공간 = 25.6 - 19.2 = 6.4px
위에 3.2px + 아래에 3.2px (half-leading)

이 half-leading 때문에 버튼 안 글자가 가운데 정렬되지 않는 것처럼 보인다 — 글자 자체는 ascent가 더 큰데, line-height 추가 공간은 균등 분배되어 위에서 보면 위쪽이 더 비어 보인다.


How — text-edge & leading-trim (CSS Inline Layout 3, 2024)

half-leading 문제를 해결하는 모던 솔루션. 버튼·태그·라벨의 padding을 정확히 글자 시각 높이에 맞추는 도구.

/* 신규 — Chrome 113+ */
.button {
  font-size: 16px;
  line-height: 1.5;
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;  /* 위: 대문자 윗변, 아래: baseline */
  padding: 12px 16px;
}
  • text-box-edge: cap alphabetic — 위쪽은 cap height, 아래쪽은 baseline. 글자가 보이는 시각 영역에만 박스를 맞춤.
  • text-box-trim: trim-both — 위아래 leading 제거.

효과: padding 12px이 진짜로 글자 위 12px이 된다. 디자이너의 Figma 수치가 그대로 CSS로 옮겨진다.

2024년 기준 Safari 16.4+, Chrome 113+. Firefox는 아직 미지원.


How — size-adjust로 fallback metric 맞추기

Web font가 늦게 도착하면, 그 사이 fallback이 그린다. 두 폰트의 metric이 다르면 — 글자 위치, 줄 높이, 단어 폭이 변한다 → CLS.

원리

@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107%;        /* Arial을 107%로 키워서 Inter와 글자 폭 맞춤 */
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}
 
body {
  font-family: "Inter", "Inter Fallback", sans-serif;
}
속성효과
size-adjust: N%폰트 전체를 N%로 스케일 (글자 폭·높이 모두)
ascent-override: N%위쪽 한계를 font-size의 N%로
descent-override: N%아래쪽 한계를 font-size의 N%로
line-gap-override: N%줄 간격을 N%로

자동 계산 도구

  • Capsize (Vanilla Extract 팀) — 두 폰트 이름을 주면 size-adjust 값 출력.
  • Fontaine (Nuxt/Vite 플러그인) — 빌드 타임 자동 변환.
  • Next.js next/font — Google Fonts·로컬 폰트 모두 자동.

효과

  • 로드 전: Arial이지만 Inter처럼 보이는 크기 (글자 폭·줄 높이 일치).
  • 로드 후: 진짜 Inter로 교체.
  • 사용자가 차이를 거의 못 느끼고 CLS 0.

What — Figma vs CSS의 차이

디자이너가 Figma에서 글자 위에 8px 라고 잰 것은 cap height 위 8px이다. CSS의 padding-top: 8px라인박스 위 8px = ascent 위 8px.

Figma 기준:   ┌──────┐
              │  8px │ ← padding
              ├──────┤ ← cap height
              │  H   │
              └──────┘

CSS 기준:     ┌──────┐
              │ leading │ ← half-leading (반-leading)
              │  8px │ ← padding
              ├──────┤ ← ascent (그 위 leading)
              │  H   │
              └──────┘

→ Figma 수치를 그대로 옮기면 실제 화면은 더 비어 보인다. 해결책:

  1. text-box-trim 사용 (모던).
  2. line-height: 1로 leading 제거 (구식, 글자가 짤릴 위험).
  3. padding경험적으로 조정 (디자인 시스템의 잘 알려진 함정).

What — 한글 폰트의 metric 함정

한글 폰트는 라틴 폰트보다 세로로 크다.

  • Pretendard ascent=1000, descent=200 → 줄 높이 1.2
  • 그런데 한글 글자 자체는 cap height에 가까운 크기 → 라틴과 섞으면 한글이 더 커 보임.

해결

:root {
  /* 한글 본문은 line-height를 더 줌 */
  line-height: 1.6;  /* 라틴 1.5보다 큼 */
}
 
/* 또는 한글에만 다른 폰트 크기 */
.korean {
  font-size: 15px;   /* 라틴 16px보다 작게 — 시각적 균형 */
}

또는 font-size-adjustx-height 비율을 맞춘다:

body {
  font-family: "Inter", "Pretendard", sans-serif;
  font-size-adjust: ex-height 0.5;  /* x-height = 0.5 × font-size 강제 */
}

→ Inter와 Pretendard가 시각적으로 같은 크기로 보임.


What-if — 잘못 쓰면

1) line-height 단위 누락 의도

:root { font-size: 16px; line-height: 1.5em; }  /* 24px 상속 */
.small { font-size: 12px; /* line-height는 여전히 24px */ }

→ 작은 글자가 지나치게 띄워짐. 항상 단위 없는 숫자.

2) Web font 없이 size-adjust 미설정

@font-face {
  font-family: "Inter";
  src: url("/inter.woff2") format("woff2");
  font-display: swap;
}
body { font-family: "Inter", Arial, sans-serif; }
  • Inter 로드 전: Arial로 그림.
  • Arial은 Inter보다 글자 폭이 7% 좁다.
  • 로드되면 텍스트가 재배치 — 카드 높이 변동, CLS 0.15+.

→ Inter Fallback을 size-adjust로 정의.

3) vertical-align 오용

button {
  display: inline-flex;
  align-items: center;
}
/* 글자가 가운데 정렬됐는데도 위로 치우쳐 보인다 */

→ 글자 자체의 ascent/descent 비대칭 때문. text-box-trim 또는 경험적 padding 으로 해결.

4) vh 단위로 line-height

h1 { line-height: 10vh; }  /* 망함 */

→ 작은 화면에선 글자가 겹친다. line-height에는 무차원 숫자.


Insight — 흥미로운 이야기

“폰트의 metric 값은 1980년대 PostScript 표준에서 왔다”

1985년 Adobe의 PostScript Type 1 폰트 포맷에 ascent, descent, line-gap 개념이 처음 등장했다. 이게 1991년 TrueType (Apple+Microsoft), 1996년 OpenType (Adobe+Microsoft)으로 이어졌고, 지금까지 바꿀 수 없는 폰트 데이터로 남았다. 그래서 같은 디자이너가 만든 폰트라도 Type 1 시대의 값이 그대로인 경우가 많다. Times New Roman의 비대칭(ascent 1825 / descent 443)은 1932년 활자 디자인의 결과를 그대로 디지털 복원한 것이고, 이게 CSS의 line-height: normal에 1.15라는 어색한 값을 만든다. CSS는 80년 묵은 활자 표준2020년대 디자인 시스템을 동시에 짊어진다.

“leading-trim은 Microsoft가 처음 제안했다”

2020년 Microsoft Edge 팀의 leading-trim 제안이 CSS Inline Layout 3에 들어갔다. 디자이너들이 “Figma 8px이 CSS 12px이 되는” 문제를 30년간 padding 마이너스 값으로 hack했던 걸 표준이 해결한 순간이었다. Apple의 SF Symbols·iOS 디자인은 이미 내부적으로 leading-trim을 자동 적용하고 있었고, Microsoft가 웹에 같은 능력을이라며 제안. 2024년 Chrome·Safari가 구현했고, 곧 디자이너의 Figma 수치가 진짜로 CSS와 일치하게 된다.


요약 + Mermaid

  • 한 줄 높이 = (ascent + descent + line-gap) / UPM × font-size.
  • line-height는 항상 단위 없는 숫자 — em/px는 상속 깨짐.
  • size-adjust·ascent-override로 fallback metric을 web font에 맞춤 → CLS 0.
  • text-box-trim·leading-trim으로 Figma와 CSS의 반-leading 격차 해결 (모던).
  • 한글은 ascent가 커서 라틴보다 큰 line-height 또는 font-size-adjust로 균형.

다음 문서 → 04-variable-fonts: 한 파일로 모든 weight·width를 표현하는 모던 폰트 기술.