subfont + tailwindcss 같이 쓰기
Written in 2022/05/15 05:11:55 UTC, categoried as web
결론부터 말하자면
둘을 같이 쓰는 순간 문제가 발생한다. 다행히 나는 해결한 상태다. 무슨 문제가 발생했고 어떻게 해결했는지 이 글에서 자세히 설명한다. 그러니 이 글을 읽는 여러분들은 걱정할 필요 없다.
멸망의 전조
블로그를 개발할 당시 고정폭 서체에 꽂혀 어떻게든 써보려고 발악을 했다. 선뜻 채택하지 못한 주된 이유는 폰트 크기이다. 난 한국인이므로 당연히 한글로 포스팅을 할 것이고, 따라서 폰트가 반드시 cjk
를 지원해야 한다. 문제는 이러면 크기가 너무 커진다는 것. 짧은 포스트 하나 읽자고 수십 MiB씩 트래픽이 오가는건 심히 좋지 않다.
핵폭탄 1번 재료: subfont
그러다 subfont
라는 훌륭한 도구를 발견했다. 이 녀석은 똑똑하게도 웹 페이지(온라인/오프라인 둘 다 가능)를 분석해서 폰트를 찾는다. 그리고 원본 폰트 파일을 수정해서 실제 사용되는 글리프만 들어있는 서브셋으로 썰어버린다. 도입 결과 200MiB가 넘는 폰트가 수십 KiB 수준으로 줄어들었다...
dist/about/index.html: 1 font (2 variants) in use, 23.5 MB total. Created subsets: 109 kB total
NotoSansMonoCJK:
700 : 128/44810 codepoints used (15 on this page), 12.1 MB (woff2) => 29.4 kB (woff2)
400 : 584/44810 codepoints used (39 on this page), 11.4 MB (woff2) => 80 kB (woff2)
dist/fallback/index.html: 1 font (2 variants) in use, 23.5 MB total. Created subsets: 109 kB total
NotoSansMonoCJK:
700 : 128/44810 codepoints used (19 on this page), 12.1 MB (woff2) => 29.4 kB (woff2)
400 : 584/44810 codepoints used (38 on this page), 11.4 MB (woff2) => 80 kB (woff2)
dist/index.html: 1 font (2 variants) in use, 23.5 MB total. Created subsets: 109 kB total
NotoSansMonoCJK:
700 : 128/44810 codepoints used (14 on this page), 12.1 MB (woff2) => 29.4 kB (woff2)
400 : 584/44810 codepoints used (95 on this page), 11.4 MB (woff2) => 80 kB (woff2)
dist/posts/blog-renewal/index.html: 1 font (2 variants) in use, 23.5 MB total. Created subsets: 109 kB total
NotoSansMonoCJK:
700 : 128/44810 codepoints used (45 on this page), 12.1 MB (woff2) => 29.4 kB (woff2)
400 : 584/44810 codepoints used (437 on this page), 11.4 MB (woff2) => 80 kB (woff2)
dist/posts/implement-ssr-on-svelte/index.html: 1 font (2 variants) in use, 23.5 MB total. Created subsets: 109 kB total
NotoSansMonoCJK:
700 : 128/44810 codepoints used (37 on this page), 12.1 MB (woff2) => 29.4 kB (woff2)
400 : 584/44810 codepoints used (328 on this page), 11.4 MB (woff2) => 80 kB (woff2)
dist/posts/k8s-cicd/index.html: 1 font (2 variants) in use, 23.5 MB total. Created subsets: 109 kB total
NotoSansMonoCJK:
700 : 128/44810 codepoints used (37 on this page), 12.1 MB (woff2) => 29.4 kB (woff2)
400 : 584/44810 codepoints used (277 on this page), 11.4 MB (woff2) => 80 kB (woff2)
dist/posts/macos-big-sur-beta-10-mds-stores-huge-memory-usage/index.html: 1 font (2 variants) in use, 23.5 MB total. Created subsets: 109 kB total
NotoSansMonoCJK:
700 : 128/44810 codepoints used (29 on this page), 12.1 MB (woff2) => 29.4 kB (woff2)
400 : 584/44810 codepoints used (138 on this page), 11.4 MB (woff2) => 80 kB (woff2)
dist/posts/meteorknight-engine-devlog-1/index.html: 1 font (2 variants) in use, 23.5 MB total. Created subsets: 109 kB total
NotoSansMonoCJK:
700 : 128/44810 codepoints used (20 on this page), 12.1 MB (woff2) => 29.4 kB (woff2)
400 : 584/44810 codepoints used (149 on this page), 11.4 MB (woff2) => 80 kB (woff2)
dist/posts/personal-svelte-pros-and-cons/index.html: 1 font (2 variants) in use, 23.5 MB total. Created subsets: 109 kB total
NotoSansMonoCJK:
700 : 128/44810 codepoints used (67 on this page), 12.1 MB (woff2) => 29.4 kB (woff2)
400 : 584/44810 codepoints used (273 on this page), 11.4 MB (woff2) => 80 kB (woff2)
dist/posts/piped-devlog-display-errmsg-gracefully/index.html: 1 font (2 variants) in use, 23.5 MB total. Created subsets: 109 kB total
NotoSansMonoCJK:
700 : 128/44810 codepoints used (32 on this page), 12.1 MB (woff2) => 29.4 kB (woff2)
400 : 584/44810 codepoints used (152 on this page), 11.4 MB (woff2) => 80 kB (woff2)
HTML/SVG/JS/CSS size increase: 48 kB
Total savings: 234 MB
핵폭탄 2번 재료: tailwindcss
처음 tailwindcss
를 발견하게 된 것은 CSS
에 대한 끝없는 증오가 나를 잠식하고 있을 때였다(지금도 그렇다). 이 놀라운 프레임워크는 CSS
를 단 한 줄도! 작성하지 않고도 자유자재로 DOM을 스타일링할 수 있게 해준다. 심지어 당연하다는 듯이 미디어 쿼리를 활용한 responsive design
도 지원한다. 이제 CSS
지옥에서 벗어날 수 있어...!
첫번째 폭발
두 도구를 스까서 개발하던 도중, 나는 갑작스레 블로그 빌드가 진행되지 않는 문제에 직면하고 만다. 그러나 불행히도 2021년 인류의 놀라운 기술력 뽕에 취해버린 나는 그만 이 문제를 과소평가해 버리고 말았다.
나: 에이. 내가
cli
옵션을 잘못 줬겠지?
하지만 슬프게도, 이것은 그리 단순한 문제가 아니었다. 나는 문제 해결을 위해 수일을 쏟아부었고, 이내 원인을 찾아냈다. 여러분들도 짐작했듯이, 원인은 subfont
와 tailwindcss
의 조합이었다.
첫번째 폭발의 자세한 경위
tailwindcss
는CSS
프레임워크로써, 거의 모든 가능한CSS
속성들을 개별class
로 쪼개놓은 거대한CSS
덩어리(와 약간의 유틸리티)다. 개발 당시에는 이 거대CSS
를 그대로 사용하고, 프로덕션 빌드시에 이 중 실제로 사용되는class
만 남겨 크기를 줄인다.tailwindcss
의 프로덕션 빌드 여부는 환경 변수로 지정하는데, 내 빌드 환경 특성상 이것이 제대로 전달되지 않아 크기가 제대로 줄어들지 않았다.tailwindcss
는 특문을 활용한class
이름이 많다. 예를 들면:-translate-y-2\.5
와 같은 것이 있다.subfont
가 모종의 이유로 이 특문에 대한escape
처리를 제대로 하지 못한다. 특문을 만나게 되면DOMException
이 발생하고, 이 예외를 처리하느라 온갖 시간을 다 쓴다.tailwindcss
는purge
되지 않으면 거대하다. 따라서subfont
는 수많은 특문class
를 처리하려 시도하고, 예외가 발생하면서 시간을 죄다 잡아먹는다.- 사용하는 사람인 내 입장에서는 빌드가 영원히 끝나지 않는 것처럼 보인다...
다행스럽게도, 이 문제는 tailwindcss
의 설정을 수정해 purge
를 항상 하도록 하면 된다.
// tailwind.config.js
module.exports = {
purge: {
enabled: true,
content: [
/* ... */
],
},
};
잠시 시간을 내서 나의 환상의 똥꼬쇼를 감상해주기 바란다. 아래 링크에서 감상할 수 있다.
두번째 폭발
완성된 블로그를 잠시 방치하다가, 최근에 다시 운영하려고 유지보수 작업을 실시했다. 패키지도 업데이트하고, html minification
도 적용하고... 그러다 두 번째 문제를 만났다. subfont
가 OOM
을 뱉고 증발한다. 심지어 40GiB나 줘도, 다 먹고도 부족한지 더 달라고 징징대다가 증발한다.
이번에도 어김없이 나는 디버깅을 시작했고, 이번엔 하루만에 원인을 찾아냈다. font-tracer
라는, 폰트를 추적하는 라이브러리 코드의 문제였다. 이 코드에는 expandPermutations
라는 함수가 있다.
// Expand an object with array values into all possible permutations of the properties
// expandPermutations({a: [1, 2], b: [3, 4]}) =>
// [
// { b: 3, a: 1 },
// { b: 4, a: 1 },
// { b: 3, a: 2 },
// { b: 4, a: 2 }
// ]
function* expandPermutations(obj, propertyNames) {
propertyNames = propertyNames || Object.keys(obj);
if (propertyNames.length === 0) {
return [];
}
const firstPropertyName = propertyNames[0];
const firstPropertyValues = obj[propertyNames[0]];
for (let i = 0; i < Math.max(1, firstPropertyValues.length); i += 1) {
if (propertyNames.length > 1) {
for (const permutation of expandPermutations(
obj,
propertyNames.slice(1),
)) {
permutation[firstPropertyName] = firstPropertyValues[i];
yield permutation;
}
} else {
const permutation = {};
permutation[firstPropertyName] = firstPropertyValues[i];
yield permutation;
}
}
}
보시다시피 가능한 모든 조합을 찾는 함수다. 그런데 입력으로 오는 객체의 키를 조사해봤더니 64개였다. 벌써부터 느낌이 좋지 않다. 아래 스크립트로 총 가능한 순열의 개수를 구해봤다.
Object.keys(obj)
.map((key) => obj[key].length)
.reduce((acc, cur) => acc * cur, 1);
35184372088832
다시 말해서, 입력으로 들어오는 순열의 총 개수는 대략 35조개가 넘는다. 이걸 배열에다 넣으려고 시도하는 것이다! 그런데 github
를 뒤적여도 최근에는 해당 코드가 수정된 흔적이 없었다. 원래 잘 되던 코드란 말인데. 더 자세한 분석을 위해 입력 객체의 덤프를 떴다.
보이는가? 무수한 --tw-
의 행렬이! tailwindcss
가 자체적으로 사용하는 엄청난 양의 CSS
속성이 이 순열 생성에 참여하고 있는 것이다.
두번째 폭발의 자세한 경위
tailwindcss
는 별다른 유틸리티 없이도 정상 동작을 보장하기 위해서 자신만이 사용하는CSS
속성을 대량으로 생성한다.subfont
는 사용중인 폰트를 분석하기 위해CSS
속성의 모든 순열을 시도한다. 앞서tailwindcss
가 폰트 계산에는 영향을 주지 않는CSS
속성을 대거 추가했다. 이는 감당할 수 없는 숫자가 된다.subfont
가 35조개 정도 되는 순열을 죄다 배열에 넣는다.OOM
이 발생하고, 커널이subfont
가 점유했던 모든 메모리를 강제로 환수한다.- 사용하는 사람인 내 입장에서는
subfont
가 무한루프를 돌며 메모리 누수를 발생시키는 것으로 보인다...
해결 방법은 의외로 간단한데, subfont
가 의존하는 라이브러리인 font-trace
의 소스코드를 수정해 --tw-
로 시작하는 모든 CSS
속성을 날려버리는 것이다.
// node_modules/font-tracer/lib/fontTracer.js L1014
// Purge the custom css properties from the tailwindcss
for (const styledText of styledTexts) {
styledText.props = Object.keys(styledText.props)
.filter((key) => !key.startsWith('--tw-'))
.reduce((acc, key) => {
acc[key] = styledText.props[key];
return acc;
}, {});
}
이후 이 변경사항을 프로젝트에 영구적으로 반영할 수 있도록 patch-package
를 사용해 고정한다.
npx patch-package font-tracer
그러면 프로젝트 내에 아래와 같은 patch
가 생긴다. 잘 커밋해둔다.
diff --git a/node_modules/font-tracer/lib/fontTracer.js b/node_modules/font-tracer/lib/fontTracer.js
index 6dff2e2..cbcc744 100644
--- a/node_modules/font-tracer/lib/fontTracer.js
+++ b/node_modules/font-tracer/lib/fontTracer.js
@@ -1011,6 +1011,16 @@ function fontTracer(
}
}
+ // Purge the custom css properties from the tailwindcss
+ for (const styledText of styledTexts) {
+ styledText.props =
+ Object.keys(styledText.props)
+ .filter(key => !key.startsWith("--tw-")).reduce((acc, key) => {
+ acc[key] = styledText.props[key];
+ return acc;
+ }, {});
+ }
+
const seenPermutationKeys = new Set();
const multipliedStyledTexts = _.flatMap(styledTexts, ({ props, ...rest }) =>
[...expandPermutations(props)]
잠시 시간을 내서 나의 환상의 현재진행형 똥꼬쇼를 감상해주기 바란다. 아래 링크에서 감상할 수 있다.
결과
두 번의 큰 어려움이 있었음에도 불구하고, 나는 이 두 도구(subfont
+ tailwindcss
)를 유지하기로 결정했다. 들이는 노력에 비해 이득이 너무 달콤하다고나 할까... 폭발 사고와 후처리가 걱정되면서도 원자력 발전소를 포기하지 않는 이유와 같다. subfont
와 tailwindcss
는 나를 무적으로 만들어 준다. 폰트를 고를 때 오직 디자인만 신경 쓸 수 있다. CSS
는 한 줄도 작성할 필요가 없다. 심지어 모바일 지원이 필요할 때 조차!
이 글을 읽고 있는 당신도 사용해보는 것은 어떨까?