
프로젝트 성능을 모니터링하며 개선하던 중, Lighthouse에서 '렌더링 차단 리소스 제거하기'와 '웹 폰트가 로드되는 동안 텍스트가 계속 표시되는지 확인하기' 경고를 발견했다. 이를 해결하려 했던 방법을 공유해보고자 한다.
나는 웹 폰트는 Google Fonts에서 import해오는디? 왜 이 경고가 떴나 고찰한 내용이다.
내 프로젝트의 경우 폰트를 css파일에서 import하여 사용하고 있었다.

나눔 고딕 폰트를 불러올 때 google fonts에서 가져오기 때문에, http요청이 가면서 지연 시간이 걸린다.
이 때, 요청은 동기적으로 작동한다. 브라우저 렌더링 순서는
DOM트리 구성CSSOM 트리 구성DOM + CSSOM => 렌더 트리 구성순서로 이어지기 때문에, CSS에서 동기적으로 import가 이루어지면, 요청이 완료되기 전까지 CSSOM이 구성되지 않는다.
따라서 폰트 응답이 오기 전에는 CSSOM이 구성되지 않기 때문에 렌더링 시간이 지연되는 것이다. 폰트 파일 크기는 작긴 하지만, 더 많이 불러오는 경우도 있기 때문에 해결 방법을 공유한다.
그렇다면 폰트 요청을 비동기적으로 보내야 하는게 관건이다. 폰트를 지연로딩(Lazy Loading) 시켜서 첫 렌더링 시간을 줄여보자.
MDN 문서
웹이 발전함에 따라서 사용자에게 보내지는 에셋의 양이 증가하면서, 이를 해결하기 위한 방법으로 등장한 전략이다.
중요하지 않은 리소스를 나중에 로드하도록 명령하여 중요 렌더링에 걸리는 시간을 단축하는 것이다. 이 포스팅의 경우에는 웹폰트들이 그 대상이 될 것이다.

html에서 폰트를 불러올 때, 속성으로 rel="preload"를 붙여주면 비동기로 폰트를 불러올 수 있다.
//index.css
@import url(http://fonts.googleapis.com/earlyaccess/nanumgothic.css);
@import url(https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900);
원래는 나는 나눔고딕 폰트와, Inter폰트를 사용하기 위해서 index.css에서 폰트를 직접 import를 했다.
이 부분을 지우고, body나 root에
body {
font-family: 'Nanum Gothic', Inter, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
을 넣어준다.
대체 폰트들은 호환성을 고려해 각 환경에서 사용하는 범용 폰트로 폴백 체인을 구성했다.
그리고 폰트를 import하기 위해 index.html에 가서
<head>
<link rel="preload" href="https://fonts.googleapis.com/earlyaccess/nanumgothic.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload"
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>
rel="preload" 속성을 넣어 렌더링을 차단하지 않고 폰트를 비동기적으로 로드this.onload=null로 이벤트 핸들러를 제거한다.(메모리 정리)'this.rel='stylesheet'로 로드된 css를 실제 스타일 시트로 전환한다.이렇게 하면 폰트가 로드되는 동안 렌더링을 먼저 시작할 수 있다.
이 방식은 js가 활용되는 방식으므로,
<noscript>
<link rel="stylesheet"
href="https://fonts.googleapis.com/earlyaccess/nanumgothic.css">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap&subset=latin">
</noscript>
js가 작동하지 않을 때, 기존 방식대로 동기적으로 로드하도록 추가했다.
Lighthouse에서 렌더링 차단 리소스 경고가 사라졌다!👍

이 lighthouse 경고는 폰트가 로드되는 동안에 텍스트가 표시가 되지 않아서 발생하는 문제이다. 폰트계의 skeleton UI가 표시되지 않아서 나타나는 경고라고 볼 수 있겠다.

해결 방법은 의외로 간단한데,
@font-face {
font-display: swap;
}
을 넣으면 된다.
font-display 속성은 여러가지가 있는데, MDN font-display
를 참고하자. 간단히 정리하자면,
@font-face {
font-display: auto; /* auto: 브라우저 기본값 사용 (대부분 block과 유사) */
font-display: block;
/* block: 3초간 텍스트 숨김 → 폰트 로드되면 표시
- 장점: FOUT(Flash of Unstyled Text) 방지
- 단점: 사용자가 3초간 텍스트를 못 봄 */
font-display: swap;
/* swap: 즉시 폴백 폰트로 표시 → 폰트 로드되면 교체
- 장점: 텍스트를 즉시 볼 수 있음
- 단점: FOUT 발생 가능 */
font-display: fallback;
/* fallback: 100ms 동안 텍스트 숨김 → 폴백 폰트 표시 → 3초 내 로드되면 교체
- 장점: FOUT 최소화 + 적절한 대기 시간
- 단점: 짧은 시간 동안 텍스트 안 보임 */
font-display: optional;
/* optional: 100ms 동안 텍스트 숨김 → 네트워크 상태에 따라 폰트 적용 여부 결정
- 장점: 네트워크 상태 고려, 레이아웃 시프트 없음
- 단점: 웹폰트가 적용되지 않을 수 있음 */
}
나의 경우에는 swap을 사용해서, 로드가 완료되는 대로 즉시 교체하도록 설정했다. 네트워크 환경이 좋지 않은 사용자가 많은 경우에는, fallback을 쓰는 것도 괜찮은 선택지인 것 같다.
아무것도 설정하지 않을 경우, default는 auto이기 때문에, 렌더링이 시작될 때 폰트가 로드되지 않았으면 3초 간 텍스트를 숨기는 상태인 것이다. 3초는 길기 때문에 바꿔주는 것이 좋다.
👀 FOUT(Flash of Unstyled Text) 이란?
폰트가 로드되기 전에 스타일이 적용되지 않은 텍스트가 기본 스타일로 잠깐 보이는 현상.
내 경우에는 FCP의 향상이 우선순위였기 때문에, FOUT은 신경쓰지 않기로 했다.
<link rel="stylesheet"
href="https://fonts.googleapis.com/earlyaccess/nanumgothic.css">
이 태그를 보면, 구글 api에서 받아오는 나눔 고딕에서 받아오는 파일이 early access로 제공되고 있다. 자료를 찾아본 결과, 2018년 6월에 정식으로 한글 폰트들이 풀렸는데, 나눔 고딕도 그 때 풀렸다고 한다. 프로젝트를 진행할 때 옛날 자료를 긁어왔나보다.. 무지성 복붙의 위험성
얼리엑세스 버전은 기능이 제한적이고, 글꼴을 전체적으로 다 가져오기 때문에, 용량이 큰 편이다.
정식버전으로 바꿔주도록 하자.
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Nanum+Gothic&display=swap">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap">
구글 폰트에서 폰트를 가져올 때, Get Embed Code를 통해 가져오게 될 텐데, 그 경우에는 보통 쿼리스트링으로 &display=swap이 들어가 있다.
이 경우에는
stylesheet에font-display:swap속성이 자동으로 들어가기 때문에, css 파일에는 굳이 넣지 않아도 된다.
폰트를 로드하는데 지연시간이 길어지는건 Lighthouse 경고의 직접적인 원인은 아니지만, 1650ms나 걸린다니까 줄여보는게 인지 상정이다.
현재 Google Fonts를 통해 불러오는 폰트 파일(ttf 등)의 가짓수는 27개다. 따라서 아무리 swap이 빠르다고 해도, 불러오는 데 시간이 걸린다. 따라서 필요한 것들만 추려서 가져와보자.
Google Fonts url에 쿼리스트링을 추가하자.
<link rel="preload"
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Nanum+Gothic&display=swap&subset=korean,latin"
as="style" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload"
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap&subset=latin"
as="style" onload="this.onload=null;this.rel='stylesheet'">
&subset=korea,latin, &subset=latin을 추가해주어서, 로드되는 폰트파일 수를 줄였다.
이제 확인해보자.
전

후

폰트 가짓수가 14개로 줄어들었다.
DOMContentLoaded 역시 150밀리초 정도 줄어든 것을 확인할 수 있다.
그럼 이제 Lighthouse를 확인해보자.

😆😆😆😆
이번 작업을 통해 브라우저가 HTML, CSS, JS를 어떻게 작동하는지에 대해 더 깊이 알 수 있었다.
사실 폰트를 로컬에 저장해서, 서버에서 바로 뿌려주는게 이상적이지만, 절차에 비해 유의미한 차이가 나지 않는다. 다음에 기회가 되면 로컬에 직접 저장해서 해봐야겠다.
React 같은 가상 DOM을 사용하는 프레임워크에서는 이런 최적화가 성능에서 크게 중요한 이슈가 아니지만, SSR환경이나 정적 사이트에서는 폰트 로드 최적화는 효과가 있을 것 같다. 지식이 늘었으니 뿌듯하다. 포스트 하나 작성하는데 참 오래걸리는 것 같다. 그러나 작성하면서 걸린 시간보다, 얻어가는게 더 많은 것 같다.