← 개발 노트 목록

한글 폰트를 늦게 불러온 뒤, 글이 밀리기 시작했다

이 포트폴리오에서 폰트 CSS를 분리한 변경과 폴백 글꼴 보정을 커밋·파일 크기·실제 코드로 따라갑니다.

포트폴리오 수정 기록 · 2026.10.21

글꼴은 그대로 두고 불러오는 순서만 바꿨는데, 다음 수정에서는 줄 높이를 손봐야 했습니다. 이 사이트의 9월 커밋에는 그 과정이 남아 있습니다. 제목용 폰트 CSS를 분리하고, 본문 폰트에도 같은 방식을 적용한 다음, 폰트가 바뀔 때 콘텐츠가 움직이는 문제를 보정했습니다. 단순히 “폰트를 비동기로 불러오면 빠르다”로 끝내기에는 뒷부분이 있는 변경입니다.

시작은 제목에만 쓰는 폰트였다

제목은 IBM Plex Sans KR의 600·700 굵기, 본문은 Pretendard 400을 사용합니다. 처음에는 폰트 CSS를 다른 스타일처럼 바로 import했습니다.

// 변경 전: src/layouts/Layout.astro
import "@fontsource/ibm-plex-sans-kr/600.css";
import "@fontsource/ibm-plex-sans-kr/700.css";

이 방식에서는 폰트 선언도 페이지 스타일을 만드는 번들에 포함됩니다. 제목 몇 줄에 쓰는 글꼴이라도, 패키지의 CSS가 짧다는 뜻은 아닙니다.2aebe64 커밋은 렌더링을 막는 CSS를 줄이기 위해 이 두 import를 바꿨습니다. 당시 커밋 메시지에는 174KB에서 26KB로 줄었다는 기록이 있습니다. 아래 표는 그때의 측정값을 재사용한 것이 아니라, 현재 잠금 파일로 설치된 패키지를 다시 읽은 결과입니다.

현재 설치본의 원본 파일 크기 — 압축 전, 네트워크 전송량 아님
파일CSS 크기@font-face
IBM Plex Sans KR 60059,516바이트94개
IBM Plex Sans KR 70059,516바이트94개
Pretendard 400274바이트1개

IBM Plex의 선언에는 unicode-range가 붙어 있습니다. 브라우저는 표시할 문자에 필요한 범위의 폰트 파일을 선택합니다. 선언이 94개라고 해서 방문할 때 94개를 모두 받는 것은 아닙니다. 하지만 그 선택에 필요한 CSS 자체는 읽어야 합니다. 두 굵기를 합치면 원본 CSS만 119,032바이트입니다.

Pretendard는 반대입니다. CSS는 짧지만 그 안에서 가리키는 WOFF2 파일 하나가 765,892바이트, 약 748KiB입니다. 파일 이름은 pretendard-latin-400-normal.woff2인데 이름만 보고 작은 영문 전용 파일이라고 판단하면 크기를 놓치게 됩니다. CSS 크기와 폰트 파일 크기를 따로 봐야 하는 이유입니다.

CSS를 없앤 게 아니라, 본문 표시를 기다리지 않게 했다

바뀐 코드는 ?url로 스타일시트의 주소를 가져옵니다. 이어서 preload와 link를 사용해 별도로 불러옵니다. 아래는 현재 파일에서 제목용 600 굵기만 추린 코드입니다.

import ibmPlex600Url from "@fontsource/ibm-plex-sans-kr/600.css?url";

<link rel="preload" as="style" href={ibmPlex600Url} />
<link
  rel="stylesheet"
  href={ibmPlex600Url}
  media="print"
  onload="this.media='all'"
/>
<noscript>
  <link rel="stylesheet" href={ibmPlex600Url} />
</noscript>

화면에 당장 적용하지 않는 print 스타일시트로 받아 둔 뒤, 로드가 끝나면 media를 all로 바꿉니다. preload는 스타일시트 요청을 일찍 시작하게 합니다. 폰트 CSS가 준비될 때까지 본문 표시 전체를 기다리는 대신, 우선 시스템 글꼴로 읽을 수 있게 하는 구성입니다. 자바스크립트가 꺼진 경우에는 onload 코드가 실행되지 않으므로 noscript의 일반 스타일시트가 필요합니다.

f68fc86 커밋에서 본문의 Pretendard도 같은 방식으로 바뀌었습니다. 파일 용량 자체를 줄이는 작업은 아닙니다. 네트워크가 느리면 폴백 글꼴로 보이는 시간이 길어지고, preload 요청도 다른 리소스와 대역폭을 나눠 씁니다.

그다음 문제는 글꼴이 바뀌는 순간이었다

먼저 보인 시스템 글꼴과 나중에 도착한 웹폰트의 줄 높이가 같지 않으면, 글 아래의 요소가 밀립니다.37ffd29 커밋에는 이 교체 과정에서 모바일 CLS가 발생했다는 기록과 폴백 보정 코드가 있습니다.

@font-face {
  font-family: "Pretendard Fallback";
  src: local("Arial");
  ascent-override: 95.21%;
  descent-override: 24.12%;
  line-gap-override: 0%;
}

--font-body: "Pretendard", "Pretendard Fallback",
  -apple-system, system-ui, sans-serif;

Pretendard가 도착하기 전에는 Arial에 위쪽·아래쪽 여백과 줄 간격을 지정한 폴백을 사용합니다. 같은 커밋에 IBM Plex용 보정값도 있습니다. 폰트 로딩을 되돌리는 대신, 교체 전후 줄 상자의 차이를 줄이는 방법을 택한 것입니다.

이 값은 다른 폰트에도 그대로 붙여 쓸 수 있는 상수가 아닙니다. Arial이 없는 기기에서는 뒤의 시스템 글꼴로 넘어가며, 세로 메트릭을 맞춰도 글자 너비가 같아지지는 않습니다. 한 줄에 들어가는 글자 수가 바뀌면 줄바꿈 때문에 문단 높이가 달라질 수 있습니다. 따라서 코드에 보정값이 있다는 것과 모든 기기에서 CLS가 0이라는 것은 다른 이야기입니다.

같은 파일을 직접 확인하려면

의존성을 설치한 저장소 루트에서 아래 명령을 실행하면 표의 CSS 크기와 선언 수를 확인할 수 있습니다. 패키지 버전이 바뀌면 결과도 달라질 수 있습니다.

node --input-type=module <<'JS'
import { readFileSync, statSync } from 'node:fs';
for (const name of [
  '@fontsource/ibm-plex-sans-kr/600.css',
  '@fontsource/ibm-plex-sans-kr/700.css',
  '@fontsource/pretendard/400.css',
]) {
  const url = new URL(import.meta.resolve(name));
  const css = readFileSync(url, 'utf8');
  console.log(name, statSync(url).size,
    (css.match(/@font-face/g) || []).length);
}
JS

로딩 동작은 pnpm build 후 pnpm preview로 확인할 수 있습니다. 브라우저 개발자 도구에서 캐시를 끄고 네트워크 속도를 낮춰 보면, 폴백에서 웹폰트로 넘어가는 시점을 보기 쉽습니다. Network에서는 CSS와 Font를 따로 보고, Performance에서는 Layout Shift가 생긴 요소를 확인합니다. 폰트가 이미 캐시된 상태의 새로고침만 보면 이 과정을 놓치기 쉽습니다.

이 기록에서 확인한 것은 파일 크기와 실제 코드, 커밋의 변경 순서입니다. 현재 배포의 PageSpeed 점수를 다시 측정한 결과는 아닙니다. 다음에 폰트를 바꾼다면 import 한 줄만 고치는 대신, 분할 방식과 파일 크기, 교체 시 줄바꿈을 함께 확인해야 합니다.

원본 코드: Layout.astro· 문법 참고: MDN ascent-override