블로그를 공개하고 나서 첫 방문 용량을 재봤다. 3,131KB였다. 글 일곱 편짜리 정적 사이트치고는 많았다.
내역을 뜯어보니 한쪽이 압도적이었다. **웹폰트가 2,290KB, 전체의 73%**였다. 나머지 841KB가 이미지와 문서 전부였다.
폰트 한 종류를 네 굵기로 쓰고 있었다.
| 파일 | 크기 |
|---|---|
| Pretendard-400 | 748KB |
| Pretendard-500 | 760KB |
| Pretendard-600 | 767KB |
| Pretendard-700 | 773KB |
네 벌이 3,048KB다. 첫 방문에 그중 세 벌을 받아 2,290KB가 됐다.
왜 한 벌이 750KB인가
한글 때문이다. 완성형 한글은 11,172자다. 자음과 모음을 조합할 수 있는 모든 경우를 미리 글자로 만들어 둔 것이라 이렇게 많다.
영문 폰트는 보통 수백 자면 끝난다. 한글 폰트는 그 스무 배 이상의 글자 그림을 담는다. 그래서 통짜 woff2가 영문 폰트의 열 배 넘게 나온다.
문제는 이 다음이다. 사이트에 실제로 나오는 글자가 몇 개인지 세어봤다.
빌드 결과물의 모든 한글 = 647자
11,172자를 받아서 647자를 쓰고 있었다. 나머지 10,525자는 한 번도 화면에 안 나오는데 굵기마다 전부 내려받고 있었다.
쪼개면 쓰는 것만 받는다
CSS의 unicode-range가 이걸 해결한다. 폰트 파일을 글자 영역별로 쪼개 놓고, 각 조각이 어느 글자를 담당하는지 선언한다. 브라우저는 페이지에 그 영역 글자가 실제로 나올 때만 그 조각을 받는다.
@font-face{
font-family:'Pretendard';
src:url('/assets/fonts/subset/Pretendard-400.subset.73.woff2') format('woff2');
unicode-range:U+ac00-ac2f, U+ac30-ac5f, ...;
}
쪼개는 작업 자체는 fonttools로 한다. 어려운 건 어디서 자르느냐였다.
경계를 직접 정하면 안 된다
처음엔 가나다 순으로 적당히 나누면 되는 줄 알았다. 그러면 안 된다.
한글 음절은 유니코드에서 연속된 번호를 갖는다. 아무 데나 자르면 한 단어 안의 글자가 서로 다른 조각으로 갈린다. 브라우저가 한쪽 조각만 받으면 그 단어의 일부 글자만 다른 폰트로 보인다.
그래서 경계는 직접 정하지 않고 Pretendard가 배포하는 동적 서브셋 CSS의 것을 그대로 가져왔다. 그 CSS에서 unicode-range 값만 뽑아 우리가 가진 원본 파일에 적용했다. 폰트를 만든 쪽이 이미 풀어 둔 문제를 다시 풀 이유가 없다.
결과는 굵기 네 개 × 조각들 = 368개의 @font-face 선언이다.
대가는 CSS 크기였다
선언이 368개면 CSS가 커진다.
| 전 | 후 | |
|---|---|---|
| CSS 원본 | 24KB | 229KB |
| brotli 압축 후 | — | 31KB |
열 배로 불었다. 그런데 서버는 압축해서 보낸다. 압축 후 31KB면, 원본 CSS도 같은 방식으로 압축해 보내고 있었다는 점을 감안할 때 실제로 늘어난 비용은 7KB보다 클 수 있다.
7KB를 더 쓰고 2,290KB를 아낀다. 계산할 것도 없었다.
결과
| 전 | 후 | |
|---|---|---|
| 첫 방문 총량 | 3,131KB | 558KB |
| 폰트 | 2,290KB | 쓰는 조각만 (한 조각 평균 10KB) |
폰트와 이미지 최적화를 합쳐 전체의 82%가 줄었다. 이미지 쪽 최적화 과정은 다른 글에서 따로 적는다.
남은 것
이 방식에는 전제가 있다. 글자 종류가 늘면 받는 조각도 늘어난다. 지금은 647자라 조각 몇 개로 끝나지만 글이 쌓이면 달라진다.
다만 늘어나는 속도는 느릴 것이다. 한국어는 자주 쓰는 음절이 정해져 있어서, 글이 열 배가 돼도 쓰는 글자 수는 열 배가 되지 않는다. 그게 바로 조각별로 나눠 두는 방식이 통하는 이유이기도 하다.
