Barrel import 제거가 _app 공통 청크를 줄인 이유
`export * as` 구조가 공통 진입점에서 그래프 입구를 넓히는 원리와, 직접 import 전환으로 shared 청크를 줄인 경로를 Webpack 분석 단계 기준으로 정리합니다.
이번에는 기능 변경 없이 import 경로만 정리했을 때 _app 공통 청크 구성이 왜 달라지는지 살펴봤습니다.
결과만 보면 shared는 4.0kB, _app은 4.3kB 줄었습니다.
이제 숫자 자체보다 원인을 중심으로, Webpack 단계별로 정리해보겠습니다.
1. 문제 배경
문제는 _app 경로에서 시작한 API import가 shared 청크 비대화로 이어지는지 확인하는 데 있었습니다.
여기서 _app 경로는 pages/_app 자체와 _app이 동기적으로 import하는 공통 모듈을 함께 의미합니다.
2. 리팩터링 진행
이번 리팩터링은 아래 3가지를 함께 적용했습니다.
import { domain } from 'api'형태의 barrel 경유 import 제거import { ... } from 'api/<domain>'직접 import 전환defaultexport 제거와 이름 충돌 정리
3. Webpack의 판단 기준
Webpack 최적화는 단일 기능이 아니라 여러 분석 단계가 이어진 결과입니다.
이 글과 직접 연결되는 핵심은 providedExports/usedExports, sideEffects, splitChunks, ModuleConcatenation + minimizer DCE였습니다.
providedExports와 usedExports
providedExports는 각 모듈이 제공 가능한 export 집합을 수집하며,
usedExports는 import 체인을 따라 실제로 참조된 export를 마킹합니다.
예를 들어 export * as club from './club'은 api/index.ts 입장에서 club namespace export를 제공하는 선언입니다.
소비측이 import { club } from 'api'를 사용하면 club 심볼이 used로 마킹됩니다.
이때 핵심은 함수 1개 사용 여부보다 namespace 경유 연결이 먼저 생긴다는 점입니다.
즉, _app 경로에서 api/index.ts를 먼저 통과하면 그래프 입구가 넓어지고 공통 후보군이 커집니다.
이후 usedExports가 세부 심볼을 줄여도, 넓어진 입구 자체는 별도 문제로 남습니다.
sideEffects
sideEffects는 “미사용 export가 아니라 모듈 파일 자체를 제거할 수 있는지”를 결정합니다.
sideEffects: false가 있으면 미사용 파일 제거를 공격적으로 수행할 수 있고, 정보가 없으면 보수적으로 유지합니다.
이번 저장소는 sideEffects가 없었기 때문에 Webpack은 모듈 파일 단위 제거를 보수적으로 처리했습니다. 따라서 번들 감소의 주 경로는 모듈 단위 제거가 아닌 그래프 연결 차단이었습니다.
splitChunks
splitChunks는 여러 엔트리에서 공통으로 참조된 모듈을 shared 청크 후보로 승격합니다.
Pages Router 기준으로는 _app 경로를 통해 도달한 모듈이 다수 페이지에서 재사용될 가능성이 높습니다.
그래서 _app 경유 입구가 넓으면 shared 청크로 이동하는 모듈도 함께 늘어납니다.
Before
_app -> 공통 모듈 -> api/index.ts -> club/index.ts, timetable/index.ts, ...
After
_app -> 공통 모듈 -> api/club/index.ts
_app -> 공통 모듈 -> api/timetable/index.ts
위 구조 차이 때문에 before는 공통 후보군이 넓고, after는 필요한 도메인 경로만 남습니다.
ModuleConcatenation + minimizer DCE
ModuleConcatenation은 같은 청크 내부 ESM 모듈을 결합해 scope hoisting을 수행합니다.
그 뒤 minimizer가 결합된 코드에서 dead code를 제거합니다.
즉, 이 단계는 “이미 같은 청크에 배치된 모듈”의 최종 코드량을 줄이는 역할입니다.
따라서 analyzer의 A + N modules (concatenated)는 원인 자체라기보다 결과 신호에 가깝습니다.
해당 표시는 청크 내부 연결이 강했고 결합 최적화 조건을 만족했다는 관찰 근거로 해석하는 편이 정확합니다.
4. 공통 진입점에서 export * as가 만드는 비용
리팩터링 이전 구조에서 barrel은 하위 도메인을 namespace 객체로 묶어 re-export했습니다.
// src/api/index.ts
export * as auth from './auth';
export * as club from './club';
export * as timetable from './timetable';
// ...
공통 경로 소비측은 barrel namespace를 가져와 사용했습니다.
// _app이 동기적으로 import하는 공통 모듈 내부 코드 예시
import { club, timetable, articles as articlesApi } from 'api';
club.getHotClub();
timetable.getMySemester(token);
문제는 _app 경로에서 api/index.ts를 경유하는 순간 의도와 무관하게 도메인 연결이 먼저 넓어진다는 점입니다.
직접 import로 바꾸면 이 입구 자체를 줄일 수 있습니다.
import { getHotClub } from 'api/club';
import { getMySemester } from 'api/timetable';
getHotClub();
getMySemester(token);
직접 import에서는 api/index.ts 경유 연결이 끊기므로 공통 후보군이 줄어듭니다.
실제 before analyzer에는 아래처럼 결합 그룹이 관찰됐습니다.
abTest, banner, cafeteria, coopshop, dept, review, room 등 도메인 인덱스가 index.ts와 함께 하나의 결합 단위를 이루었습니다.
이는 _app 경로에서 barrel 경유 연결 폭이 컸다는 근거가 됩니다.
5. sideEffects와 이번 케이스
이번 저장소는 루트 package.json에 sideEffects 필드가 없었습니다.
sideEffects 정보가 없으면 Webpack은 모듈 파일 단위 제거를 보수적으로 처리합니다.
결국 미사용 모듈이 export 관점에서는 불필요해도, 파일 자체는 번들에 남을 수 있습니다.
즉, 이번 감소는 sideEffects 기반 제거가 아닌 다른 경로로 발생했습니다.
실제 감소 원인은 그래프 연결 단절이었습니다.
barrel import가 전체 제거되면서 api/index.ts는 더 이상 import되지 않았고, 그래프 탐색 대상 자체에서 빠졌습니다.
아래 before/after 검색 결과로 확인할 수 있습니다.
barrel을 유지한 채 sideEffects: false만 추가하는 접근은 보완책으로는 유효합니다.
다만 이번 구조에서는 그래프 입구 자체를 좁힌 직접 import 전환이 더 직접적인 수단이었습니다.
원인을 정리했으니, 이제 같은 조건에서 나온 수치 결과로 마무리 검증하겠습니다.
6. 측정 결과 요약
동일 절차 3회 중앙값에서 공통 지표는 아래처럼 감소했습니다.
First Load JS shared by all:199.0kB -> 195.0kB(-4.0kB)chunks/pages/_app-*.js:89.3kB -> 85.0kB(-4.3kB)
analyzer _app 기준으로도 제거/축소가 동반됐습니다.
_app parsed:272,135 -> 254,248 bytes(-17,887)- 완전 제거 모듈:
27 - 축소 모듈:
19
parsed는 비압축 코드량이고 First Load JS는 gzip 압축 후 전송 크기입니다.
두 지표는 기준이 다르지만 감소 방향은 일치했습니다.
아래는 _app 청크 기준 모듈 목록 변화입니다. 제거된 27개 모듈 대부분이 api/**/APIDetail.ts와 도메인 인덱스 계열로, barrel 경유로 연결됐던 서브모듈이 공통 청크에서 분리됐음을 보여줍니다.
7. 마무리
정리하면 이번 사례에서 중요했던 건 barrel import 자체보다, 공통 진입점에서 모듈 그래프 입구가 얼마나 넓어지는가였습니다.
직접 import로 전환하자 api/index.ts 경유 연결이 사라졌고, _app과 shared 지표가 함께 감소했습니다.
결국 코드 양보다 의존성 경로 구조를 먼저 점검하는 편이 공통 번들 최적화에 더 효과적이었습니다.
참고 문서
- Next.js Custom App (Pages Router): https://nextjs.org/docs/pages/building-your-application/routing/custom-app
- Next.js Package Bundling (Pages Router): https://nextjs.org/docs/pages/building-your-application/optimizing/package-bundling
- Webpack Tree Shaking: https://webpack.js.org/guides/tree-shaking/
- Webpack Optimization (
usedExports,sideEffects,splitChunks): https://webpack.js.org/configuration/optimization/ - Webpack ModuleConcatenationPlugin: https://webpack.js.org/plugins/module-concatenation-plugin/
- Webpack SplitChunksPlugin: https://webpack.js.org/plugins/split-chunks-plugin/