본문으로 건너뛰기
zoe

번들 분석 화면에서 멈춰 선 곳

이전에 @next/bundle-analyzer 로 번들을 들여다본 적이 있다. 그때 가장 먼저 눈에 들어온 건 아이콘이었다. 페이지 어느 자리에나 들어가지만 그래서 잘 의식하지 않던 작은 컴포넌트들이, 묶음 단위로 보니 생각보다 큰 자리를 차지하고 있었다.

번들 분석 결과 - 아이콘 비율

화면을 띄워둔 채 잠시 생각이 머물렀다. 실제로 한 화면에서 쓰는 아이콘은 많아야 열 개 안팎이다. 그런데 번들에는 거의 모든 아이콘이 들어가 있었다.

스토리북을 위한 코드가 서비스 번들로 새고 있었다

코드를 다시 열어보고 나서야 비로소 눈에 들어왔다. 아이콘 패키지는 결이 다른 두 코드를 한 묶음으로 들고 있었다.

하나는 SVGR 로 변환된 개별 아이콘 컴포넌트, 다른 하나는 스토리북에서 검색 기능을 받쳐주는 iconRegistry 라는 메타데이터 묶음이었다.

스토리북 아이콘 검색 기능

iconRegistry 는 팀원이 스토리북에서 "apple" 이라고 치면 Apple 아이콘이 잡히도록 돕는 인덱스다. 서비스 코드는 한 번도 이 객체를 import 한 적이 없다. 그런데도 번들에 같이 실리고 있었다. 같은 패키지의 같은 입구를 쓰고 있었기 때문이다.

트리셰이킹과 그 앞에 깔아야 했던 것들

답은 정해져 있다. 쓰지 않는 코드를 번들에서 떼어내는 일, 트리셰이킹이다. 다만 트리셰이킹이 동작하려면 번들러가 "이 코드는 떼어내도 안전하다" 고 판단할 수 있어야 한다. 그래서 패키지를 쓰는 쪽이 아니라 패키지를 만드는 쪽에서 손볼 게 더 많았다.

모듈을 합치지 않고 그대로 두기

빌드는 Vite 로 돌리고 있었다. 기본 설정은 모든 모듈을 합쳐 하나의 큰 파일을 만든다. 합쳐버리면 아무리 잘 import 해도 묶음 단위로 따라온다. preserveModules 를 켜서 모듈을 각각의 파일로 남도록 두었다.

옵션역할
preserveModules모듈을 합치지 않고 그대로 둠
preserveModulesRoot빌드 결과의 폴더 기준점
entryFileNameschunk 파일명 규칙

부수효과 없음을 선언하기

아이콘 컴포넌트는 DOM 을 만지거나 CSS 를 끌어오지 않는다. import 만 하고 쓰지 않으면 그대로 사라져도 무방하다. 다만 번들러는 그걸 스스로 알 수 없다. package.json 에 한 줄을 더해 명시적으로 알려야 했다.

입구를 둘로 가르기

여기까지 와도 한 가지 문제가 남는다. 다음처럼 가져오는 순간 모든 아이콘이 같이 끌려온다.

iconRegistry 는 스토리북에서만 의미가 있다. 그런데 서비스가 같은 입구로 들어와서 그 옆에 놓인 모든 아이콘을 함께 들고 가는 구조였다. 입구가 하나라서 생긴 문제라면, 입구를 가르면 된다.

서비스는 @crabit/assets-icons 의 기본 입구로, 스토리북은 @crabit/assets-icons/registry 로 들어온다. 같은 패키지 안에 있지만 다른 길로 갈라놓은 셈이다.

작아진 번들, 그 뒤에 남은 생각

작업 후 번들을 다시 띄워봤다.

최적화 후 번들 크기

화면이 단정해졌다. 실제로 쓰는 아이콘만 들어와 있었다.

숫자보다 더 인상에 남은 건 이 문제가 아이콘 패키지에만 해당하는 일이 아니라는 점이다. 개발자 도구를 위한 코드 — 스토리북, 디버깅 헬퍼, 테스트 픽스처는 자연스럽게 같은 패키지 안에 같이 살게 된다. 그리고 입구를 따로 가르지 않는 한, 그 옆의 모든 코드를 함께 끌고 서비스 번들로 흘러 들어간다. 패키지를 만들 때 입구를 미리 갈라놓지 않으면, 나중에 트리셰이킹을 아무리 잘 들여다봐도 줄어들 폭에 한계가 생긴다.

다시 만든다면

이번엔 번들 분석을 돌려보고 나서야 입구의 문제를 알았다. 하지만 이건 처음 패키지를 설계할 때 결정했어야 했다. 서비스용 입구개발자 도구용 입구를 처음부터 둘로 갈라두는 것. 단순한 빌드 설정이 아니라, 이 패키지 안의 코드가 누구를 위해 존재하는지를 미리 갈라 두는 일에 가깝다.

다음번에 새 패키지를 시작한다면 가장 먼저 정할 질문은 그래서 이것이 될 듯하다.