
웹 개발하면서 폰트는 항상 CDN 방식으로 가져와서 사용했더니 다른 방식에 대해서는 거의 잊고 있었다가 최근에 듣던 강의에서 폰트 파일을 직접 public 폴더에 넣고 불러오는 방식을 사용하는 것을 보고 '아! 이 방법도 있었지!' 생각이 들면서 그럼 어떤 방식이 더 좋은 걸까? CDN에서 가져오는 게 좋을까, 아니면 직접 파일로 가져오는 게 좋을까? 의문점이 생겼다. 이 의문을 해결하기 위해 찾아보다가 결국 웹폰트 최적화에 대해 공부해야겠다는 생각이 들었다. 이번 기회에 웹폰트 최적화에 대해 제대로 정리해 보려 한다. 👩🏻💻💭
🔎 폰트 최적화에 대해 알아보기 전에, 먼저 웹에서 사용할 수 있는 폰트 종류에 대해 정리해 봤다.
웹 폰트는 사이트의 디자인과 브랜딩에 중요하지만, 폰트 파일은 생각보다 크기 때문에 성능에 영향을 미친다.
각 폰트 파일마다 별도의 네트워크 요청이 필요하다. 폰트 패밀리 하나에도 보통 레귤러, 볼드, 이탤릭 등 여러 스타일이 있고, 각각 별도의 HTTP 요청을 발생시킨다. HTTP/1.1 환경에서는 동시 연결 수 제한으로 인해 이런 요청이 병목 현상을 일으킬 수 있다.
특히 무거운 폰트 파일이나 여러 폰트 웨이트를 사용할 경우 다운로드 시간이 길어진다. 한글 폰트는 영어 폰트에 비해 파일 크기가 훨씬 크다. 일반적인 영문 폰트가 20~30KB 정도인 반면, 한글 폰트는 기본만 해도 1~2MB에 달하는 경우가 많다. 이는 특히 모바일 환경이나 느린 네트워크에서 큰 문제가 될 수 있다.
폰트가 로드될 때까지 텍스트 표시가 지연되거나 스타일 변경될 수 있다. 이런 현상은 크게 두 가지로 나타난다.
🌟 FOIT(Flash of Invisible Text) : Chrome, Safari 등의 브라우저에서 웹폰트 로딩 중에 텍스트가 보이지 않는 현상. 사용자는 글자 위치에 아무것도 보이지 않다가 폰트가 로드되면 갑자기 텍스트가 나타나는 것을 경험한다.
🌟 FOUT(Flash of Unstyled Text) : IE, Edge 등에서 웹폰트 로딩 중에 기본 폰트로 먼저 보이다가 로딩 완료 후 바뀌는 현상. 텍스트는 보이지만 스타일이 갑자기 바뀌면서 레이아웃이 흔들릴 수 있다.
폰트가 로드되면서 레이아웃이 변경되어 사용자 경험을 해칠 수 있다.
CLS(Cumulative Layout Shift)는 페이지 로딩 중에 레이아웃이 얼마나 많이 바뀌는지 측정하는 지표다. 웹폰트가 로드되면서 글자 크기나 간격이 바뀌면 버튼이나 이미지 등 다른 요소들의 위치도 함께 이동해, 사용자가 클릭하려던 요소가 갑자기 다른 위치로 이동하는 경험을 할 수 있다. 이는 구글의 Core Web Vitals에서 중요하게 측정하는 성능 지표 중 하나다.
한글 폰트는 수천 개의 완성형 글자를 포함하고 있어 파일 크기가 매우 크다. 서브셋 폰트는 실제로 사용하는 글자만 추출해서 폰트 파일을 작게 만드는 방법이다.
<!-- Google Fonts에서는 subset 파라미터로 간단하게 적용 가능 -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap&subset=korean" rel="stylesheet">
직접 서브셋 만들려면 unicode-range를 설정하거나 전문 도구를 사용할 수 있다.
@font-face {
font-family: 'Open Sans';
src: local('Open Sans Regular'), local('OpenSans-Regular'),
url('/fonts/open-sans-subset.woff2') format('woff2');
font-weight: 400;
font-style: normal;
unicode-range: U+AC00-D7AF; /* 한글 범위 설정 */
}
U+0020-002F, U+003A-0040, U+005B-0060, U+007B-007EU+0000-FFFFU+AC00-D7AF폰트 파일 포맷에 따라 압축률과 로딩 속도가 달라진다. 최신 포맷을 우선적으로 사용하면 파일 크기를 크게 줄일 수 있다.
@font-face {
font-family: 'MyWebFont';
src: url('myfont.woff2') format('woff2'), /* 최신 브라우저 */
url('myfont.woff') format('woff'), /* 대부분의 브라우저 */
url('myfont.ttf') format('truetype'); /* 구형 브라우저 */
}
브라우저는 선언된 순서대로 지원하는 첫 번째 포맷을 사용하므로, 가장 효율적인 포맷을 먼저 작성하는 것이 좋다.
font-display 속성은 폰트가 로드되는 동안 텍스트가 어떻게 표시될지 제어한다. 이를 통해 FOIT나 FOUT 현상을 관리할 수 있다.
@font-face {
font-family: 'MyWebFont';
src: url('webfont.woff2') format('woff2');
font-display: swap; /* 이 부분이 중요 */
}
auto : 브라우저 기본값 (대부분 FOIT)swap : 폰트가 로딩 중에는 기본 폰트로 즉시 표시, 로드 완료 후 웹폰트로 교체 (FOUT 발생)block : 짧은 시간(보통 3초) 동안 텍스트를 표시하지 않다가 폰트가 로드되면 표시 (FOIT와 유사)fallback : 짧은 차단 기간 후 기본 폰트로 표시, 폰트가 빨리 로드되면 교체optional : 브라우저가 네트워크 상태에 따라 폰트 로드 여부 결정 (느린 연결에서는 로드 안 함)대부분의 경우 swap이 사용자 경험 측면에서 가장 좋은 선택이다. 사용자가 콘텐츠를 즉시 볼 수 있기 때문이다.
중요한 폰트는 미리 로드하도록 지시할 수 있다. 이렇게 하면 브라우저가 다른 리소스보다 폰트를 우선적으로 가져온다.
<link rel="preload" href="webfont.woff2" as="font" type="font/woff2" crossorigin>
preload: 현재 페이지에서 곧 필요한 리소스를 미리 로드prefetch: 다음 페이지에서 필요할 리소스를 미리 로드preconnect: 외부 도메인과의 연결을 미리 설정 (CDN 사용 시 유용)⚠️ 주의할 점은 너무 많은 리소스를 preload하면 오히려 다른 중요한 리소스의 로딩을 방해할 수 있으므로, 꼭 필요한 폰트만 preload 하는 것이 좋다.
여러 폰트 웨이트나 스타일이 필요할 때 각각 다른 파일을 로드하는 대신, 하나의 변수 폰트로 모든 두께를 커버할 수 있다.
@font-face {
font-family: 'MyVariableFont';
src: url('myvariablefont.woff2') format('woff2-variations');
font-weight: 100 900;
}
.light-text {
font-weight: 300;
}
.bold-text {
font-weight: 700;
}
변수 폰트는 여러 스타일을 하나의 파일에 통합하므로, 여러 폰트 웨이트나 스타일을 사용하는 사이트에서 특히 효과적이다. 다만 아직 모든 폰트가 변수 폰트로 제공되지는 않는다.
사용자의 컴퓨터에 이미 설치된 폰트가 있다면 다운로드 없이 그걸 먼저 사용하도록 설정할 수 있다.
@font-face {
font-family: 'MyWebFont';
src: local('Noto Sans KR'),
url('webfont.woff2') format('woff2');
}
이렇게 하면 'Noto Sans KR'이 사용자 컴퓨터에 이미 설치되어 있을 경우 그걸 사용하고, 없으면 웹폰트를 다운로드한다. 시스템에 설치된 폰트를 활용하므로 추가 다운로드 없이 즉시 텍스트를 표시할 수 있다.
<link> 태그 사용<!-- link 방식 (권장) -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap">
@import 규칙 사용/* @import 방식 */
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap');
@font-face 직접 정의 (로컬 폰트 사용 시)@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
}
<link> : 병렬 로딩으로 속도 빠름, 모든 브라우저 지원
@import : CSS 파일이 모두 다운로드된 후에야 로딩 시작, 직렬 방식으로 상대적으로 느림. IE11 이하 지원 안 됨
@font-face : 직접 폰트 파일 지정 시 사용, 자체적으로 성능 차이 없음
CSS의 @font-face 규칙 외에도, JavaScript의 Font Loading API를 사용하면 폰트 로딩을 더 세밀하게 제어할 수 있다. 특히 중요한 텍스트가 빨리 보여야 하거나 폰트 로딩 상태에 따라 UI를 변경하고 싶을 때 유용하다.
// 모든 폰트가 로드됐는지 확인
document.fonts.ready.then(function() {
console.log('모든 폰트가 로드됐다!');
document.documentElement.classList.add('fonts-loaded');
});
// 특정 폰트 로드하기
const font = new FontFace('MyWebFont', 'url(webfont.woff2)', {
style: 'normal',
weight: '400',
display: 'swap'
});
font.load().then(function(loadedFont) {
document.fonts.add(loadedFont);
document.body.style.fontFamily = 'MyWebFont, sans-serif';
});
이 방식을 사용하면
1. 조건부 폰트 로딩: 필요한 경우에만 특정 폰트를 로드
2. 폰트 로딩 상태 추적: 폰트 로드 시점에 UI 업데이트
3. 폰트 로딩 실패 처리: 네트워크 오류 시 대체 방안 제공
아직 직접 사용해 본 적은 없지만, 복잡한 폰트 전략이 필요하거나 사용자 경험을 최대한 세밀하게 제어하고 싶을 때 유용할 것 같다.
내가 고민했던 것은 이런 상황이었다.
@font-face {
font-family: 'Ownglyph';
src: url("https://fastly.jsdelivr.net/gh/projectnoonnu/2411-3@1.0/Ownglyph_ParkDaHyun.woff2") format('woff2');
}
@font-face {
font-family: 'Ownglyph';
src: url("/Ownglyph_ParkDaHyun.ttf");
}
첫 번째는 눈누에서 제공하는 CDN 주소를 직접 참조하는 방식이고, 두 번째는 폰트 파일을 다운로드 받아 내 프로젝트의 public 폴더에 넣고 불러오는 방식이다.


⚙️ 개발 환경에서는 직접 호스팅 방식 5~11ms 정도 걸리고 TTF 파일로 첫 로딩에 2.0MB가 소요되었지만, 이는 실제 사용자 경험과는 차이가 있다.
두 방식 모두 캐싱 이후에는 성능 차이가 거의 없어 보였다. 따라서 선택은 프로젝트 상황과 우선순위에 따라 달라질 수 있다.
1. 단기적 해결책: CDN 방식 유지하면서 최적화
2. 장기적 해결책: 직접 호스팅으로 전환하되 최적화 적용
소규모 또는 개인 프로젝트에서는 CDN의 편리함이 큰 장점이고, 대규모 상업 서비스에서는 직접 호스팅의 안정성이 중요할 것 같다.
내 현재 프로젝트에서는 일단 CDN 방식을 유지하면서 font-display: swap을 적용하고, 추후 프로젝트가 성장하게 된다면 직접 호스팅으로 전환하는 계획을 세우는 것이 좋을 것 같다.
웹 폰트 최적화에 대해 공부해 보니, 이전에 그냥 폰트 링크만 넣어서 사용했던 방식에서 많은 개선이 가능하다는 걸 알게 됐다.
특히 한글 폰트는 수천 개의 글리프를 포함하고 있어 파일 크기가 매우 크기 때문에 최적화가 더욱 중요하다. 최적화 방법 중에서도 서브셋팅과 WOFF2 같은 최신 포맷 사용이 가장 효과적이라는 점을 배웠다.
또한 font-display: swap 속성을 설정하면 폰트가 로딩되는 동안 시스템 폰트로 텍스트를 먼저 보여주기 때문에 사용자가 콘텐츠를 바로 볼 수 있어 사용자 경험이 크게 향상된다는 것도 알게 되었다.
CDN vs 직접 호스팅이라는 고민으로 시작했지만, 개발환경에서는 차이가 있었던 두 방식도 프로덕션 환경과 브라우저 캐싱이 적용된 후에는 성능상 큰 차이가 없다는 것을 추가 테스트를 통해 발견했다. 따라서 순수한 로딩 성능보다는 안전성, 유지보수 용이성, 커스터마이징 가능성 등 다른 요소들을 기준으로 프로젝트에 맞는 방식을 선택하는 것이 중요하다.
결국 웹 최적화는 완벽한 해결책보다는 상황에 맞는 트레이드오프를 찾아가는 과정이라는 것을 깨달았다. 내 프로젝트의 규모, 목적, 그리고 우선순위에 따라 적절한 전략을 선택하는 것이 가장 중요한 것 같다.🤓