한글 웹 타이포그래피 (9) 줄상자의 해부
시리즈 한글 웹 타이포그래피 9 / 11
- 1한글 웹 타이포그래피 (1) 서구 규칙을 그대로 쓰면 안 되는 이유
- 2한글 웹 타이포그래피 (2) 명조와 고딕, 화면에서 무엇을 언제
- 3한글 웹 타이포그래피 (3) 글자 크기와 굵기, 그리고 본문 대비
- 4한글 웹 타이포그래피 (4) 글 길이와 줄끝 처리
- 5한글 웹 타이포그래피 (5) 자간·행간과 문단 간격
- 6한글 웹 타이포그래피 (6) 위계와 타입스케일: 본문에서 값을 뽑아 조립하기
- 7한글 웹 타이포그래피 (7) 웹폰트 로딩과 성능
- 8한글 웹 타이포그래피 (8) 속공간과 커닝
- 9한글 웹 타이포그래피 (9) 줄상자의 해부
- 10한글 웹 타이포그래피 (10) 문장부호가 데려오는 사이 공간
- 11한글 웹 타이포그래피 (11) 폴백 폰트가 밀어내는 줄

5편에서 행간을 1.8로 정했고, 8편에서 왜 그렇게까지 벌리는지를 세로 속공간으로 설명했다. 그런데 우리가 line-height: 1.8이라고 쓸 때, 브라우저는 정확히 무엇을 하는가. 그 숫자 하나가 화면에서 어떻게 ‘픽셀’로 번역되는가. 이 편은 그 번역 과정을 픽셀 단위로 해부한다. 시리즈에서 제일 기계적이고 제일 어렵다. 대신 이걸 통과하면, 오래 괴롭혔던 두 가지가 한꺼번에 풀린다. 버튼 안의 한글이 왜 늘 살짝 위로 떠 보이는지, 그리고 문단 맨 위 글자와 컨테이너 사이에 왜 정체불명의 공백이 생기는지.
결론부터 말하면, line-height는 ‘줄 간격’이 아니다. 그건 줄상자(line box)를 그리는 규칙이고, 그 상자의 실제 치수는 폰트가 자기 안에 들고 있는 세로 자(font metrics)가 절반쯤 정한다. CSS 값과 폰트 파일이 협상해서 픽셀이 나온다. 이 협상 테이블을 들여다보자.
줄 하나는 상자다
브라우저가 인라인 텍스트를 배치할 때, 한 줄은 그냥 글자들의 나열이 아니라 하나의 상자다. 이걸 줄상자라 부른다. 그 줄에 놓인 각 인라인 조각(글자 런, 인라인 이미지 등)은 저마다 콘텐츠 영역이라는 세로 높이를 갖고, 줄상자의 높이는 그 조각들을 담아낸 결과로 정해진다.
핵심은 이거다. 텍스트 한 조각의 콘텐츠 영역 높이는 글자 크기(font-size)만으로 정해지지 않는다. font-size는 em 사각형의 크기를 정할 뿐이고, 그 안에서 글자가 세로로 얼마를 차지하는지는 폰트 파일이 따로 들고 있는 치수가 정한다. 그래서 같은 font-size: 16px라도 폰트를 바꾸면 줄 높이가 달라진다. 폰트가 세로 자를 바꿔 쥐기 때문이다.
폰트가 들고 있는 세로 자
폰트 파일 안에는 좌표계가 하나 있다. 그 단위가 em인데, 폰트마다 1em을 몇 칸으로 쪼갤지가 정해져 있다(unitsPerEm, 보통 1000 또는 2048). 이 좌표계 위에 폰트는 자기 세로 치수를 적어둔다.
- 어센트(ascent): 베이스라인 위로 글자가 뻗는 높이. 라틴의
b,l,h꼭대기 언저리. - 디센트(descent): 베이스라인 아래로 뻗는 깊이.
g,y,p의 꼬리. - 라인갭(line gap): 폰트가 권장하는 줄과 줄 사이 여분.
- 베이스라인(baseline): 글자가 앉는 기준선.
브라우저가 텍스트 조각의 콘텐츠 영역을 그릴 때 쓰는 높이가 대략 이 어센트 + 디센트다. 여기서 8편의 이야기가 픽셀로 확인된다. 한글은 네모틀을 위아래로 꽉 채우므로, 잘 만든 한글 폰트는 어센트와 디센트를 라틴보다 크게 잡아 내보낸다. 그래서 같은 font-size라도 한글 폰트의 콘텐츠 영역이 더 크고, line-height: normal이 더 크게 나온다. 세로 속공간이 작다는 말은, 메트릭으로 옮기면 어센트·디센트가 em을 거의 다 먹는다는 뜻이다.
여기 첫 번째 함정이 있다. 폰트에는 세로 치수 표가 하나가 아니라 여러 벌 들어 있다. hhea 테이블의 ascent·descent·lineGap, OS/2 테이블의 sTypoAscender·sTypoDescender·sTypoLineGap(이른바 타이포 메트릭), 그리고 같은 OS/2의 usWinAscent·usWinDescent(윈 메트릭). 셋의 값이 서로 다른 경우가 흔하다. 브라우저는 이 중 하나를 골라 콘텐츠 영역과 줄 높이를 계산하는데, 어느 표를 읽느냐가 브라우저마다 달랐다. 같은 폰트, 같은 CSS인데 크롬과 사파리와 파이어폭스에서 줄 높이가 미묘하게 어긋나던 오랜 버그의 뿌리가 이것이다. 요즘 엔진들은 타이포 메트릭 쪽으로 수렴하는 편이고(폰트의 fsSelection에 USE_TYPO_METRICS 비트가 켜져 있으면 특히 그렇다. 크로미움은 콘텐츠 높이에 타이포 메트릭을 쓴다), 그래서 웹폰트를 만들 때 이 비트와 세 표의 정합성을 맞추는 게 크로스브라우저 조판의 숨은 전제가 된다.
line-height: normal의 정체
이제 line-height가 이 그림에 어떻게 얹히는지 보자. 값이 normal이면, 줄 높이는 폰트가 준 어센트 + 디센트 + 라인갭으로 정해진다. 폰트가 자기 라인갭을 얼마로 적었느냐에 따라 normal이 폰트마다 다르다는 뜻이다. 어떤 폰트는 normal이 1.15처럼 나오고 어떤 폰트는 1.5처럼 나온다. normal은 상수가 아니라 폰트에게 물어보는 값이다.
반면 숫자(line-height: 1.8)를 주면 이야기가 달라진다. 이때는 폰트의 라인갭을 무시하고, 줄 높이를 곧장 font-size × 1.8로 못박는다. 단위 없는 숫자를 권하는 이유도 여기 있다. 1.8은 자식마다 자기 font-size에 곱해지지만, 28.8px 같은 고정값은 부모 기준으로 계산돼 자식으로 그대로 상속되면서 작은 글자에서 줄이 끼거나 큰 글자에서 겹친다. 배수로 주면 각 요소가 제 크기에 맞는 줄 높이를 다시 계산한다.
반행간: 남는 공간을 반씩 나눈다
여기가 이 편의 심장이다. line-height로 정한 줄 높이가 폰트의 콘텐츠 영역(어센트+디센트)보다 크면, 그 차이만큼 빈 공간이 남는다. 이 남는 공간을 **반행간(leading)**이라 하고, 브라우저는 이걸 위아래로 절반씩 나눠 붙인다.
반행간(leading) = line-height − (어센트 + 디센트)
위 반행간 = 반행간 / 2 (콘텐츠 영역 위에 붙음)
아래 반행간 = 반행간 / 2 (콘텐츠 영역 아래에 붙음)
8편에서 “행간을 키우면 줄 위아래가 고르게 벌어진다"고 했던 것의 기계적 정체가 이 반씩 나누기다. line-height를 키운다는 건 반행간을 키워 콘텐츠 영역 위아래에 똑같이 바르는 일이다. 그래서 줄과 줄 사이만 벌어지는 게 아니라, 각 글자의 위아래로 균등하게 숨통이 트인다.
그리고 이 반씩 나누기가 두 번째 수수께끼를 푼다. 첫 줄에도 위 반행간이 그대로 붙는다. 그래서 문단을 감싼 컨테이너의 맨 위 경계와 첫 글자의 잉크 사이에는, 아무 margin도 안 줬는데 반행간만큼의 빈틈이 생긴다. 카드 안 텍스트를 위로 딱 붙이려고 음수 마진을 때려 넣어 본 적이 있다면, 그때 싸우던 상대가 바로 이 반행간이다.
왜 세로 가운데가 안 맞는가
이제 버튼 문제다. 높이가 고정된 버튼 안에 한글 텍스트를 넣으면, 눈에는 글자가 살짝 위로 떠 보인다. 아래 여백이 위 여백보다 넓다. line-height를 아무리 만져도 안 고쳐진다. 왜인가.
두 가지가 겹친다. 첫째, 폰트의 어센트와 디센트는 대개 비대칭이다. 베이스라인 위(어센트)가 아래(디센트)보다 크다. 그래서 콘텐츠 영역의 기하학적 중심과, 글자 잉크가 실제로 앉는 시각적 중심이 어긋난다. 둘째, 반행간은 위아래로 똑같이 나뉜다. 비대칭인 글자에 대칭인 여백을 발라봐야 중심은 안 옮겨진다. 오히려 어긋남이 그대로 보존된다. 한글은 메트릭이 위로 꽉 찬 만큼 이 어긋남이 라틴보다 도드라져서, 버튼·뱃지·인풋처럼 세로 중앙이 중요한 자리에서 유독 티가 난다.
여태 우리가 쓰던 우회로는 padding을 위아래 다르게 줘서 눈속임하는 것이었다. 하지만 이건 그 폰트, 그 크기에만 맞는 미봉책이다. 폰트를 바꾸면 메트릭이 바뀌어 다시 어긋나고, 웹폰트가 로딩되기 전 폴백 폰트일 때 또 어긋난다. 근본 문제는 우리가 만지는 상자(줄상자)가 글자의 실제 잉크가 아니라 폰트 메트릭 여백까지 포함한 상자라는 데 있었다.
상자를 직접 자른다: text-box-trim
그래서 CSS에 상자 자체를 잘라내는 속성이 새로 생겼다. 처음엔 leading-trim이라는 이름이었고, 지금은 text-box-trim과 text-box-edge(단축 text-box)로 표준화됐다. 반행간과 메트릭 여백을 걷어내 상자를 실제 글자에 맞추는 도구다.
/* 라틴 UI: 위는 대문자 꼭대기, 아래는 베이스라인까지 잘라
잉크에 딱 맞는 상자로 만든다 */
.btn {
text-box-trim: trim-both; /* none | trim-start | trim-end | trim-both */
text-box-edge: cap alphabetic; /* 위 엣지 | 아래 엣지 */
}
/* 단축 한 줄로도 같다 */
.btn { text-box: trim-both cap alphabetic; }
text-box-trim은 어느 쪽 반행간을 걷을지 정한다(trim-both면 위아래 다). text-box-edge는 어디까지 자를지를 정하는데, 위 엣지는 text(어센트) · cap(대문자 꼭대기) · ex(엑스 높이) 중에서, 아래 엣지는 text(디센트) · alphabetic(베이스라인) 중에서 고른다. cap alphabetic은 라틴 대문자 위부터 베이스라인까지, 곧 라틴 잉크의 실제 위아래에 상자를 맞추라는 뜻이다.
여기서 한글은 조심해야 한다. cap·ex·alphabetic은 전부 라틴의 기준선이다. 한글에는 대문자 꼭대기도, 라틴식 베이스라인도 그대로 대응되지 않는다. 한글·CJK를 위한 엣지 키워드로 ideographic과 ideographic-ink이 명세에는 들어 있지만, 2026년 현재 이걸 구현한 브라우저는 아직 없다. 그러니 한글에 cap alphabetic을 그대로 쓰면 의도와 다르게 잘린다. 한글 UI에서는 엣지를 기본값 text(어센트·디센트, 곧 em 상자 기준)로 두고 반행간만 걷어내는 게 지금으로선 가장 안전하다.
/* 한글 UI: 엣지는 기본값(text)으로 두고 반행간만 대칭으로 걷어낸다.
text-box-edge를 생략하면 text가 기본이라 아래 한 줄로 충분하다 */
.btn-ko {
text-box-trim: trim-both;
}
/* 폴백: 아직 지원 안 하는 브라우저(파이어폭스 등)에서는
기존 방식(padding 보정 등)을 유지하도록 분기한다 */
@supports (text-box-trim: trim-both) {
.btn-ko { padding-block: 0.6em; } /* 트림이 되면 위아래 대칭 패딩만 */
}
@supports not (text-box-trim: trim-both) {
.btn-ko { padding-block: 0.72em 0.48em; } /* 트림이 안 되면 비대칭 보정 유지 */
}
지원 범위도 알아두자. text-box-trim 계열은 2025년에 크롬·엣지 133과 사파리 18.2에서 기본 탑재됐고, 파이어폭스는 아직(2026년 기준) 미탑재다. 그래서 이건 “모든 브라우저에서 켜는 기본기"가 아니라, 지원되는 곳에서 정렬을 픽셀로 다듬고 안 되는 곳에선 우아하게 물러서는 점진적 향상(progressive enhancement)으로 쓴다.
실전 정리
정렬 상자를 자를 수 있게 됐다고 해서 아무 데나 자르는 게 아니다. 자리마다 다르다.
본문(장문) : 트림 안 함. 행간은 일부러 넓힌 것이고(5·8편),
반행간이 곧 그 숨통이다. 걷어내면 답답해진다.
UI 조각 : 버튼·뱃지·인풋·칩처럼 세로 중앙이 중요한 자리에서
text-box-trim: trim-both 로 반행간을 걷어 픽셀 정렬.
인접 정렬 : 아이콘 옆 텍스트, 제목과 메타를 baseline으로 맞출 때 트림이 특히 값어치.
한글 : 엣지는 text(기본). cap/alphabetic은 라틴용, ideographic은 아직 미지원.
폴백 : @supports로 분기. 지원 안 되면 기존 padding 보정 유지.
원리는 하나다. 본문은 반행간이 읽기의 숨통이니 그대로 두고, UI는 반행간이 정렬의 오차이니 걷어낸다. 같은 반행간을 한쪽에선 살리고 한쪽에선 죽이는 것, 그 판단의 기준은 “이 자리에서 흰 공간이 리듬인가 오차인가"다.
정리
line-height는 줄 간격이 아니라 줄상자를 그리는 규칙이었다. 폰트가 어센트·디센트로 콘텐츠 영역을 정하고, line-height가 남긴 반행간이 그 위아래에 절반씩 발린다. 첫 줄 위 정체불명의 공백도, 버튼 속 한글이 위로 뜨는 것도, 전부 이 상자의 구조에서 필연으로 흘러나온 결과였다.
폰트 메트릭(어센트·디센트) → 콘텐츠 영역
line-height − 콘텐츠 = 반행간 → 위아래 절반씩
한글은 메트릭이 크고 위로 치우침 → 본문은 넓은 행간이 약, UI는 세로 정렬이 어긋남
text-box-trim → 그 상자를 실제 잉크에 맞춰 자름(단, 한글은 text 엣지)
이 시리즈가 내내 한 말을 마지막으로 한 번 더 한다. 값을 외우지 말고 그 값이 그리는 상자를 보라. line-height: 1.8은 숫자가 아니라, 폰트가 준 상자 위에 반행간을 얹어 그리는 한 줄의 부피다. 그 부피의 정체를 알면, 왜 본문에선 넉넉히 두고 UI에선 걷어내는지가 규칙이 아니라 이해로 남는다. 흰 공간을 배열하는 눈, 이번에는 그 눈이 줄상자의 안쪽까지 들어갔다.
이 글, 어떠셨어요?