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 이해 부족에서 온다.
- 버튼 안의 글자가 위로 치우침 — 폰트의 ascent가 descent보다 훨씬 커서 라인박스 중앙이 글자 중앙과 다르다.
- 카드 높이가 폰트 로드 후 점프 — fallback 폰트와 web font의 metric이 달라 줄 높이 변동.
- 디자이너의 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 |
| ascent | baseline에서 위쪽 한계까지 (UPM 단위) |
| descent | baseline에서 아래쪽 한계까지 (UPM 단위, 음수일 수도) |
| line-gap | 줄 간 여백 (UPM 단위) |
| cap-height | 대문자 H의 위쪽 |
| x-height | 소문자 x의 위쪽 |
실제 폰트 값 예 (UPM 1000 기준)
| 폰트 | ascent | descent | line-gap | x-height |
|---|---|---|---|---|
| Inter | 968 | 232 | 0 | 540 |
| Roboto | 1900 | 500 | 0 | 528 (UPM 2048 기준) |
| Pretendard | 1000 | 200 | 0 | 555 |
| Times New Roman | 1825 | 443 | 87 | 916 (UPM 2048) |
| Helvetica | 1577 | 471 | 0 | 1099 (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 수치를 그대로 옮기면 실제 화면은 더 비어 보인다. 해결책:
text-box-trim사용 (모던).line-height: 1로 leading 제거 (구식, 글자가 짤릴 위험).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-adjust로 x-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를 표현하는 모던 폰트 기술.