[WEB_DEV] 웹 폰트와 코드 스플리팅 + 업무고민이 살짝 첨가된

로선생·2025년 8월 5일

업무고민

밸런스 찾기

요즘 아주 열심히(??) 스터디를 하고 스터디를 하고 공부를 하고 사이드를 하고 발표를 하고 영어공부를 하는 나날을 보내고 있었다.
퇴근-공부-퇴근-공부의 연속 (야근야근야근특근특근파티가 벌어지기 전까지는,,)

취미좋아인간이지만,, 취미를 모두 미뤄두고 스터디만 주구장창하는 이 성과중심주의적 삶을 살 수 있었던 이유는,
내가 저연차에서 중간연차로 넘어가는 기간이기도 해서 실력을 더 쌓아야겠다고 다짐한 것도 있지만,
'언젠가 끝날 것이여서' 였다. 생각으로는 2-3년 정도만 이렇게 살면 나중에는 취미생활도 하면서 살 수 있겠지라는 순진한 생각을 했다.

그 런 데

우리 팀의 팀장님은 40대 중반이신데도 여전히 야근과 공부와 스터디를 병행하시고 있었다.. 그리고 여전히 진로고민중이셨다 ㅠ ㅠ
매니지먼트로 갈지 계속 개발자의 역량을 공부할지 고민중신..

그렇다.. 이 성과중심주의 피곤한 삶은 끝이 없었던 것이었다..

일도 중요하지만, 사실 책읽고 요가하는 시간도 나에게는 정말 행복한 시간임을 안다.

취미를 하자니 나보다 잘 하는 사람이 많아서 불안해지고, 공부만 하자니 나라는 인간이 너무 납작해진다.

이 중간 지점을 찾는 것이 아직 너무 혼란되고 어렵다.


(읽지는 않았습니다 ^.^)

이런 고민을 비단 나 혼자만 하는 것은 아니라고 생각한다. 한국인의 삶은 힘들군용..

새로운 아이디어

피그마 플러그인 만들기

회사에 디자인 시스템이 있어서, 이를 MCP로 -> 커서 -> 실제 코드화 시키는 작업을 진행하였다.
기존 문제점은 여기에서 코드 수정이 들어가고 ㅠㅠ 완전하지 못한 코드들이 나오는 이슈가 있었고 + 추가로 시간이 소요된다는 단점이 있었다.
이를 조금 더 디벨롭시켜서, 완전히 MCP를 빼고, 피그마 플러그인으로 코드화시키는 과정을 진행해보기로 하였다.

총 예상 개발기간은 총 4주 이며, 이번 주는 다른 레퍼런스 찾기 + 코드 분석 (시간이 된다면 PoC 빠르게) 해보는 주로 생각하고 있다.

웹 폰트 최적화

웹 폰트는 다운로드 시간이 걸리기 때문에, FCP등의 페이지의 로드 시간과 렌더링 시간 모두에 영향을 줄 수 있다.
또한 font display(폰트 렌더링 방식) 속성을 잘못 설정하면, CLS(누적 레이아웃 이동)을 유발할 수 있다.

CLS?
누적 레이아웃 이동은 세 가지 Core Web Vitals 측정항목 중 하나로, CLS는 페이지 전체 수명 중 가장 큰 레이아웃 이동 묶음의 점수를 측정한다.
점수는 영향 영역 비율 × 이동 거리 비율로 계산한다.

우수한 사용자 환경을 제공하기 위해 사이트는 페이지 방문의 75% 이상에서 CLS가 0.1 이하가 되도록 노력해야 한다.

JavaScript에서 CLS 측정

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('Layout shift:', entry);
  }
}).observe({type: 'layout-shift', buffered: true});


import { onCLS } from 'web-vitals';

onCLS(console.log);

웹 폰트 최적화를 위한 브라우저가 웹 폰트를 인식하는 방식

1. 발견한다

페이지의 웹 폰트는 스타일 시트의 @font-face 선언을 통해 정의된다. 하기 코드에서 확인할 수 있듯, 폰트 이름을 정의하고 리소스의 위치를 브라우저에 알린다.

@font-face {
  font-family: "Open Sans";
  src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
}

브라우저는 대역폭을 아끼기 위해, 실제 렌더링에 필요할 때까지 폰트를 다운로드 하지 않는다. 브라우저가 실제 <h1> 요소를 만나야 폰트를 다운로드 한다.

h1 {
  font-family: "Open Sans";
}

또한, @font-face 선언이 외부 스타일 시트에 있다면, 폰트는 늦게 인식되기 때문에, preload 지시어를 사용하여 브라우저가 폰트를 더 빨리 찾도록 도울 수 있다. 다만, 과도한 preload 사용은 불필요한 폰트를 다운로드하여 대역폭 낭비를 발생시킬 수 있다.

<link rel="preload" as="font" href="/fonts/OpenSans-Regular-webfont.woff2" crossorigin>

주의: 폰트는 CORS 리소스이므로, 자체 호스팅한 경우에도 crossorigin 속성이 필요합니다.

왜 폰트가 CORS?

추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 어플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.
서버가 어떤 출처(도메인, 스킴, 포트)에서 리소스를 불러올 수 있는지 브라우저에 알려준다.
CORS는 서버가 명시적으로 외부 요청을 허용할 수 있도록 해준다.

  • 출처(origin) : URL의 스킴(Protocal, Host, Port)을 통해 정의된다. 서로 다른 두 객체의 스킴이 일치 할 때 같은 출처라고 볼 수 있다.

  • 결론:
    폰트는 단순한 파일처럼 보이지만, 다음과 같은 위험성이 있다.

특정 사이트 전용의 폰트를 무단으로 사용할 수 있음
정품 폰트 사용 여부에 따른 라이선스 위반 가능성
데이터 추적을 위한 폰트 사용 (tracking font 등)
렌더링 결과나 레이아웃 변형을 통한 정보 유출 가능성

브라우저는 font 리소스를 보안 민감한 리소스로 보고, 출처가 다른 도메인에서 폰트를 요청할 때 CORS 정책을 적용한다.

  1. 당신의 웹사이트가 https://example.com이고,

  2. @font-face로 https://fonts.gstatic.com/myfont.woff2 같은 다른 도메인의 폰트를 불러올 경우,

  3. 브라우저는 이 요청을 cross-origin 요청으로 판단합니다.

  4. 이때 서버 응답에 CORS 관련 허용 헤더가 없으면, 브라우저는 보안상의 이유로 요청을 차단합니다.

Access-Control-Allow-Origin: https://example.com
<link
  rel="preload"
  href="https://fonts.gstatic.com/some-font.woff2"
  as="font"
  type="font/woff2"
  crossorigin="anonymous"
/>

c.f. 어떤 경우 CORS가 필요할까?

자바스크립트 API: fetch(), XMLHttpRequest
웹 폰트 (@font-face)
WebGL 텍스처
Canvas에 이미지 그릴 때 (drawImage())
이미지 기반 CSS 쉐이프

CORS 접근제어 시나리오 3가지
Simple Request: Preflight Request 없이 바로 Actual Request를 보내는 것
Preflight Request: 사전 확인 작업, 되는지 안되는지 먼저 물어보는 작업
Credentialed Request: 인증 관련 헤더를 포함할 떄 사용

HTML의 <head><style>로 @font-face를 인라인 선언할 수 있다. 그러나 렌더링 차단 리소스가 모두 로드된 후에야 폰트 파일 다운로드가 시작된다. 인라인 선언은 실제 필요한 폰트만 다운로드되므로 preload보다 효율적일 수 있다.

2) 왜 프리로드하면 다 다운로드 되는거지

2. 다운로드 한다.

웹 폰트가 필요한 것으로 확인되면 브라우저가 다운로드를 시작하고, 웹 폰트의 수, 크기, 형식은 다운로드 속도에 영향을 준다.

웹 폰트 자체 호스팅
구글 폰트 같은 제3자 서비스를 쓰면 연결 비용이 추가되기 때문에, preconnect을 사용하면 이런 비용을 줄일 수 있다.

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

3) preconnect 가 머 죠

당연히 자체 호스팅은 외부 연결 지연을 피할 수 있고 일반적으로 더 빠르다. (자체 호스팅 시 CDN, HTTP/2 또는 3, 적절한 캐시 헤더 설정이 필요)

WOFF2 추천
4) 이 게 먼 데

웹 폰트 서브셋

일부만 사용할 경우, 필요 없는 glyphs 줄여, 폰트 크기를 줄일 수 있다.

https://fonts.googleapis.com/css?family=Roboto&subset=latin
https://fonts.googleapis.com/css?family=Monoton&text=Welcome

3. 폰트 렌더링 방식

기본적으로 브라우저는 웹 폰트가 다운로드될 때까지 렌더링을 차단한다. 렌더링 동작은 font-display 속성으로 제어할 수 있다.

block: 폰트가 로드될 때까지 텍스트 렌더링을 차단합니다 (Safari는 무기한).
swap: 즉시 대체 폰트를 보여주고, 웹 폰트가 로드되면 교체합니다.
fallback: 짧은 차단 후 대체 폰트 표시, 이후 웹 폰트로 전환합니다.
optional: 100ms 이내에 로드되지 않으면 웹 폰트를 생략하고 대체 폰트를 사용합니다.

코드 스플리팅

큰 JavaScript 파일을 로드하면 페이지 속도와 반응성이 저하된다. 페이지 요소들은 초기 HTML과 CSS로 인해 표시되기는 하나, JavaScript 파일이 너무 크다면 해당 상호작용 요소들이 작동하는 데 필요한 JavaScript가 아직 파싱 및 실행 중일 수 있다. 이는 사용자 경험을 저하시킨다. INP(다음 페인트까지의 상호작용 지연)

JS메인 스레드에서 파싱 및 컴파일 되기 때문에, 메인 스레드 차단이 발생한다. TBT(총 차단 시간)

따라서 이 문제를 해결하기 위해 두 가지 접근을 할 수 있다.
1) 페이지 작동에 필요한 JavaScript만 로드
2) 코드 스플리팅이라는 기법을 통해 나중에 로드

Lighthouse는 JavaScript 실행이 2초 이상 걸리면 경고를 표시하고, 3.5초 이상 걸리면 실패로 처리한다.

코드 스플리팅을 통해 JS 번들을 두 부분으로 나눌 수 있다.

  • 페이지 로드시 필요한 JavaScript
  • 이후 시점에 로드 가능한 나머지 JavaScript

코드 스플리팅 구현
동적 import() 문법을 사용하여 구현

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  const { validateForm } = await import('/validate-form.mjs');
  validateForm();
}, { once: true });

오직 사용자가 폼의 <input> 필드를 블러할 때에만, validate-form.mjs 모듈은 다운로드되고, 파싱되며, 실행된다.

c.f. React는 React.lazy 문법을 통해 동적 import()를 추상화하는데, 내부적으로는 동적 import 를 활용한다.

webpack, Parcel, Rollup, esbuild 같은 JavaScript 번들러는 JavaScript 번들을 더 작은 청크로 분할하도록 설정할 수 있다. 소스 코드에서 동적 import() 호출을 감지할 때마다 청크를 나눈다.

코드 스플리팅 유의사항

1) 가능하면 번들러를 사용하기.
: 번들러는 단순히 JavaScript 코드에 최적화를 적용할 뿐만 아니라, 번들 크기와 압축률 같은 성능 요소를 균형 있게 조정하는 데에도 매우 효과적.
또한 모듈 트리를 번들링하지 않으면 각 모듈이 별도의 HTTP 요청이 된다.

2) 스트리밍 컴파일을 실수로 비활성화하지 마세요

webpack
webpack에는 SplitChunksPlugin이라는 플러그인이 내장되어 있다. 이 플러그인을 사용하면 번들러가 JavaScript 파일을 어떻게 나눌지 설정할 수 있다.

SplitChunksPlugin의 동작 chunks 옵션

chunks: async는 기본값이며, 동적 import() 호출을 의미합니다.
chunks: initial은 정적 import 호출을 의미합니다.
chunks: all은 동적/정적 import 모두를 다루며, 초기 및 비동기 import 간에 청크를 공유할 수 있게 합니다.

webpack이 동적 import() 문을 만나면, 해당 모듈을 위한 별도의 청크를 생성한다.

import myFunction from './my-function.js';

myFunction('Hello world!');

if (condition) {
  await import('/form-validation.js');
}

위 코드 예시의 기본 webpack 설정은 두 개의 별도 청크를 생성

  • main.js 청크(초기 청크로 분류됨)는 main.js와 ./my-function.js 모듈을 포함
  • async 청크는 form-validation.js만 포함 (이 청크는 condition이 참일 때만 다운로드)
profile
이제는 이것저것 먹어요

0개의 댓글