size-limit이 번들 크기를 온전히 재지 못했다 — 디자인 시스템 번들 게이트를 다시 세운 이야기
사내 디자인 시스템의 번들 CI 게이트가 컴포넌트 스텁 파일만 재고 의존 청크를 집계하지 못하던 결함을 발견하고, 측정 방식을 다시 세운 기록. 발행 후 실측으로 원인 서술을 정정했다.
관련 프로젝트: 사내 디자인 시스템
정정 (2026-08-18) 처음 발행할 때 원인을 "진입점 파일(index)만 측정"이라고 썼는데, 나중에 설정 히스토리를 다시 확인해 보니 틀린 서술이었다. size-limit 설정은 최초 커밋부터 컴포넌트별 개별 경로를 재고 있었다. 실제 원인은 진입점이냐 아니냐가 아니라, 다중 entry 빌드에서 컴포넌트 파일이 스텁이 되어 의존 청크가 집계에서 빠진 것이었다. 본문을 실측 기준으로 고쳤다. 틀린 채로 두는 것보다 고친 기록을 남기는 편이 낫다고 봤다.
배경
사내 OMS 디자인 시스템(@h2o/design-system)을 빈 레포에서 구축하면서 디자인 토큰·컴포넌트·번들 파이프라인을 담당했다. 디자인 시스템은 여러 서비스가 가져다 쓰는 기반이라, 컴포넌트가 늘수록 번들 크기가 소비 측 성능에 직접 영향을 준다. 그래서 PR마다 번들 증가분을 막는 size-limit CI 게이트를 세워 두고 있었다.
문제: 게이트가 움직이지 않았다
어느 날 컴포넌트를 여러 개 추가했는데, size-limit 게이트 수치가 거의 변하지 않았다. 감각적으로 이상해서 실제 배포 번들을 직접 재봤더니, 게이트가 보고하는 값과 실제 크기가 크게 달랐다.
원인은 측정 범위였다.
빌드는 tsup 다중 entry 구성이었다. 공용 코드가 별도 청크로 빠지면서, dist/components/Button/Button.js 같은 개별 컴포넌트 파일은 그 청크를 import해서 다시 내보내기만 하는 스텁이 되어 있었다. 게이트는 그 스텁 파일 하나만 재고, 스텁이 끌고 들어오는 의존 청크는 집계에 넣지 않았다.
| 게이트가 재던 것 | 실제 소비자가 받는 것 | |
|---|---|---|
| 측정 대상 | 스텁 파일 하나 | 스텁 + 의존 청크 + CSS, 트리셰이킹 결과 |
| Button 기준 | 136 B | 8,970 B |
| 결과 | 증가분을 못 잡음 | — |
둘 다 brotli 압축 후 값이다. 압축 방식의 문제가 아니라 무엇을 재느냐의 문제였다.
즉, 게이트가 사실상 무의미했다. 더 큰 문제는, 이전에 팀에 "번들이 이 정도"라고 공유된 수치가 실제와 무관했다는 점이었다. 잘못된 지표를 기준으로 판단이 이뤄지고 있었다.
접근: 무엇을 '재야' 하는가부터
숫자를 고치기 전에 기준부터 다시 잡았다. 재야 하는 건 "진입점 파일 크기"가 아니라 소비 측이 import했을 때 최종적으로 번들되는 형태(트리셰이킹·CSS 포함, 압축 후) 크기다.
- size-limit의
preset-small-lib로 측정 방식을 교체 — 실제 번들링을 시뮬레이션하고 brotli 압축 기준으로 계산. - 게이트 8개를 컴포넌트별 실측 기준으로 재설계. Button / Input / Dropdown / Badge 등을 brotli 압축 기준으로 번들 크기 가드를 설정.
- 이전에 공유된 수치가 실제와 무관함을 밝히고, 근거가 없던 내 이전 주장도 함께 철회했다 — 팀이 잘못된 기준을 그대로 믿지 않도록.
결과
- 게이트가 실제 번들 증가를 잡는 도구로 복구됐다.
- 잘못된 기준치를 정정해, 팀의 의사결정 기반을 교정했다.
- 언더카운트 규모는 컴포넌트마다 달랐다. 의존 청크가 많을수록 격차가 컸다 — 스텁만 재던 값 대비 의존성 포함 값이 Button 기준 약 66배였다.
함께 세운 토큰 파이프라인
같은 시기에 토큰·컴포넌트 파이프라인도 구축했다.
flowchart LR
A[Tokens Studio<br/>디자인 토큰] --> B[Style Dictionary v4<br/>코드로 자동 변환]
B --> C[base 원시 팔레트]
C --> D[semantic alias]
D --> E[컴포넌트<br/>semantic만 참조]
E --> F[tsup<br/>CSS 사전 컴파일 동봉]
F --> G[소비 측<br/>import 2줄]- 2계층 토큰:
base원시 팔레트 +semanticalias. 컴포넌트는 semantic만 참조해 하드코딩 값을 줄였다. - 패키지화: tsup으로 CSS를 사전 컴파일해 동봉 → 소비 측은 import 2줄로 사용.
- 다크모드: Figma 매핑으로 라이트/다크 토큰 대응.
- 컴포넌트 8종(오버레이 3종 포함)
0.2.5배포.
배운 점
- CI 게이트는 "숫자가 보인다"와 "맞는 숫자다"가 다르다. 도구가 무엇을 재는지 스스로 검증하지 않으면, 게이트가 켜져 있어도 아무것도 지키지 못한다.
- 잘못된 지표는 없는 지표보다 위험하다. 팀이 그 숫자를 믿고 판단하기 때문이다. 그래서 발견 즉시 정정 기록을 남기는 것까지가 수정이었다.
이 경험 이후로, 새 도구를 붙일 때 "이게 정확히 무엇을 측정/보장하는가"를 먼저 실측으로 확인하는 습관이 생겼다.