번들 분석 화면에서 멈춰 선 곳
이전에 @next/bundle-analyzer 로 번들을 들여다본 적이 있다. 그때 가장 먼저 눈에 들어온 건 아이콘이었다. 페이지 어느 자리에나 들어가지만 그래서 잘 의식하지 않던 작은 컴포넌트들이, 묶음 단위로 보니 생각보다 큰 자리를 차지하고 있었다.
화면을 띄워둔 채 잠시 생각이 머물렀다. 실제로 한 화면에서 쓰는 아이콘은 많아야 열 개 안팎이다. 그런데 번들에는 거의 모든 아이콘이 들어가 있었다.
스토리북을 위한 코드가 서비스 번들로 새고 있었다
코드를 다시 열어보고 나서야 비로소 눈에 들어왔다. 아이콘 패키지는 결이 다른 두 코드를 한 묶음으로 들고 있었다.
하나는 SVGR 로 변환된 개별 아이콘 컴포넌트, 다른 하나는 스토리북에서 검색 기능을 받쳐주는 iconRegistry 라는 메타데이터 묶음이었다.
iconRegistry 는 팀원이 스토리북에서 "apple" 이라고 치면 Apple 아이콘이 잡히도록 돕는 인덱스다. 서비스 코드는 한 번도 이 객체를 import 한 적이 없다. 그런데도 번들에 같이 실리고 있었다. 같은 패키지의 같은 입구를 쓰고 있었기 때문이다.
트리셰이킹과 그 앞에 깔아야 했던 것들
답은 정해져 있다. 쓰지 않는 코드를 번들에서 떼어내는 일, 트리셰이킹이다. 다만 트리셰이킹이 동작하려면 번들러가 "이 코드는 떼어내도 안전하다" 고 판단할 수 있어야 한다. 그래서 패키지를 쓰는 쪽이 아니라 패키지를 만드는 쪽에서 손볼 게 더 많았다.
모듈을 합치지 않고 그대로 두기
빌드는 Vite 로 돌리고 있었다. 기본 설정은 모든 모듈을 합쳐 하나의 큰 파일을 만든다. 합쳐버리면 아무리 잘 import 해도 묶음 단위로 따라온다. preserveModules 를 켜서 모듈을 각각의 파일로 남도록 두었다.
| 옵션 | 역할 |
|---|---|
preserveModules | 모듈을 합치지 않고 그대로 둠 |
preserveModulesRoot | 빌드 결과의 폴더 기준점 |
entryFileNames | chunk 파일명 규칙 |
부수효과 없음을 선언하기
아이콘 컴포넌트는 DOM 을 만지거나 CSS 를 끌어오지 않는다. import 만 하고 쓰지 않으면 그대로 사라져도 무방하다. 다만 번들러는 그걸 스스로 알 수 없다. package.json 에 한 줄을 더해 명시적으로 알려야 했다.
입구를 둘로 가르기
여기까지 와도 한 가지 문제가 남는다. 다음처럼 가져오는 순간 모든 아이콘이 같이 끌려온다.
iconRegistry 는 스토리북에서만 의미가 있다. 그런데 서비스가 같은 입구로 들어와서 그 옆에 놓인 모든 아이콘을 함께 들고 가는 구조였다. 입구가 하나라서 생긴 문제라면, 입구를 가르면 된다.
서비스는 @crabit/assets-icons 의 기본 입구로, 스토리북은 @crabit/assets-icons/registry 로 들어온다. 같은 패키지 안에 있지만 다른 길로 갈라놓은 셈이다.
작아진 번들, 그 뒤에 남은 생각
작업 후 번들을 다시 띄워봤다.
화면이 단정해졌다. 실제로 쓰는 아이콘만 들어와 있었다.
숫자보다 더 인상에 남은 건 이 문제가 아이콘 패키지에만 해당하는 일이 아니라는 점이다. 개발자 도구를 위한 코드 — 스토리북, 디버깅 헬퍼, 테스트 픽스처는 자연스럽게 같은 패키지 안에 같이 살게 된다. 그리고 입구를 따로 가르지 않는 한, 그 옆의 모든 코드를 함께 끌고 서비스 번들로 흘러 들어간다. 패키지를 만들 때 입구를 미리 갈라놓지 않으면, 나중에 트리셰이킹을 아무리 잘 들여다봐도 줄어들 폭에 한계가 생긴다.
다시 만든다면
이번엔 번들 분석을 돌려보고 나서야 입구의 문제를 알았다. 하지만 이건 처음 패키지를 설계할 때 결정했어야 했다. 서비스용 입구와 개발자 도구용 입구를 처음부터 둘로 갈라두는 것. 단순한 빌드 설정이 아니라, 이 패키지 안의 코드가 누구를 위해 존재하는지를 미리 갈라 두는 일에 가깝다.
다음번에 새 패키지를 시작한다면 가장 먼저 정할 질문은 그래서 이것이 될 듯하다.