폰트 하나 바꾸는 데 왜 Dynamic Feature Module까지 쓰게 됐나
![]()
"폰트 weight가 가이드보다 얇아 보여요." 오늘의집 일본 서비스에 붙은 버그 리포트 한 줄에서 시작된 이야기예요. 처음엔 기종 차이인가 싶었는데, 뜯어보니 더 큰 문제였어요. 일본어와 한자는 Pretendard가 아니라 시스템 폰트로 그려지고 있었거든요.
해결은 간단해 보였어요. Pretendard JP로 교체하면 끝. 근데 그 순간 APK가 10MB 부풀어 올랐어요. 일본 안 쓰는 한국·미국 사용자까지 일본어 글자를 다운로드해야 하는 상황. Android 개발자 Zemic 님이 이걸 어떻게 풀었는지, 그 과정이 꽤 쫀쫀해요.
일본어가 Pretendard가 아니었다
국내 서비스에 Pretendard가 잘 적용돼 있으니, 당연히 일본도 같은 폰트라고 생각했죠. 근데 FontForge로 파일을 열어보니 들어있는 건 특수문자·영어·한글 자모·한글 문자집합(KS X 1001)까지만이었어요. 일본어와 한자는 없어요. 아예 없어요.

렌더링 엔진은 폰트에 해당 글자가 없으면 시스템 폰트로 대체해요. 그러니까 일본어 화면의 한자는 Pretendard 느낌이 아닌, OS가 알아서 깔아주는 폰트로 보이고 있었던 거예요. 숫자랑 영어는 Pretendard라서 맞았고, 한자만 달랐으니 "weight가 얇다"는 제보가 들어온 거죠.

디자인 가이드와 같은 경험을 주려면 Pretendard JP를 넣어야 해요. 여기까지는 30분짜리 작업. 문제는 다음이에요.
10MB의 함정 — 한 줄 교체는 답이 아니었다
Pretendard는 다행히 일본어용 Pretendard JP를 공식 제공해요. 파일만 바꾸면 될 줄 알았죠. 파일을 받아봤어요. 원본보다 훨씬 무거워요.

단순 교체하면 APK가 10MB 증가해요. 일본어 글자가 있으니까 용량이 늘어나는 건 자연스러워요. 근데 그게 한국·미국 사용자한테도 날아간다는 게 문제였어요. 일본어를 한 글자도 안 쓰는 사람한테 히라가나·가타카나·한자 전부를 내려받게 만드는 건, 상식적으로 이상하잖아요.

그래서 방향을 틀었어요. "필요한 사용자에게만 필요한 리소스를." 이 원칙이 이후의 모든 판단을 결정해요.
경량화: 3.7MB를 2.4MB로
먼저 할 일은 폰트 다이어트였어요. 사실 국내용 Pretendard도 원본을 그대로 쓴 게 아니었어요. 한국산업규격 KS X 1001에 등록된 한글 음절 + 영어 + 특수문자만 남기고 다 잘라낸 버전. 똑같은 원칙을 JP에도 적용하면 됐죠.

일본어 통역사 한 분께 "일본 서비스에 실제로 쓰일 문자 리스트"를 뽑아달라고 요청했어요. 히라가나·가타카나·한자만 있는 게 아니었거든요. 전각기호, 반각기호, 자주 쓰이는 특수문자까지. (이걸 개발자 혼자 가늠했으면 어디가 덜 빠졌을지 모를 일이에요.)
도구 선택에서 한 번 삐끗했어요. 폰트 분석용 FontForge를 경량화에도 그대로 쓰려 했는데, 생성된 파일에서 "國內發送" 같은 문자의 상단 패딩이 이상하게 부풀어 오르는 버그가 났어요. 더 황당한 건, 아무 수정 없이 열었다 저장만 해도 같은 문제가 터졌다는 점. 결국 TTFont로 갈아탔어요.

Claude로 스크립트 세 개를 만들었어요. ① 원본에서 대상 폰트로 유니코드 복사 ② 유니코드 목록 추출 ③ 목록에 없는 글자 제거. 이걸 순서대로 돌리니까 깔끔하게 경량화된 Pretendard JP가 나왔어요.

결과. 각 3.7MB였던 폰트 파일이 2.4MB로 줄었어요. 약 35% 절감. 숫자만 보면 작아 보이는데, 여러 weight를 묶으면 MB 단위로 차이가 나요.

조건부 배포 — Dynamic Feature Module로 수렴한 이유
경량화로 절반은 풀렸어요. 남은 질문. 이걸 일본 사용자에게만 어떻게 주지? 세 가지 길을 검토했어요.
첫째 길은 App Bundle의 `enableSplit = true` 플래그. 언어 리소스를 Play Store가 기기별로 쪼개서 보내주는 기본 기능이에요. 가장 깔끔해요. 근데 오늘의집은 서드파티 라이브러리 에러 때문에 이 플래그가 이미 꺼져 있었어요. 켜려면 정책 재논의부터 해야 하는데, 폰트 하나 바꾸자고 열기엔 판이 너무 커요. 패스.
둘째는 Downloadable Font. Google의 FontProvider에 폰트를 올려두고 필요할 때 받아오는 방식이에요. 앱 번들에 폰트 안 넣어도 되니까 매력적이었어요.

근데 Google FontProvider는 Google Fonts에 등록된 폰트만 써요. Pretendard는 Google Fonts에 없어요. 자체 Provider를 세우는 선택지도 있긴 한데, 번들 넣거나 다운로드 로직을 직접 짜는 거라 일이 두 배예요. 이것도 패스.

셋째. Dynamic Feature Module. Play Store가 앱 다운로드 시점에 특정 조건의 사용자에게만 모듈을 얹어주는 방식. '특정 조건'에 국가가 포함돼요. 일본 기기에만 일본어 폰트 모듈이 딸려가요. 이게 답이었어요.
디자인 시스템 모듈 아래로 들어가는 구조
오늘의집 Android는 사내 디자인 시스템이 별도 모듈로 빠져 있고, feature 모듈들이 이걸 참조하는 구조예요. JP 폰트는 이 디자인 시스템 모듈 하위에 dynamic-feature-module로 붙였어요.

폰트 주입은 FontFamilyFactory에서 담당해요. SplitInstallManagerFactory로 동적 모듈이 설치됐는지 확인하고, 있으면 JP 폰트를, 없으면 한국어 폰트를 fallback으로 내려요. 조건부 배포인데 실패해도 화면이 박살 나지 않는 구조. 중요한 포인트예요.

구조상의 장점이 하나 더 있어요. 다른 나라가 추가돼도 같은 패턴으로 모듈을 붙이면 돼요. 지금은 한국어 폰트가 전부 합쳐도 1MB가 조금 넘어서 fallback으로 둔 거지만, 필요하면 얘도 dynamic-feature-module로 분리할 수 있어요. 확장성까지 챙긴 설계예요.

숫자로 떨어지는 결론
일본 외 사용자의 APK 용량. 그대로예요. 경험 차이 없음. 일본 사용자에게만 7.6MB가 추가되고, 그 대가로 디자인 가이드와 일치하는 Pretendard JP가 화면에 정확히 떠요.


"폰트 파일 교체" 한 줄짜리 티켓이 어떻게 Dynamic Feature Module 도입까지 갔는지를 보면, 현장에서 기술 스택을 고르는 기준이 선명해져요. 정답은 경량화 35%가 아니라, 다른 사용자를 건드리지 않는다는 제약이었어요. 제약이 먼저 설계를 좁히고, 그 다음에 남은 선택지 중 최선을 고르는 거예요.

그리고 한 가지. 이번 작업에서 Claude로 폰트 경량화 도구 세 개를 만든 장면이 의외로 상징적이에요. 개발 생산성 논의가 "AI가 코드를 대신 짠다"에 머물러 있지만, 실제 현장에서는 "1회성 스크립트"를 찍어내는 용도가 훨씬 자주 쓰여요. 폰트 유니코드를 복사·추출·제거하는 스크립트. 이게 AI 없으면 하루는 쓸 일이거든요. 그걸 한 시간으로 줄이는 지점에서, 이미 개발 워크플로우가 바뀌고 있어요.
결국 남는 건 명확한 결과예요. 글로벌 서비스의 폰트 일관성 ✓. 다른 지역 용량 증가 없음 ✓. 확장 가능한 구조 ✓. 세 가지 다 만족시키는 길은 하나밖에 없었어요.