한글 웹 타이포그래피 (7) 한영 혼용 조판

6편에서 값은 다 나왔다. 서체, 크기, 굵기, 행간, 자간, measure, 줄끝, 문단 간격, 대비까지 하나의 CSS 변수 블록으로 조립했고, 그걸 복붙하면 한글 본문 조판은 끝난다. 그런데 실제 글을 쓰다 보면 한글만 있는 문단은 거의 없다. React 18이 나오고, 2026년이 지나가고, word-break 같은 코드가 인라인으로 박힌다. 한글 사이사이에 라틴 글자와 숫자가 섞인다.
이 마지막 글은 그 섞임을 다룬다. 앞의 여섯 편이 한글 본문 자체의 값을 정하는 일이었다면, 이건 그 위에 얹는 마감이다. 없어도 글은 읽히지만, 있으면 한 끗이 다르다.
같은 px인데 라틴이 작아 보이는 이유
먼저 눈에 걸리는 문제부터 보자. 한글 문장 안에 라틴 단어를 같은 font-size로 넣으면, 라틴 쪽이 한글보다 살짝 작고 아래로 처져 보인다. 분명히 둘 다 17px인데 그렇다.
이건 착시가 아니라 글자를 담는 방식이 달라서 생기는 실제 차이다. 한글은 네모틀 글자라 활자 한 칸(em 사각형)을 거의 꽉 채우도록 설계된다. 반면 라틴은 그 칸 안에서 소문자의 몸통 높이, 곧 엑스하이트(x-height)가 칸의 절반을 조금 넘는 정도다. g나 y처럼 기준선 아래로 내려가는 획(디센더)과 대문자 위쪽 여백까지 칸 안에 품어야 하니까, 소문자 몸통은 그만큼 작게 들어간다. 같은 17px이어도 한글은 칸을 꽉 쓰고 라틴 소문자는 칸의 일부만 쓰니, 나란히 놓으면 라틴이 작아 보이는 게 당연하다.
기준선(baseline)도 문제다. 라틴 글자는 기준선 위에 올라앉고, 그 아래로 디센더가 걸린다. 한글은 네모 칸을 기준으로 정렬돼서, 두 문자를 한 줄에 섞으면 라틴의 기준선과 한글 칸의 아랫변이 딱 맞지 않는다. 그래서 라틴 단어가 한글보다 살짝 아래로 가라앉은 것처럼 보이는 것이다.
여기서 정직하게 짚자. 이건 라틴을 몇 퍼센트 키우고 몇 px 올려라 하는 식의, 실증 상수가 있는 문제가 아니다. 폰트마다 엑스하이트와 기준선 위치가 달라서 보정값도 폰트마다 다르다. 그러니 규칙보다 감각으로 접근해야 한다. 내가 쓰는 순서는 이렇다.
첫째, 웬만하면 손대지 않는다. 잘 만든 한글 웹폰트는 라틴 글리프도 한글과 어울리게 크기와 높이를 맞춰서 담아둔다. 뒤에서 볼 폰트 스택을 제대로 짜면 이 문제의 대부분이 폰트 안에서 이미 해결돼 있다. 그러니 첫 선택은 항상 그냥 두는 것이다.
둘째, 그래도 라틴이 확연히 작아 보이는 큰 제목 같은 자리에서만 국소적으로 보정한다. 예를 들어 h1에 라틴이 섞여 눈에 거슬리면, 그 라틴 부분만 em 단위로 아주 조금 키운다. 본문 스무 줄에는 절대 이 짓을 하지 않는다. 매 단어마다 크기가 튀면 그게 더 눈에 띈다.
/* 큰 제목에서 라틴이 확연히 작을 때만, 국소적으로 아주 살짝 */
.markdown h1 .latin {
font-size: 1.03em; /* 3%가 한계. 본문에는 쓰지 않는다 */
}
여기서 .latin은 라틴 부분을 직접 감싼 클래스다. 자동으로 붙는 게 아니라 마크업에서 일일이 감싸야 하니, 그 수고를 들일 값어치가 있는 제목 한두 곳에만 쓴다.
기준선 보정도 마찬가지다. CSS에 vertical-align으로 라틴만 몇 px 올리는 방법이 있긴 한데, 본문에서 이걸 손으로 잡기 시작하면 끝이 없다. 본문 기준선 정렬은 폰트에 맡기고, 사람 손은 제목 한두 곳에서 멈추는 게 맞다.
폰트 스택 순서가 하는 일
크기와 기준선 문제의 실질적인 해법이 결국 폰트 스택이다. 이 블로그의 실제 본문 스택은 이렇다.
:root {
--font-sans: "Geist", "Pretendard Variable", Pretendard,
-apple-system, sans-serif;
}
라틴 폰트인 Geist가 맨 앞에 오고, 한글 폰트인 Pretendard가 그 뒤에 온다. 이 순서가 우연이 아니다.
font-family의 목록은 흔히 오해하듯 이 폰트 없으면 다음 폰트 하는 식의 단순 대체 목록이 아니다. 브라우저는 이걸 글자 하나하나마다 다시 훑는다. 어떤 글자를 그릴 때, 목록 맨 앞 폰트부터 보면서 그 폰트에 이 글자의 글리프가 있으면 그걸로 그리고, 없으면 다음 폰트로 넘어간다. 폴백(fallback)이 요소 단위가 아니라 문자 단위로 일어난다.
그래서 React 18을 쓴다라는 한 문장을 이 스택으로 그리면 이렇게 갈린다. R, e, a, c, t, 공백, 1, 8은 Geist에 다 있으니 Geist가 그린다. 을, 쓴, 다 같은 한글은 Geist에 없다. Geist는 라틴 폰트라 한글 글리프를 안 담았으니, 브라우저가 다음 후보인 Pretendard로 넘어가서 한글을 그린다. 한 문장 안에서 라틴은 Geist, 한글은 Pretendard가 그린다. 각 문자를 그 문자에 맞게 설계된 폰트가 그리는 것이다.
순서를 뒤집어 Pretendard를 앞에 두면 어떻게 될까. Pretendard는 한글 폰트지만 라틴 글리프도 함께 담고 있어서, 라틴까지 전부 Pretendard가 그려버린다. 이게 나쁘다는 게 아니다. Pretendard의 라틴도 잘 빠졌다. 다만 라틴 전용으로 다듬어진 Geist의 라틴을 쓰고 싶어서 스택을 짠 거라면, 순서가 뒤집히는 순간 그 의도가 통째로 사라진다. 라틴은 라틴 전용 폰트에 맡기고 싶으니 라틴 폰트를 앞에, 그 폰트에 없는 한글은 한글 폰트가 받도록 뒤에 둔다. 이 방향이 원칙인 이유는 단순하다. 대부분의 라틴 폰트는 한글을 담지 않지만, 대부분의 한글 폰트는 라틴을 담기 때문이다. 라틴 폰트를 앞에 둬도 한글이 안 그려질 걱정은 없다. 뒤의 한글 폰트가 반드시 받아준다.
앞 절에서 본 크기·기준선 문제가 여기로 이어진다. Geist의 라틴과 Pretendard의 한글은 서로 어울리도록 크기감이 얼추 맞는 조합이라, 스택만 이렇게 제대로 짜두면 혼용 크기 문제가 폰트 층위에서 상당히 눌린다. 스택 순서가 곧 혼용 조판의 첫 단추다.
숫자와 영문이 섞인 본문
혼용에서 실무적으로 제일 자주 만나는 게 숫자다. 2026년, React 18, 4.5:1 대비, 680px. 한글 기술 글은 숫자와 라틴이 계속 끼어든다. 여기서 걸리는 지점 두 개만 짚는다.
첫째는 폭이다. 라틴 폰트의 숫자는 보통 글자마다 폭이 다른 비례폭(proportional)으로 나온다. 1은 좁고 0은 넓다. 본문에 흘려 쓸 때는 이게 자연스럽지만, 숫자를 세로로 줄 맞춰 쌓는 표나 코드 블록에서는 자리가 안 맞아 지저분해진다. 그럴 때만 등폭 숫자(tabular figures)를 켜서 모든 숫자의 폭을 똑같이 맞춘다. 본문에는 굳이 켜지 않는다. 이건 표와 숫자 정렬이 필요한 자리에서만 쓰는 국소 도구다.
/* 숫자를 세로로 줄 맞춰야 하는 표에서만 */
.markdown table {
font-variant-numeric: tabular-nums;
}
둘째는 줄바꿈이다. 4편에서 word-break: keep-all로 한글을 어절 단위로 끊기로 정했다. 그런데 React 18 같은 덩어리는 브라우저가 라틴과 숫자 사이를 끊을 수 있는 지점으로 볼 여지가 있어서, 줄 끝에 걸리면 React와 18이 위아래 줄로 갈라질 수 있다. 이런 짝은 갈라지면 안 되니 사이를 줄바꿈 없는 공백( )으로 묶어준다. 버전 표기, 단위가 붙은 수치처럼 한 덩어리로 읽혀야 하는 짝에만 쓰면 된다. 남발하면 오히려 줄 끝이 들쭉날쭉해진다.
원리는 4편 줄끝 규칙의 연장이다. 한글은 어절로 끊고, 라틴·숫자 덩어리는 그 덩어리가 깨지지 않게 지킨다. 이 두 개만 신경 써도 숫자 섞인 본문이 훨씬 정돈된다.
부록: 웹폰트를 어떻게 불러올까
여기까지가 혼용 조판이고, 마지막으로 폰트를 실제로 불러오는 이야기를 가볍게만 얹는다. 이건 조판보다 성능 쪽 주제라 깊게는 안 간다. 감만 잡아두자.
한글 웹폰트는 무겁다. 라틴 폰트는 글리프가 수백 개면 되지만, 한글은 현대 음절만 11,172자에 기호까지 하면 폰트 하나가 수 MB로 불어난다. 그래서 두 가지를 챙긴다.
하나는 로딩 중에 글자를 어떻게 보여줄지다. 폰트가 다 내려오기 전까지 브라우저는 글자를 안 그리고 기다리거나(FOIT), 시스템 폰트로 먼저 그렸다가 웹폰트가 도착하면 바꿔 그린다(FOUT). 본문은 일단 읽히는 게 우선이니 font-display: swap으로 시스템 폰트로 먼저 그리게 둔다. 텍스트가 안 보이는 시간을 없애는 대신, 폰트가 바뀌는 순간 글자 폭이 달라지면서 레이아웃이 툭 밀리는 현상(CLS)을 감수한다. 이 밀림을 줄이려면 폴백 폰트의 크기감을 웹폰트와 비슷하게 맞춰주는 손질이 필요한데, 그건 성능 튜닝의 영역이라 여기선 이름만 걸어둔다.
다른 하나는 용량이다. 한글 폰트 전체를 다 내려받게 두면 무겁다. 그래서 서브셋(subset)을 쓴다. 실제로 페이지에 나오는 글자, 또는 자주 쓰이는 음절 집합만 잘라낸 작은 폰트를 만들어 내려받게 하는 것이다. Pretendard도 필요한 글자만 조각내 받는 동적 서브셋 방식을 제공해서, 전체를 통째로 받는 것보다 훨씬 가볍게 쓸 수 있다.
정리하면 부록의 결론은 두 줄이다. 로딩 중에는 font-display: swap으로 본문을 먼저 읽히게 하고, 용량은 서브셋으로 줄인다. 나머지 세밀한 성능 최적화는 이 시리즈의 범위 밖이다.
시리즈를 닫으며
일곱 편을 여기서 닫는다.
값은 6편에서 이미 다 나왔다. 서구 규칙을 한글에 그대로 쓰면 왜 어긋나는지에서 시작해서(1편), 서체(2편), 크기·굵기·대비(3편), 줄 길이와 줄끝(4편), 자간·행간·문단 간격(5편)을 하나하나 정하고, 그걸 한 덩어리 CSS 변수 블록으로 조립했다(6편). 그 블록만 있으면 한글 본문 조판은 끝난다.
7편의 혼용은 그 위에 얹는 마감이었다. 라틴이 작아 보이는 건 대개 폰트에 맡기고 제목에서만 손대고, 스택은 라틴 먼저 한글 나중으로 짜서 각 문자를 맞는 폰트가 그리게 하고, 숫자 덩어리는 갈라지지 않게 지킨다. 웹폰트는 swap으로 먼저 읽히고 서브셋으로 가볍게 부른다. 이게 마감의 전부다.
이 시리즈를 관통한 태도 하나만 남긴다. 값을 외우지 말고 왜 그 값인지를 쥐라는 것이다. 17px도 1.8도 40자도 절대 상수가 아니라 한글 글자 구조에서 유도한 출발선이었고, 실증 상수인지 조판 관습인지를 매번 구분해서 말했다. 그래서 폰트가 바뀌고 화면이 바뀌어도 스스로 다시 값을 찍어낼 수 있다. 그게 이 일곱 편으로 남기고 싶었던 것이다.