한글 웹폰트 서브셋과 next/font
이번 포스팅에서는 한글 웹폰트를 가볍게 보내는 방법에 대한 이야기를 해보려고 한다.
본문, 제목, 코드, 인용에 폰트를 나눠 쓰는 한글 페이지를 운영하는 프론트엔드 개발자를 위한 글이다. 웹폰트를 줄이는 여러 방법이 각각 얼마나 효과가 있는지 실제로 재 보고, 어떤 방법부터 손대야 하는지 정리한다.
한글 폰트는 글자 수가 많아서 굵기 하나가 수백 kB 에서 MB 단위다. 이걸 덜 받고, 빨리 받고, 받는 동안 화면이 흔들리지 않게 하는 방법은 여럿이다. 파일 형식을 woff2 로 바꾸거나, 서브셋으로 나누거나, Google 서버에서 직접 받거나, next/font 로 폰트를 미리 받게 하고(preload) 대체 글꼴을 맞추거나, font-display 로 폰트가 늦게 올 때의 화면을 정할 수 있다.
그럼 이 방법들은 실제로 얼마나 차이가 날까? 폰트 네 개를 쓰는 페이지 하나를 만들어 방법마다 따로 배포하고, 폰트를 얼마나 받는지, 다 받기까지 몇 초 걸리는지, 화면이 얼마나 밀리는지를 재 보자. 국내 서비스 다섯 곳이 어떤 방법을 쓰고 있는지도 같이 살펴본다. 데모와 측정 스크립트는 jiji-hoon96/blog-labs 에 있고, 배포한 페이지는 hooninedev-web-fonts.netlify.app 에서 직접 열어 볼 수 있다.
폰트 네 개를 쓰는 페이지
실험 페이지는 이 블로그의 overlay-kit 글 본문을 코드 블록과 인용까지 그대로 옮긴 것이다. 짧은 예제 대신 실제 글 한 편을 고른 이유는, 한글 폰트의 전송량이 페이지에 쓰인 글자의 종류를 따라 움직이기 때문이다. 폰트는 기술 블로그에서 흔히 보는 구성대로 네 개를 나눠 쓴다.

본문은 font-family: 'Inter', 'Noto Sans KR' 이다. 영문과 숫자는 Inter 로, 한글은 Noto Sans KR 로 그리고, 굵기는 300, 400, 500, 700 네 가지를 쓴다. 제목은 명조 계열인 Noto Serif KR 700 하나, 코드는 고정폭인 JetBrains Mono 400 하나다. 인용은 Inter 의 이탤릭으로 그린다. Noto Sans KR 에는 이탤릭이 없어서, 인용 안의 한글은 브라우저가 글자를 기울여 합성한다.
패밀리로는 넷이고, 굵기와 스타일까지 세면 폰트 열한 종류다. Inter 가 다섯(300, 400, 500, 700, 이탤릭 400), Noto Sans KR 이 넷, 나머지 둘이 하나씩이다.
브라우저가 폰트를 받는 규칙
웹폰트는 CSS 를 읽는 순간 내려오지 않는다. @font-face 규칙은 폰트 이름, 굵기, 파일 주소, 그 파일이 담은 글자 범위(unicode-range)를 선언할 뿐이고, 브라우저는 텍스트를 그리다가 그 폰트가 필요해진 시점에 파일을 요청한다. CSS Fonts Level 4 명세는 이 지연 요청을 설계 의도로 적는다. 같은 명세의 unicode-range 절은 범위 밖의 글자에는 그 폰트를 내려받지도 쓰지도 말아야 한다(must not)고 정한다.
그래서 폰트 하나를 글자 범위별 파일로 나누고 파일마다 unicode-range 를 적어 두면, 브라우저는 페이지에 쓰인 글자가 든 파일만 받는다. 이렇게 일부 글자만 잘라 낸 파일을 서브셋(subset)이라고 하고, 이 글에서는 그렇게 나눈 파일 하나하나를 조각이라고 부른다. 아래는 이 실험에서 쓴 조각 하나의 규칙이다. U+ac00(가), U+c774(이), U+d558(하) 같은 자주 쓰는 한글이 공백과 영문 일부와 함께 들어 있다.
@font-face {
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 300 700;
font-display: swap;
src: url(nskr-119.woff2) format('woff2');
unicode-range: U+20-22, U+27-2a, U+2c-38, U+3a-3b, U+3f, U+41-47, U+4a-4c, U+4f-5d, U+61-7b, U+7d, U+a1, U+ab, U+ae, U+b7, U+bb, U+bf, U+2013-2014, U+201c-201d, U+2122, U+ac00, U+ace0, U+ae30, U+b2e4, U+b85c, U+b9ac, U+c0ac, U+c2a4, U+c2dc, U+c774, U+c778, U+c9c0, U+d558;
}
한글 폰트에서는 이 차이가 클 수밖에 없다. 한글 음절만 11,172자이고, Noto Sans KR 은 한자까지 담아 23,174자가 들어 있다. 서브셋이 없으면 글 한 편을 그리려고 이 전부를 받는다.
비교한 다섯 페이지
앞에서 든 방법 중 형식과 서브셋부터 본다. 같은 페이지를 폰트 전달 방식만 바꿔 다섯 벌 만들었고, Google 이 이미 나눠 둔 조각을 그대로 쓰는 경우도 넣었다. 본문과 font-family 선언은 다섯 벌이 모두 같고, 바뀌는 것은 fonts.css 한 파일과 그 파일이 가리키는 폰트 파일뿐이다.
출발점은 Google Fonts CSS API 다. Google Fonts 문서는 이 API 가 요청한 브라우저에 맞춘 스타일시트를 돌려준다고 적는다. curl 처럼 브라우저로 알아보지 못하는 클라이언트가 요청하면 굵기마다 TTF 주소가 하나씩 오고, 최신 Chrome 이 요청하면 조각마다 @font-face 가 붙은 woff2 응답이 온다. 앞의 응답에서 TTF 주소만 뽑아 CSS 에 고정해 두면 어떤 브라우저로 접속해도 TTF 를 받는다.
- A. TTF: API 가 curl 에 주는 TTF 파일을 그대로 받아 호스팅했다. TTF 주소를 고정해 둔 경우와 같은 모양이다.
- B. woff2: A 의 TTF 를 woff2 로 바꾸기만 했다. 서브셋은 없다.
- C. 정적 조각: A 의 TTF 를 Google 이 패밀리마다 쓰는 글자 범위대로 잘라 woff2 조각으로 만들었다. 굵기마다 따로 자른다.
- D. Google 응답: 같은 API 가 최신 Chrome 에 주는 CSS 와 폰트 파일을 그대로 받아 호스팅했다. 조각 하나가 여러 굵기를 담은 가변 폰트다.
- D-range: D 와 같지만 API 에 굵기를
wght@300;400;500;700처럼 나열하지 않고wght@300..700처럼 범위로 요청했다.
A, B, C 는 같은 TTF 에서 출발한다. A 에서 B 로 가며 형식만, B 에서 C 로 가며 서브셋만 바뀌므로 각각 얼마를 줄였는지 나눠 볼 수 있다. 변환과 자르기에는 폰트 파일을 다루는 Python 라이브러리 fontTools 를 썼다. 다만 Google 의 범위는 폰트가 담은 글자를 다 덮지 않는다. Noto Sans KR 은 23,174자 중 17,655자만 덮고, 빠진 것은 대부분 드물게 쓰는 한자다. 그래서 C 는 B 보다 담은 글자가 적지만, 이 페이지에 쓰인 글자는 네 패밀리 모두 범위 안에 있어서 받는 양에는 영향이 없다.
다섯 벌은 Netlify 사이트 하나에 올려 같은 호스트에서 HTTP/2 로 받게 했다. D 도 Google 서버가 아니라 같은 사이트에서 받으므로 받는 곳의 차이는 결과에 섞이지 않는다. Google 서버에서 직접 받을 때의 차이는 뒤에서 따로 잰다. 모든 파일에는 Cache-Control: public, max-age=31536000, immutable 을 걸어 CDN(사용자 가까이에 파일을 복사해 두는 캐시 서버)이 원본 서버에 다시 묻지 않고 파일을 내주게 했다.
변형별 폰트 전송량
다섯 벌의 폰트 전송량을 먼저 나란히 놓는다.
| 변형 | 폰트 파일 | 폰트 전송량 | CSS 전송량 |
|---|---|---|---|
| A. TTF | 11 | 17,200 kB | 0.4 kB |
| B. woff2 | 11 | 12,045 kB | 0.4 kB |
| C. 정적 조각 | 58 | 679 kB | 107 kB |
| D. Google 응답 | 28 | 579 kB | 107 kB |
| D-range | 28 | 579 kB | 43 kB |
A 에서 C 까지 폰트 전송량이 96% 줄었다. 이 숫자들이 DevTools 의 어디에서 나오는지 변형마다 Network 화면으로 짚어 본다.
TTF 를 고정한 A

화면을 읽기 전에 설정 세 가지를 맞춘다. Font 필터(1)를 켜야 폰트 요청만 남고, Disable cache(2)를 켜야 새로고침할 때마다 처음 방문한 사용자처럼 전부 다시 받는다. Content-Encoding 열은 기본으로 보이지 않으므로, 열 머리를 우클릭해 Response headers 에서 Content-Encoding 을 켠다.
이름(3)의 확장자가 .ttf 이고, 굵기와 스타일마다 파일이 하나씩 모두 열한 개다. 크기(4)를 보면 한글 폰트 파일이 하나에 2.8 MB 에서 2.9 MB 다. 페이지에 글자가 몇 개 있든 이 파일에는 23,174자가 다 들어 있기 때문이다.
가장 큰 파일은 본문 폰트가 아니라 제목 폰트다. Noto Serif KR 700 하나가 4,949 kB 이고, 압축을 풀면 14 MB 다. 이 페이지에서 이 폰트로 그리는 한글은 제목 아홉 개(h1 하나와 h2 여덟 개)에 든 서로 다른 글자 50자뿐이다.
하단 줄은 숫자를 둘씩 보여준다. "17,206 kB / 17,243 kB transferred"(6)에서 앞의 값은 필터에 걸린 요청만 더한 것이고 뒤의 값은 페이지 전체다. transferred 는 네트워크를 지난 양이고, resources(7)는 받은 뒤 압축을 푼 크기다. 둘이 두 배 넘게 차이 나는 것은 Content-Encoding(5)이 Brotli(Google 이 만든 압축 방식)를 뜻하는 br 이기 때문이다. TTF 는 파일 자체에 압축이 없어서 Netlify 가 Brotli 로 압축해 보냈다. 사용자가 기다리는 것은 transferred 쪽이므로 이 글의 수치는 전부 transferred 기준이다.
형식만 바꾼 B
흔히 듣는 조언대로 TTF 를 woff2 로 바꾸면 얼마나 줄어들까. B 가 그 경우다.

17,200 kB 가 12,045 kB 가 됐다. 30% 줄었다. 이번에는 Content-Encoding(3)이 비어 있고 transferred(4)와 resources(5)가 거의 같다. woff2 는 파일 안에서 이미 압축된 형식이라 Netlify 는 다시 압축하지 않았다.
이 30% 는 압축하지 않은 파일과 비교한 값이 아니다. A 도 Brotli 로 압축돼 왔으므로 남은 차이는 압축 방식과 압축 수준에서 나온다. W3C WOFF2 명세를 보면 woff2 는 글자 모양 데이터를 압축하기 좋은 형태로 먼저 바꾼 뒤 Brotli 로 압축한다. 그런데 Netlify 가 TTF 에 건 Brotli 는 압축 수준이 낮았다. Brotli 는 압축 수준을 0 부터 11 까지 고를 수 있고, 높을수록 파일이 작아지는 대신 압축에 시간이 더 걸린다. 같은 Noto Sans KR 400 TTF 를 Netlify 는 2,866 kB 로 보냈는데, 수준 4 로 압축한 크기와 거의 같다. 가장 높은 수준인 11 로 미리 압축하면 2,475 kB 이고 woff2 는 2,053 kB 다. 서버가 TTF 를 어떻게 압축하느냐에 따라 woff2 로 바꿔 줄어드는 양도 달라진다. Google 의 gstatic 은 같은 파일을 gzip 으로 3,023 kB 에 보낸다.
비교 기준을 잘못 잡으면 이 숫자가 부풀려진다. A 의 resources 40,490 kB 와 B 를 비교하면 70% 가 줄어든 것처럼 보이지만, 사용자는 그 크기를 받은 적이 없다. 반대로 자기 서버가 TTF 를 압축 없이 보내고 있다면 사용자는 정말 40 MB 를 받는다. 어느 쪽인지는 Content-Encoding 열에서 바로 보인다.
그래도 B 는 글 한 편을 그리려고 12 MB 를 받는다. 파일 하나에 글자 전부가 들어 있으니, 형식을 바꿔도 받는 글자 수는 그대로다.
서브셋으로 나눈 C
C 는 같은 TTF 를 Google 이 쓰는 글자 범위대로 잘랐다. Noto Sans KR 과 Noto Serif KR 은 124개, Inter 는 7개, JetBrains Mono 는 6개 범위다. 한글 범위를 나누는 기준은 Google Fonts 팀이 2018년 일본어 지원을 발표하는 글에서 함께 밝혔다. 많이 쓰는 글자 2,000자를 빈도순으로 20조각에 나누고, 나머지는 유니코드 번호 순으로 100조각에 나눈다. Google 의 CSS 에는 한글 조각의 번호가 없어서, 필자가 CSS 에 나온 순서대로 0번부터 번호를 붙였다. 0번부터 99번이 유니코드 번호로 나눈 조각이고(CSS 에는 번호가 큰 글자의 조각부터 나온다), 100번부터 119번이 빈도순 조각이다. 마지막에 라틴, 확장 라틴, 키릴, 베트남어 조각 4개가 붙어 124개가 된다. C 의 파일 이름도 이 번호를 따랐다.
C 의 CSS 에는 조각마다 굵기마다 규칙이 하나씩 있어서 모두 661개다. Noto Sans KR 이 124개 범위에 굵기 넷이라 496개, Noto Serif KR 이 124개, Inter 가 7개 범위에 다섯 종류라 35개, JetBrains Mono 가 6개다.

규칙 661개 가운데 파일 58개만 받았고 합계(7)는 679 kB 다. A 의 4% 다. 파일 이름에 굵기가 들어 있어서 이름순으로 정렬하면 굵기마다 몇 조각을 받았는지 바로 보인다.
본문 굵기인 400 이 19조각(3)으로 가장 많고, 첫 문단의 300 이 5조각(2), 소제목의 500 이 9조각(4), 굵은 글씨의 700 이 13조각(5)이다. 대부분이 많이 쓰는 글자를 담은 100번대 조각이다. 400 의 56번처럼 번호가 동떨어진 조각도 있다. 이 페이지에서 56번 범위에 드는 글자는 "나눴다" 의 "눴" 한 글자뿐인데, 그 한 글자 때문에 조각 하나를 받았다.
A 에서 4,949 kB 이던 제목 폰트는 6조각(6), 113 kB 가 됐다. 제목 50자가 든 조각만 받았기 때문이다. Inter 는 굵기와 스타일마다 라틴 조각 하나씩(1), 다섯 개를 받았다.
Noto Sans KR 의 라틴 조각은 하나도 받지 않았다. 공백, 숫자, 영문을 전부 앞에 둔 Inter 가 그렸기 때문이다. 이 측정의 Chrome 에서는 font-family 의 순서가 받는 파일까지 바꿨다.
woff2 변환과 서브셋이 줄인 양을 나눠 보면 이렇다.
| 단계 | 전송량 | 줄어든 양 |
|---|---|---|
| A. TTF (Brotli) | 17,200,139 B | 기준 |
| B. woff2, 서브셋 없음 | 12,045,102 B | woff2 로 바꿔 5,155,037 B 를 줄였다 |
| C. woff2, 서브셋 | 678,925 B | 서브셋으로 나눠 11,366,177 B 를 더 줄였다 |
줄어든 양의 69% 가 서브셋에서 나왔다. 이 비율은 woff2 로 먼저 바꾸고 서브셋을 나중에 나눈 순서로 센 것이라, woff2 변환에 가장 유리하게 센 값이다. 서브셋을 먼저 나누면 woff2 가 압축할 대상 자체가 작아지므로 서브셋이 줄인 양의 비율은 더 커진다.
Google 응답을 쓴 D
D 는 Google 이 최신 Chrome 에 실제로 주는 결과물이다. 범위는 C 와 같은데 파일 수가 절반이다.

차이는 Noto Sans KR 에서 난다. C 는 굵기마다 조각을 따로 받아 46개였는데 D 는 19개(2)다. D 의 조각이 가변 폰트(variable font)이기 때문이다. 가변 폰트는 굵기를 wght 라는 축의 값으로 다루는 폰트다.

배포된 nskr-119.woff2 를 폰트 검사 도구인 Wakamai Fondue 에 넣으면 가변 폰트로 나오고(1), wght 축에 100 부터 900 까지가 들어 있다(3). 300, 400, 500, 700 은 모두 그 사이의 값이므로 16 KB 짜리 이 파일 하나(2)가 네 굵기를 모두 그린다. 설치 없이 확인하려면 Google 의 CSS 를 열어 보면 된다. 굵기가 다른 @font-face 넷이 같은 파일 주소를 가리키면 가변 파일이다. D 가 받은 19조각은 C 에서 굵기 400 이 받은 19조각과 번호가 모두 같다. 다른 굵기로 그린 글자가 모두 400 으로 그린 글자와 같은 조각 안에 있었다는 뜻이다.
대신 D 의 조각 하나는 C 보다 크다. 같은 119번 조각의 전송량이 C 의 400 은 9.0 kB 이고 D 는 16.7 kB 로 약 1.9배다. 가변 폰트는 굵기 축을 그리는 데이터를 더 담고, 필자의 fontTools 와 Google 의 제작 과정도 다르다. 그래서 Noto Sans KR 만 놓고 보면 파일 수는 46개에서 19개로 크게 줄었지만 바이트는 441 kB 에서 370 kB 로 16% 줄었다. 조각 하나가 약 1.9배 크므로, 조각 하나를 평균 두 굵기 가까이 같이 써야 가변 조각 쪽이 덜 받는다.
Inter 는 다섯 개가 둘(1)이 됐다. 일반 스타일의 네 굵기가 가변 파일 하나로 합쳐졌고, 이탤릭은 원래 400 하나라 그대로 하나다. 이 이탤릭 파일은 가변이 아니라 정적 파일이다. Google 은 굵기를 하나만 요청하면 그 굵기만 담은 정적 파일을 주기 때문이다.
같은 이유로 제목 폰트(3)는 C 와 D 가 거의 같다. 둘 다 6조각, 113 kB 다. 이 페이지는 Noto Serif KR 을 700 하나만 요청했다. 이 글의 네 패밀리처럼 가변 버전이 있는 폰트는 두 굵기 이상을 요청하면 그제야 가변 파일이 오는데, 축은 요청한 범위로 잘리지 않고 폰트가 가진 전체(Noto Serif KR 은 200 부터 900)가 온다. JetBrains Mono 도 400 하나라 정적 파일이다.
굵기를 범위로 요청한 D-range
D 와 D-range 는 같은 폰트 파일 28개를 받는다. 받지 않는 조각까지 포함해 두 CSS 가 가리키는 파일 268개를 하나씩 비교해 봐도 바이트까지 같았다. 다른 것은 CSS 뿐이다.

wght@300;400;500;700 처럼 굵기를 나열해 요청하면, Google 은 같은 파일을 가리키는 규칙을 굵기마다 하나씩 써 준다. Noto Sans KR 하나에 124개 곱하기 굵기 넷, 496개다. wght@300..700 처럼 범위로 요청하면 앞에서 본 코드처럼 font-weight: 300 700 을 적은 규칙이 조각마다 하나만 온다. 그래서 압축을 푼 CSS 가 429 kB 에서 173 kB 로(2, 4), 전송량은 107 kB 에서 43 kB 로(1, 3) 줄었다.
다만 이 전송량은 Netlify 가 압축해 보낸 결과다. Netlify 는 압축을 푼 크기의 4분의 1 정도로만 줄여 보냈는데, 같은 D 의 CSS 를 Brotli 로 미리 압축하면 가장 낮은 수준인 0 으로도 41 kB 다. 가장 높은 수준인 11 로는 D 가 14.9 kB, D-range 가 14.3 kB 다. 수준 11 에서는 차이가 0.6 kB 로 줄어든다. 같은 문자열이 반복되는 규칙은 압축이 잘 되기 때문이다. 범위 요청이 확실히 줄이는 것은 전송량보다 브라우저가 읽어야 할 CSS 의 양이다.
Fast 4G 의 폰트 도착 시각
바이트가 이만큼 다르면 시간은 얼마나 다를까. 같은 배포본을 DevTools 의 Fast 4G 조건으로 다시 쟀다. DevTools 소스를 보면 이 preset 의 기준값은 지연 60 ms, 9 Mbps 이고, 실제로는 배수를 곱해 지연 165 ms, 내려받기 8.1 Mbps 로 적용한다. 측정에도 이 적용값을 썼다. 시간은 문서 요청부터 마지막 폰트 응답이 끝날 때까지이고, 변형마다 캐시를 끈 새 브라우저로 9번 열어 잰 값의 중앙값을 쓴다. 이 조건은 요청마다 지연과 대역폭만 흉내 낸 것이라, 실제 모바일 망의 손실이나 휴대폰 CPU 는 들어 있지 않다.

A 는 18.5초, B 는 13.2초였다. 이 둘은 바이트를 대역폭으로 나눈 값에 가깝다. 17.2 MB 를 8.1 Mbps 로 받으면 17초, 12.0 MB 는 12초가 걸린다. 9회가 1초 안에 모였고, 같은 날 세 번을 따로 재도 두 변형의 중앙값은 0.3초 안에서 같았다.
서브셋이 있는 C, D, D-range 는 2초 안팎이다. 대역폭이 병목인 조건이라 시간이 바이트를 그대로 따라가서, 형식이 줄인 5초보다 서브셋이 줄인 11초가 크다.
반면 C, D, D-range 사이의 순서는 이 측정으로 정할 수 없었다. 같은 변형 안에서도 회차마다 1.5초에서 3.0초까지 흔들렸고, 그 폭이 변형 사이의 차이보다 컸다. 흔들림은 대부분 Netlify 의 CDN 에서 왔다.
같은 D 를 10월 27일에 같은 조건으로 8번 다시 열고 회차마다 Network 탭을 찍었다. 이번에는 응답 헤더 cache-status 를 열로 띄웠다. 그중 두 회차를 나란히 놓으면 이렇다.

위(1)는 폰트가 전부 CDN 캐시에서 나온 회차라 hit 이 찍혀 있다. 아래(4)는 fwd=miss 로, CDN 에 파일이 없어 원본 서버까지 다녀온 회차다. 이 회차는 HTML 과 CSS 도 원본까지 갔다. Waterfall(2, 5)의 막대에서 초록은 서버의 첫 응답을 기다린 시간이고 파랑은 내용을 받은 시간이다. 캐시에서 나온 회차는 초록이 짧고, 폰트 28개가 대역폭을 나눠 쓰느라 파랑이 길다. 원본까지 간 회차는 막대 대부분이 초록이다. 두 Waterfall 은 각자의 전체 시간에 맞춰 그려져서 막대 길이를 위아래로 비교할 수는 없다. 시간은 하단 줄(3, 6)에서 비교한다. HTML 이 원본에서 늦게 오니 DOMContentLoaded 가 373 ms 에서 889 ms 로 밀렸고, 폰트 요청도 그만큼 늦게 시작했다. 이 페이지는 마지막 폰트가 끝난 시각과 Load 가 같아서 그 차이가 Load 1.42초와 2.21초로 남는다. 8번 중 6번은 전부 캐시에서 나와 1.39초에서 1.44초에 끝났고, 원본까지 간 2번은 2.21초와 4.00초였다. 폰트를 하나도 바꾸지 않았는데 캐시 상태만으로 변형 사이의 차이보다 큰 차이가 난다.
10월 26일에 CDN 을 미리 불러 두는 횟수를 6번으로 늘려 다시 쟀다. 응답 헤더의 cache-status 로 보면, 폰트가 CDN 캐시에 없어 원본 서버까지 간 회차는 마지막 폰트의 첫 바이트가 오기까지 대개 370 ms 에서 750 ms, 한 번은 1.3초가 걸렸다. 캐시에서 나온 회차는 중앙값 약 210 ms 였다. 가장 늦은 회차들(2.3초에서 3.3초)은 모두 폰트나 HTML, CSS 중 하나가 원본 서버까지 간 회차였다. 폰트가 전부 캐시에서 나온 회차만 남겨도 변형마다 0.2초에서 0.8초의 흔들림이 남았고, C 는 그런 회차가 9번 중 3번뿐이었다. 그래서 C 와 D 의 차이는 시간이 아니라 바이트와 파일 수로만 말한다.
첫 화면이 그려지는 시점(FCP)도 이 측정으로는 판정하지 못했다. 중앙값이 A 1.0초, B 1.0초, C 1.4초, D 1.1초, D-range 1.3초였지만, 같은 날 다른 측정에서는 C 가 1.0초였고, CSS 가 가장 작은 D-range 가 D 보다 늦었다. 변형 사이의 차이가 회차 사이의 차이보다 작다. 이 페이지는 font-display: swap 을 적어 두어서 폰트가 오기 전에 다른 폰트로 먼저 그린다. 그래서 A 의 사용자도 1초 만에 글을 읽기 시작하고, 그 뒤 17초 동안 폰트가 도착하는 대로 글자가 그 폰트로 바뀐다.
서브셋이 남기는 비용
서브셋을 나누는 것이 답이라고 해서 비용이 없지는 않다. 다섯 가지가 있다.
첫째, CSS 가 커진다. A 의 fonts.css 는 전송량 0.4 kB 인데 C 와 D 는 107 kB 다. 규칙 661개가 글자 범위를 하나씩 들고 있기 때문이다. 압축을 풀면 C 는 434 kB, D 는 429 kB 다. <link> 로 거는 CSS 는 다 받기 전까지 브라우저가 화면을 그리지 않으므로 폰트 파일보다 먼저 받아야 한다. 자체 호스팅한다면 이 CSS 를 높은 수준으로 미리 압축해 두는 편이 좋다. 앞에서 본 것처럼 Netlify 가 보낸 크기와 최고 수준으로 미리 압축한 크기는 이 CSS 에서 일곱 배 차이가 난다.
둘째, 요청 수가 늘어난다. B 의 파일 11개가 C 에서는 58개가 됐다. HTTP/2 이상에서는 한 연결 위에서 여러 요청을 동시에 보낼 수 있어 요청 하나가 늘 때의 비용이 작지만, 0 은 아니다. 요청 수가 얼마를 넘으면 그 비용이 바이트 절감을 넘어서는지는 이번에 재지 않았다.
셋째, 조각마다 겹치는 데이터가 생긴다. 조각마다 폰트의 공통 데이터가 반복해서 들어가기 때문에, 조각을 많이 받을수록 이 중복이 쌓인다. Google 범위가 덮는 Noto Sans KR 의 17,655자를 400 굵기 파일 하나로 자르면 1,302,420 B 인데, 같은 글자를 124개로 나눈 조각을 전부 더하면 1,823,312 B 로 약 40% 크다. 거의 모든 조각을 받는 페이지라면 파일 하나보다 많이 받는 셈이다. 이 페이지는 굵기 하나에 124조각 중 많아야 19조각을 받았으므로 그 경우와는 거리가 멀다.
넷째, 배포하는 파일이 많아진다. 이 데모의 C 폴더에는 조각이 661개 있다. 브라우저가 받는 것은 58개지만 서버에는 전부 올라가야 한다. 브라우저가 받는 양과 서버에 올리는 양은 따로 세야 한다.
다섯째, 범위 밖의 글자는 대체 글꼴로 그려진다. Google 의 범위는 Noto Sans KR 의 23,174자 중 17,655자만 덮으므로, 나머지 5,519자가 나오면 웹폰트가 아닌 글꼴로 그린다. 대부분 드물게 쓰는 한자라 이 페이지에는 영향이 없었지만, 인명이나 지명에 한자가 섞이는 서비스라면 확인해야 한다.
그리고 입력창이나 채팅처럼 글자가 나중에 들어오는 화면에서는, 새 글자가 나올 때 그 글자의 조각을 그 시점에 받는다. 글마다 쓰는 글자가 다르니 다음 글로 넘어갈 때도 새 조각을 받을 수 있다. 첫 화면에서 잰 전송량이 전부가 아니다. 두 번째 페이지에서 얼마를 더 받는지는 이번에 재지 않았다.
서브셋을 적용하는 길
Google Fonts 를 쓴다면 CSS API 를 그대로 연결하면 된다. Next.js 의 next/font/google 도 이 응답을 빌드할 때 받아 그대로 자체 호스팅한다. 이 두 길의 차이는 뒤에서 따로 잰다.
자체 호스팅이라면 Fontsource(오픈소스 폰트를 npm 패키지로 묶어 둔 프로젝트) 같은 패키지가 서브셋을 함께 준다. 2026-10-22 기준 최신인 @fontsource/noto-sans-kr 5.3.1 의 CSS 는 Google 과 같은 124개 범위로 나뉜 C 모양이고, 이 구조는 5.2.8 부터다. 5.2.7 에서는 기본 CSS 에 한글 규칙이 없고 한글이 범위 없는 파일 하나로 따로 있었다. 가변 조각이 필요하면 @fontsource-variable/noto-sans-kr 가 같은 범위에 font-weight: 100 900 하나로 선언된 D-range 모양이다. 쓰는 버전의 CSS 를 열어 unicode-range 를 확인하는 편이 안전하다.
Google 에 없는 폰트라면 직접 자른다. C 가 그 경우다. Google 의 범위 목록을 가져와 fontTools 의 subset 모듈(pyftsubset 과 같은 엔진)로 범위마다 잘라 woff2 로 저장하고, 조각마다 unicode-range 를 적은 규칙을 만든다. 데모 저장소의 scripts/ 에 있는 빌드 스크립트가 이 과정을 한다.
자기 서비스에서 확인할 때는 파일 이름에 기대지 않는다. 이 글의 그림은 읽기 쉽게 파일 이름을 바꿔 두었지만, 실제 Google 응답이나 next/font 산출물은 이름이 해시라서 굵기와 조각 번호가 보이지 않는다. 대신 Font 필터를 켜고 파일 수와 크기를 본다. 굵기마다 MB 단위 파일이 하나씩 있다면 서브셋이 없다. 조각이 수십 개인데 굵기 수에 비례해 늘어난다면 정적 조각이고, CSS 필터에서 fonts.css 의 응답을 열어 font-weight 가 100 900 같은 범위로 적혀 있다면 가변 조각이다.
Google 서버에서 받는 비용
받는 양은 서브셋으로 줄었다. 남은 방법은 받는 양을 거의 줄이지 못한다. 대신 폰트를 언제 받는지와 받는 동안 화면이 어떻게 보이는지를 정한다. 그 첫째가 폰트를 어디서 받느냐다.
지금까지의 D 는 Google 의 응답을 내려받아 같은 사이트에서 보냈다. 실제 서비스에서는 Google 의 CSS 주소를 그대로 거는 경우가 많다. 그러면 브라우저는 fonts.googleapis.com 에서 CSS 를, fonts.gstatic.com 에서 폰트를 받으므로 연결할 곳이 둘 늘어난다. 이 비용을 보려고 D 와 같은 요청을 Google 에 직접 거는 페이지를 둘 더 만들었다. 하나는 CSS 링크만 걸었고, 다른 하나는 fonts.google.com 이 주는 삽입 코드대로, 연결을 미리 열어 두는 preconnect 를 두 곳에 더했다.
10월 26일에 같은 Fast 4G 조건으로 쟀고, 폰트가 전부 캐시에서 나온 회차의 중앙값이다. D 는 9회 중 7회, 나머지는 9회 모두 해당했다. 앞의 그림과는 측정한 날과 미리 불러 두는 횟수가 달라서 D 의 값도 다르다(2.1초와 1.74초). 이 표 안에서만 비교한다.
캐시에서 나온 회차만 고른 것은 같은 사이트에서 받는 쪽에 유리한 조건이다. 트래픽이 적은 사이트는 CDN 캐시가 자주 비어서, 앞에서 본 C 처럼 원본 서버까지 가는 회차가 많다. 그 비용은 자체 호스팅이 진다.
| 페이지 | 폰트 전송량 | 마지막 폰트 도착 |
|---|---|---|
| D, 같은 사이트에서 받음 | 579 kB | 1.74초 |
| Google 에 직접 연결 | 576 kB | 1.73초 |
| Google 에 직접 연결, preconnect 추가 | 576 kB | 1.76초 |
셋의 차이는 0.03초 안이다. 같은 페이지를 여러 번 열 때 생기는 차이(0.2초에서 0.8초)보다 작다. 연결 비용은 분명히 있었다. fonts.googleapis.com 에 처음 연결하는 데(암호화 연결을 맺는 TLS 포함) 회차마다 약 80 ms 에서 190 ms 가 걸렸고, 이 시간은 CSS 를 받기 전에 붙는다. 그런데 fonts.gstatic.com 의 연결 시간은 폰트 요청에서 0 으로 나왔다. Google 의 CSS 응답에 Link: <https://fonts.gstatic.com>; rel=preconnect; crossorigin 헤더가 들어 있어서, 브라우저가 CSS 를 받는 동안 폰트 서버 연결을 미리 열기 때문이다. HTML 에 넣은 preconnect 중 gstatic 쪽 한 줄이 차이를 만들지 못한 것도 같은 이유로 보인다. googleapis 쪽 한 줄도 차이가 없었는데, 그 이유와 googleapis 연결 시간이 마지막 폰트 도착 시각에 드러나지 않은 이유는 나눠 보지 않았다.

다만 이 표는 연결할 곳이 늘어나는 비용을 작게 보여준다. DevTools 의 Fast 4G 는 요청마다 지연을 더하는 방식이라, 연결을 맺는 과정(DNS, TCP, TLS)에는 지연이 걸리지 않는다. 이 측정에서도 Fast 4G 를 켠 채 Netlify 에 연결하는 데 대개 147 ms 안팎(그중 TLS 74 ms 안팎)이 걸렸는데, 필자의 회선(서울) 그대로의 값이다. Lighthouse 의 throttling 문서도 요청 단위로 거는 지연은 패킷 단위보다 부정확하다고 적는다. 연결 한 번에는 왕복이 여러 번 들어가므로, 왕복 시간이 긴 망일수록 연결할 곳 하나가 늘어나는 비용도 커진다. 그 크기는 이번에 재지 않았다.
예전에는 Google 에 직접 연결하는 쪽에 이점이 하나 있다고 여겨졌다. 여러 사이트가 같은 Google 폰트 주소를 쓰므로, 다른 사이트에서 이미 받은 폰트를 브라우저 캐시에서 다시 쓸 수 있다는 것이다. Chrome 은 86 부터 이 공유를 막았다. Chrome 의 캐시 분할 안내에 따르면 캐시 키에는 리소스 주소와 함께 최상위 사이트와 프레임의 사이트가 들어간다. 사이트가 다르면 같은 주소라도 캐시를 따로 쓴다는 뜻이다.
실제로 같은 주소의 폰트라도 사이트가 바뀌면 처음부터 다시 받았다. Chrome 프로필 하나로 Google 에 직접 연결하는 페이지를 배포한 사이트에서 두 번 열고, 이어서 필자의 로컬 서버(다른 사이트)에서 두 번 열었다. 같은 사이트에서 다시 열면 폰트 28개가 모두 캐시에서 나왔지만, 사이트를 바꾸자 같은 주소의 폰트 28개, 576 kB 를 다시 받았다. 같은 순서를 캐시 분할을 끈 Chrome(--disable-features=SplitCacheByNetworkIsolationKey)으로 돌리면 사이트를 바꿔도 28개 모두 캐시에서 나왔다. 지금의 Chrome 에서 폰트를 사이트끼리 나눠 쓰는 캐시는 없다. HTTP Archive 의 2024 Web Almanac 폰트 장도 캐시 분할로 공유 폰트 CDN 의 이점이 사라졌다고 적고, 폰트를 자기 사이트에서만 보내는 사이트가 데스크톱 기준 2022년 22% 에서 2024년 28% 로 늘었다고 적는다.
next/font 의 preload 와 대체 글꼴
Next.js 의 next/font 는 이 선택을 빌드 단계에서 대신한다. 빌드할 때 Google 의 CSS 와 폰트를 내려받아 자기 사이트에서 보내고, 정해 둔 폰트를 preload 하고, 폰트가 오기 전에 쓸 대체 글꼴의 크기를 웹폰트에 맞춰 둔다. 16.3.4 의 로더는 Chrome 104 의 User-Agent 로 CSS 를 받아 그 안의 조각 파일을 전부 내려받고 주소만 바꾸므로 unicode-range 는 그대로 남는다. 2026-10-21 에 같은 User-Agent 로 요청해 보니 D 와 같은 규칙과 같은 파일이 왔다.
같은 본문과 같은 네 패밀리로 Next.js 16.3.4 페이지를 만들어 정적 파일로 내보내고 같은 사이트에 올렸다. Noto Sans KR 은 굵기 넷을 나열했고, 네 패밀리 모두 subsets: ['latin'] 이다. Noto Sans KR 의 subsets 에 넣을 수 있는 값은 cyrillic, latin, latin-ext, vietnamese 뿐이라 한글을 고를 방법이 없다. 그래도 한글이 빠지지는 않는다. subsets 는 어느 조각을 preload 할지만 정하고, 한글 조각의 @font-face 는 CSS 에 모두 남는다. 산출물에는 폰트 파일이 268개 있다.
받는 양은 631 kB 로 D 보다 52 kB 많았다. 이 페이지에 쓰이지 않는 Noto Sans KR 의 latin 조각(26 kB)을 받았고, Inter 이탤릭이 가변 파일로 와서 D 의 정적 파일보다 26 kB 컸다. 앞의 표와 같은 조건에서 마지막 폰트는 1.81초에 도착했다(7회 중앙값). 표의 D(1.74초)와 0.07초 차이로, 여러 번 열 때 생기는 차이보다 작다. next/font 가 그 위에 더하는 것은 preload 와 대체 글꼴 보정이다.
preload 는 한글 조각에 적용되지 않는다. HTML 에 들어간 preload 는 5개이고 모두 latin 조각이다. next/font 의 subsets 로는 한글 조각을 고를 수 없기 때문이다. 빈도순 조각 20개처럼 어느 글에서나 받을 조각을 HTML 에 직접 preload 로 적을 수는 있지만, 이번에는 재지 않았다. 게다가 이 5개 중 Noto Sans KR 의 latin 조각(25,804 B)은 이 페이지에 필요 없는 파일이었다. preload 를 끄고 빌드하면 이 파일은 요청조차 되지 않는다. 앞에서 본 것처럼 라틴 글자를 Inter 가 먼저 그리기 때문이다. preload 를 끈 빌드는 마지막 폰트가 1.87초로, D 와 0.13초 차이였다.
대체 글꼴 보정은 라틴 글자에만 걸린다. next/font 는 폰트마다 Noto Sans KR Fallback 같은 규칙을 만들고, size-adjust 와 ascent-override 로 대체 글꼴의 크기를 웹폰트에 맞춘다. 그런데 그 바탕이 local("Arial") 이다(Noto Serif KR 은 Times New Roman). Arial 에는 한글이 없으므로 한글은 보정이 없는 다음 대체 글꼴로 넘어간다. 첫 화면이 그려진 직후 본문 문단을 실제로 그린 폰트를 보면, 라틴은 이미 Inter 였고 아직 조각이 도착하지 않은 한글은 macOS 의 기본 한글 폰트인 Apple SD Gothic Neo 였다. 이 값은 DevTools Elements 패널의 Computed 탭 맨 아래 Rendered Fonts 에서도 볼 수 있고, 측정에는 같은 정보를 주는 Chrome DevTools Protocol 의 CSS.getPlatformFontsForNode 를 썼다.
next/font 를 고르는 이유
여기까지 보면 next/font 가 한글 페이지를 더 빠르게 만들지는 않았다. 서브셋은 Google 이 이미 해 주고, preload 와 대체 글꼴 보정은 한글에 적용되지 않는다. 그런데도 많이 쓰는 데는 다른 이유가 있다.
next/font 는 2022년 10월 Next.js 13에서 @next/font 라는 이름으로 처음 나왔다. 발표문은 이 도구가 외부 네트워크 요청을 없애 개인정보와 성능을 챙기고, 빌드할 때 폰트를 받아 자체 호스팅하며, size-adjust 로 레이아웃 밀림을 없앤다고 소개한다. 발표문은 브라우저가 Google 에 요청을 하나도 보내지 않는다는 점을 따로 강조한다.
개인정보 쪽에는 같은 해 초의 독일 판결이 있었다. 2022년 1월 20일 뮌헨 지방법원 판결(사건번호 3 O 17493/20)은 웹사이트가 Google Fonts 를 불러오면서 방문자의 IP 주소를 동의 없이 Google 에 넘긴 것을 위법으로 보고 100유로 배상을 인정했다. 판결은 Google 서버에 연결하지 않고도 Google Fonts 를 쓸 수 있다는 점을 근거로 들었다. 자체 호스팅을 하면 이 문제가 생길 자리가 없다.
나머지 이유도 앞에서 본 것과 이어진다. Google 에 직접 연결해서 얻던 공유 캐시는 Chrome 에서 사라졌다. 연결할 곳이 늘어나는 비용은 이번 측정에 작게 나왔지만, Fast 4G 가 연결 시간에 지연을 걸지 않아서다. 그리고 Google 응답을 내려받아 주소를 바꾸고, preload 를 걸고, 대체 글꼴을 맞추는 일을 손으로 하면 이 글의 데모처럼 스크립트가 여러 개 필요하다. next/font 는 그 일을 함수 호출 하나로 줄인다.
폰트가 늦게 올 때의 화면
font-display 는 폰트가 도착하기 전에 무엇을 그릴지 정한다. 적지 않으면 기본값은 auto 인데, web.dev 의 폰트 문서에 따르면 Chromium 계열과 Firefox 는 이때 폰트를 기다리며 최대 3초 동안 글자를 그리지 않는다. Google 의 CSS 도 주소에 display=swap 을 붙이지 않으면 font-display 가 없는 규칙을 준다. 지금까지의 페이지는 모두 swap 을 적었다. 대체 글꼴로 먼저 그리고, 폰트가 오면 바꾼다. 바꿀 때 글자 폭과 줄 높이가 달라지면 문단이 밀리는데, 이 밀림을 누적해 잰 값이 CLS(Cumulative Layout Shift)다.
swap 인 D 계열 페이지의 CLS 는 중앙값 0.018 에서 0.020 이었고, 회차별로는 0.024 까지 나왔다. web.dev 의 CLS 문서가 좋음 기준으로 정한 0.1 보다 한참 낮다. 밀림은 한 번이 아니라 여러 번에 나뉘어 일어났다. 조각마다 도착 시각이 달라서, 첫 화면 직후에는 같은 문단 안에서 일부 글자만 Noto Sans KR 이고 나머지는 Apple SD Gothic Neo 인 경우가 있었다.
next/font 의 대체 글꼴 보정을 끈 빌드와 켠 빌드도 CLS 가 0.017 과 0.018 로 같았다. 보정이 적용되는 라틴 글자는 밀림과 관계가 없었다. preload 를 끈 빌드만 0.009 로 절반쯤이었는데, 큰 밀림이 두 번에서 한 번으로 준 것으로 보이며 원인은 확인하지 않았다.
D 의 파일은 그대로 두고 font-display: optional 로만 바꾼 페이지도 쟀다. CLS 는 9회 모두 0.0002 이하였다. 대신 첫 방문에서는 폰트를 다 내려받고도 끝까지 대체 글꼴로 그려졌다. 한글은 Apple SD Gothic Neo, 라틴은 시스템 글꼴이었고, 첫 방문을 따로 세 번 열어 봐도 같았다. CSS Fonts 4 명세의 font-display 절에서 optional 은 첫 페인트에 쓸 수 있을 만큼 바로 준비되지 않은 폰트를 그 페이지에서 쓰지 않는다. 이번 측정의 Chrome 은 폰트를 끝까지 내려받았으므로, 처음 온 사용자는 폰트를 받기만 하고 보지 못한다. 캐시에 남은 폰트는 다음 방문에 쓰일 수 있다. 2024 Web Almanac 의 데스크톱 기준으로 swap 을 쓰는 사이트는 약 44%, optional 은 1% 미만이다.
이 CLS 는 macOS 의 대체 글꼴로 잰 값이다. Windows 에서는 대체 글꼴이 맑은 고딕이라 값이 다를 수 있다. next/font 가 맞추지 못하는 한글 대체 글꼴은 직접 맞출 수도 있다. local("Apple SD Gothic Neo") 나 local("Malgun Gothic") 를 바탕으로 한 @font-face 에 size-adjust 와 ascent-override 를 적어 대체 글꼴로 쓰는 방법이다. 이번에는 재지 않았다. 실제 사용자에게서 일어나는 밀림의 크기와 가장 크게 밀린 요소는 web-vitals 라이브러리의 attribution 빌드로 모으는 편이 실험실 측정보다 정확하다.
국내 서비스의 폰트
실제 서비스는 어떻게 하고 있을까. 2026년 10월 26일에 국내 서비스 몇 곳의 홈을 headless Chrome 으로 처음 방문하듯 열고, 네트워크 제한 없이 폰트 전송량을 쟀다. 각각 2회씩 쟀고 두 회차의 차이는 수백 바이트 안이었다.
| 서비스 | 폰트 | 서브셋 | 폰트 파일 | 폰트 전송량 |
|---|---|---|---|---|
| 무신사 | Pretendard, 굵기 넷 | 없음 | 4 | 3,279 kB |
| 토스 | Toss Product Sans, 굵기 넷 | 없음 | 5 | 2,049 kB |
| 카카오 회사 소개 | 카카오 서체 두 종, 굵기 둘씩 | 없음 | 4 | 1,395 kB |
| 컬리 | Pretendard 가변 폰트 | 92조각 | 19 | 501 kB |
| 당근 | Pretendard 가변 폰트 | 92조각 | 10 | 262 kB |

서브셋이 없는 쪽은 이 글의 B 와 같은 상태다. woff2 이지만 파일 하나가 굵기 하나의 글자 전부를 담는다. 무신사의 Pretendard 는 굵기 하나에 800 kB 가 넘는다. 토스의 파일 다섯 개 중 넷은 Toss Product Sans 의 굵기별 파일이고, 나머지 하나는 10 kB 짜리 다른 폰트다. 반대로 컬리와 당근은 Pretendard 가변 폰트를 92개 조각으로 나눈 CSS 를 쓰고, 페이지에 쓰인 글자가 든 조각만 받는다. Fast 4G 의 8.1 Mbps 로 나누면 3,279 kB 는 약 3.2초, 262 kB 는 약 0.3초다. 잰 값이 아니라 계산값이지만, 이 글에서 잰 Google 직접 연결, preload 의 시간 차이보다 열 배 넘게 크다.
홈 한 페이지를 첫 방문으로만 잰 값이라 페이지와 시점에 따라 달라진다. 굵기마다 파일 하나인 방식은 한 번 받으면 다음 방문에는 캐시에서 쓰므로, 재방문이 많은 서비스라면 차이가 작아진다.
표에 넣지 않은 곳도 있다. 네이버는 본문을 시스템 폰트로 그리고 일부에만 나눔스퀘어 네오 파일 하나(389 kB)를 받았다. 배달의민족은 Pretendard 를 선언해 두었지만 홈에서는 받지 않았고, 자체 서체 파일 하나(140 kB)만 받았다.
서브셋부터 보는 순서
서브셋이 없는 폰트는 페이지가 몇 글자를 쓰든 파일마다 글자 전부를 받는다. 폰트 네 개를 쓰는 이 페이지에서 그 양은 TTF 로 17.2 MB, woff2 로 12.0 MB 였고, Fast 4G 에서 마지막 폰트가 오기까지 18.5초와 13.2초가 걸렸다. 서브셋을 나누면 받는 양이 페이지에 쓰인 글자를 따라간다. 같은 페이지가 0.6 MB, 2초 안팎이 됐다.
한글 페이지에서 서브셋이 특히 중요한 이유는 파일 크기다. woff2 로 바꾸면 3분의 1 정도가 줄지만, 그래도 이 페이지의 한글 폰트는 굵기 하나에 2 MB 에서 3.2 MB 가 남았다. 제목 몇 개를 그리려고 넣은 명조 폰트 하나가 본문 폰트보다 컸다.
서브셋을 나눈 뒤의 선택들은 이 측정으로 시간을 가르지 못했다. 정적 조각과 가변 조각은 순서를 정할 수 없었고, Google 에 직접 연결할지 자기 사이트에서 보낼지와 preload, 대체 글꼴 보정이 만든 차이는 같은 페이지를 여러 번 열 때 생기는 차이보다 작았다. 다만 Fast 4G 는 연결을 맺는 시간에 지연을 걸지 않으므로, 연결할 곳이 느는 Google 직접 연결의 비용은 이 측정에 작게 나왔을 수 있다. 그래서 이 선택들은 다른 기준으로 고르는 편이 낫다. 개인정보와 캐시 분할을 보면 자체 호스팅이 낫고, 그 일을 덜어 주는 도구가 next/font 다. font-display 는 swap 으로 작은 밀림을 받아들일지, optional 로 첫 방문에 대체 글꼴을 보여 줄지 정하면 된다.
이번에 비교하지 않은 방법도 있다. 쓰는 굵기와 패밀리 수를 줄이면, 서브셋이 없을 때 받는 양이 줄인 파일 수만큼 준다. 이 블로그도 굵기를 400 과 700 둘만 쓴다. 서브셋 없이 가변 폰트 파일 하나로 굵기 넷을 보내는 방법도 있다. 둘 다 이번에 재지 않았다.
확인하지 못한 것도 남았다. 왕복 시간이 긴 망에서 연결할 곳 하나가 더하는 비용, Windows 의 맑은 고딕으로 그릴 때의 CLS, preload 를 끄자 CLS 가 절반이 된 원인, 다음 글로 넘어갈 때 새로 받는 조각의 양, 그리고 캐시에서 나온 회차끼리도 남은 흔들림의 원인이다. 필자는 어느 것도 서브셋이 만든 차이를 뒤집을 크기는 아니라고 본다. 다만 잰 값이 아니라 추론이다.
국내 대형 서비스에도 아직 서브셋 없이 굵기마다 수백 kB 파일을 보내는 곳이 있다. 이 글을 읽는 분들도 자기 서비스의 Network 화면에서 Font 필터로 이런 파일이 있는지 한번 보기를 권한다. 있다면 next/font 나 preload 보다 서브셋을 먼저 보면 된다.