콘텐츠로 이동

한글 웹폰트 2MB 를 300KB 로

한글 웹폰트는 크기가 문제입니다. 라틴 폰트는 200자면 되지만 한글은 음절만 11,172자입니다. Pretendard 가변 폰트가 2MB, SUIT 이 610KB 입니다. 이 사이트도 한동안 매 페이지에서 2.6MB 를 받고 있었습니다.

두 가지로 611KB 까지 줄였습니다. 글자가 빠지는 일 없이요.

전 후
Pretendard 2,009KB 314KB (+ 나머지 867KB, 거의 안 받음)
SUIT 610KB 297KB
한 페이지 2,619KB 611KB

고치기 전에 무엇이 무거운지부터 봐야 합니다. 서체가 아니라 이미지가 문제일 수도 있습니다.

// 응답을 종류별로 더한다
page.on('response', (r) => {
const len = Number(r.headers()['content-length'] || 0)
const type = (r.headers()['content-type'] || '').split(';')[0]
// font / image / js / css 로 묶어 합계를 낸다
})

이 사이트에서는 전송량 2.8MB 중 2.7MB 가 서체였습니다. 그러면 서체부터 손대는 게 맞습니다.

가변 폰트는 굵기 축의 양 끝을 저장하고 그 사이를 보간합니다. Pretendard 는 45–930 을 들고 있는데, 실제로 쓰는 굵기는 몇 개뿐인 경우가 대부분입니다.

먼저 쓰는 굵기를 세어 보십시오.

터미널 창
grep -rhoE "font-(thin|light|normal|medium|semibold|bold|black)" src/ | sort | uniq -c

이 사이트는 normal·medium·semibold 셋뿐이었습니다. 400–600 만 남기면 됩니다.

터미널 창
python3 -m fontTools.varLib.instancer font.ttf wght=400:600 -o narrowed.ttf

이것만으로 2,009KB 가 1,327KB 가 됩니다. 글자는 하나도 안 빠지므로 위험이 없습니다. 여기서 멈춰도 괜찮습니다.

2. 상용 한글과 나머지를 가릅니다

섹션 제목: “2. 상용 한글과 나머지를 가릅니다”

여기서부터가 큽니다. 한글 11,172자 중 실제 문장에 쓰이는 것은 훨씬 적습니다.

KS X 1001 상용 한글 2,350자가 기준으로 쓸 만합니다. 현대 한국어 산문은 거의 이 안에서 끝납니다. 이 사이트 105쪽을 훑어 보니 실제로 쓰인 음절은 844자였습니다.

파이썬에서 이 목록을 얻는 방법이 있습니다.

def ks_x_1001():
"""EUC-KR 두 바이트 중 첫 바이트가 B0–C8 인 것이 상용 한글 2350자다."""
out = []
for cp in range(0xAC00, 0xD7A4):
try:
encoded = chr(cp).encode('euc_kr')
except UnicodeEncodeError:
continue
if len(encoded) == 2 and 0xB0 <= encoded[0] <= 0xC8:
out.append(cp)
return out

여기에 실제로 쓰는 글자와 라틴·구두점을 더해 core 를 만들고, 남은 음절을 rest 로 둡니다.

터미널 창
python3 -m fontTools.subset narrowed.ttf \
--unicodes=AC00-AC01,AC04,AC07-AC0A,... \
--flavor=woff2 --layout-features='*' --no-hinting \
--output-file=core.woff2

두 조각의 unicode-range 가 겹치지 않아야 합니다. 그래야 브라우저가 화면에 실제로 있는 글자를 보고 필요한 조각만 받습니다.

@font-face {
font-family: 'Pretendard';
src: url('/fonts/pretendard-core.woff2') format('woff2-variations');
font-weight: 400 600;
font-display: swap;
unicode-range: U+0000-00FF, U+AC00-AC01, U+AC04, ...;
}
@font-face {
font-family: 'Pretendard';
src: url('/fonts/pretendard-rest.woff2') format('woff2-variations');
font-weight: 400 600;
font-display: swap;
unicode-range: U+AC02-AC03, U+AC05-AC06, ...;
}

글자가 빠지는 일은 없습니다. 흔치 않은 음절이 있는 페이지에서는 브라우저가 그때 rest 를 받습니다. 평소에는 core 만 옵니다.

unicode-range 문자열은 길어집니다. 이 사이트는 core 가 14KB, rest 가 17KB 였습니다. 압축하면 그 절반 아래로 내려가고, 그 대가로 1.5MB 를 아낍니다.

조각을 잘못 나누면 화면은 멀쩡한데 일부 글자만 시스템 서체로 조용히 떨어집니다. 눈으로는 잘 안 보입니다. 캔버스로 재면 확실합니다.

const cv = document.createElement('canvas').getContext('2d')
const missing = []
for (const ch of hangulOnThisPage) {
cv.font = '40px Pretendard'
const a = cv.measureText(ch).width
cv.font = '40px "NoSuchFontXYZ"' // 반드시 없는 이름
const b = cv.measureText(ch).width
if (Math.abs(a - b) < 0.01) missing.push(ch) // 폭이 같으면 대체된 것
}

없는 서체 이름으로 잰 폭과 같다면 그 글자는 대체 서체로 그려진 것입니다. 이 사이트에서는 한 페이지의 한글 291자가 모두 통과했고, rest 조각은 아예 내려오지 않았습니다.

빌드마다 다시 자르면 배포가 파이썬과 fontTools 에 묶입니다. 서체는 일 년에 한 번 바뀔까 말까 한 것이라 그럴 값이 없습니다. 자른 결과를 저장소에 넣고, 자르는 스크립트도 함께 두십시오.

원본은 public/ 밖으로 옮기십시오. 참조하지 않아도 그 안에 있으면 배포물과 이미지에 실려 나갑니다. 다시 자를 때 필요하니 지우지는 마시고요.

하는 일 줄어드는 양 위험
축 좁히기만 2,009 → 1,327KB 없음
+ 상용/나머지 분리 2,009 → 314KB 범위를 겹치게 두면 역효과
쓰는 글자만 남기기 더 작아짐 새 글에 새 글자가 나오면 깨짐

마지막 것은 권하지 않습니다. 글을 새로 쓸 때마다 없던 음절이 하나쯤은 나오고, 그러면 그 글자만 다른 서체로 그려집니다. rest 조각을 두는 편이 안전합니다.