Engineering note · CSS

웹 타이포그래피 심연: 폰트 매트릭스와 line-height, vertical-align의 원리

2026년 07월 21일이현수
태그csstypographyhtml

font-size와 line-height가 실제로 그려지는 원리인 폰트 매트릭스(Ascender, Descender)를 이해하고, vertical-align의 동작 원리와 다국어 폰트(unicode-range) 적용 방법을 정리한다.

참고한 글

line-heightvertical-align을 사용할 때 텍스트가 미세하게 위아래로 치우치거나, 같은 font-size여도 폰트 종류를 바꾸면 높이와 정렬이 완전히 어그러지는 문제가 있었다. 브라우저가 글자를 렌더링할 때 사용하는 폰트 매트릭스의 동작 방식이 있었고 찾아봤다.

폰트 매트릭스(Font Metrics)와 em-square

폰트 매트릭스 다이어그램

웹에서 font-size는 실제 글자의 픽셀 크기가 아니었다. 폰트 디자이너가 만든 보이지 않는 상자(em-square 또는 UPM)가 있는데, 이 박스의 크기를 의미한다.

폰트 구조 매트릭스

폰트 내부에는 글자를 그리기 위한 수치 정보가 들어 있다.

  • Ascender: 기준선(Baseline) 위로 솟아오른 영역의 높이
  • Descender: 기준선 아래로 내려간 영역의 높이(소문자 g, p, y 등의 꼬리)
  • Capital Height: 대문자의 순수한 높이
  • x-height: 소문자 x의 높이
  • Line Gap: 폰트 디자이너가 권장하는 기본 줄 간격

폰트 디자이너마다 Ascender와 Descender 비율을 다르게 설계한다. 따라서 font-size가 같아도 폰트가 다르면 화면에 렌더링되는 실제 글자 높이와 기준선이 모두 달라진다.

Ascender와 Descender 비율 수치

폰트의 정보를 보면 em-squre 1000에서 ascender 1100 + descender 540 을 더해 1640 / 10 을 font-size 100px 에서 크기를 정의한다.

폰트 영역 비교

line-height와 높이 계산

html
<p>
    Good design will be better.
    <span class="a">Ba</span>
    <span class="b">Ba</span>
    <span class="c">Ba</span>
    We get to make a consequence.
</p>

Content-area와 Line-box

텍스트 영역은 두 가지 박스로 나뉜다.

  • Content-area: 폰트 매트릭스(Ascender + Descender)로 인해 글자가 차지하는 눈에 보이지 않는 높이.
  • Line-box: line-height 만큼 차지하는 박스이고 실제 텍스트 줄의 최종 높이를 결정한다.

line-box 높이 계산

line-boxes 는 같은 줄의 글자 중 가장 큰 line-height 로 높이가 설정된다. c class 의 Ba 때문에, 두번째줄의 line-box 높이가 다른 애들보다 커진 것.

line-height에서 content-area를 뺀 나머지 공간을 행간(Leading)이라고 한다. 이 여분의 공간은 content-area 위와 아래에 반씩 공평하게 분배된다.

인라인 요소의 padding과 line-box

인라인 요소의 padding, margin, border는 시각적인 배경 영역은 늘려주지만 line-box의 높이와는 상관 없다. 레이아웃 높이에 여백을 반영하려면 요소를 inline-block으로 선언해야 함.

line-height : normal

line-height normal 계산

line-height : normalcontent-area와 동일한 값이다. 보통 1640 단위 또는 1.64라고 보면 된다.

vertical-align

vertical-align baseline 정렬

vertical-align의 기본값은 baseline이다. 텍스트는 이 기준선에 맞춰 정렬된다. 같은 font-family, font-size, line-height 를 가진 경우에는 line-box와 line-height가 완전히 일치하고, 배치에도 문제가 없다. 하지만 위 값들이 달라졌을 때, 위 사진은 두번째 요소의 font-size를 반토막냈을 때 결과이다. line-height 보다도 line-box의 크기가 커져버렸다. line-height 는 동일한 상태로, 정렬이 baseline에 맞춰져서 아래행간이 증가했기 때문이다.

font-size 변경에 따른 baseline 변화

가운데 정렬을 위해 vertical-align: middle을 주로 사용하지만, 실제로는 정확한 중앙에 위치하지 않는다. W3C 스펙에 따르면 middle은 부모 박스의 baseline에 부모 폰트의 x-height 절반을 더한 지점에 자식 박스의 중간점을 정렬한다. 폰트마다 x-height 비율이 다르기 때문에 우리가 생각하는 절대적인 중앙과 오차가 생긴다.

이 오차를 보정하기 위해 CSS 커스텀 속성(calc)으로 폰트의 Ascender와 Descender 수치를 역산하는 방법도 있지만, 폰트 로드 실패로 폴백(Fallback) 폰트가 적용될 때 계산이 모두 틀어지는 문제가 있다. 따라서 정확한 수직 중앙 정렬이 필요할 때는 Flexbox(align-items: center)를 사용하는 것이 안전하다.

정확히 높이가 100px인 글자 표현

css
p {
  /* 폰트 매트릭스 수치 (예: Catamaran) */
  --font: Catamaran;
  --fm-capitalHeight: 0.68;
  --fm-descender: 0.54;
  --fm-ascender: 1.1;
  --fm-linegap: 0;

  /* 목표 대문자 높이 */
  --capital-height: 100;
  font-family: var(--font);

  /* 대문자 높이 100px 기준 font-size 계산 */
  --computedFontSize: (var(--capital-height) / var(--fm-capitalHeight));
  font-size: calc(var(--computedFontSize) * 1px);

  /* 수직 중앙 정렬을 위한 여백 계산 */
  --distanceBottom: var(--fm-descender);
  --distanceTop: (var(--fm-ascender) - var(--fm-capitalHeight));
  --valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));
}

span {
  vertical-align: calc(var(--valign) * -1px);
}

대문자의 순수한 높이를 정확히 100px로 맞추려면 font-size: 100px을 지정하는 것이 아니라, 폰트 매트릭스의 Capital Height 비율로 100px을 나눈 font-size를 역산해야 한다.

--capital-height 변수에 원하는 대문자 높이(100)를 입력하고 폰트 고유의 Capital Height 비율(--fm-capitalHeight)로 나누면 필요한 font-size가 구해진다. 상하 여백의 차이(--distanceBottom--distanceTop)를 계산해 vertical-align까지 수식으로 보정하면, 글자 상하 여백을 완벽하게 등분할 수 있다.

100px 글자 수치 계산

다만 이 방식은 몇 가지 한계가 있다.

  • 폰트 파일이 로드되지 않아 폴백 폰트로 바뀔 때 매트릭스 수치가 어긋나면서 계산된 레이아웃이 모두 틀어질 수 있음
  • 브라우저나 운영체제 렌더링 엔진마다 미세하게 다를 수 있음.. 이건 어쩔 수 없는듯

추천하는 방법인 아니고, 글자 단독 수직 중앙 정렬은 flex, grid 의 중앙 정렬을 사용하는 게 편함!

참고 자료