안녕하세요 서운입니다!
이번에는 번들링이 진행되는 과정에대해 자세히 살펴보려고합니다.
저번 글에서 번들링이 무엇인지, 어떤 배경에서 등장했는지에 대해 먼저 다뤘는데요. 이번에는 그 연장선으로 “번들러가 내부으로 어떤 순서로 일을 처리하는지”에 초점을 맞춰 정리합니다.

번들러와 트랜스파일링 - 1


제가 이전 글에서 번들링을 “여러 파일을 하나로 포장하는 작업”이라고 설명했던 이유는, 실제로 번들링이 수많은 JS/CSS/이미지(정적 자원)들을 브라우저가 효율적으로 가져오게 만들기 위한 과정이기 때문입니다.

다만 여기서 중요한 포인트는:

  • 번들링은 무조건 ‘한 파일로 다 합치기’만 의미하지 않습니다.
  • 실제 목적은 “요청/로딩/캐싱을 더 유리하게 설계할 수 있도록” 자원을 묶거나(merge) 나누는(split) 전략을 적용하는 것입니다.

이 작업을 위해 번들러는 프로젝트 전체를 훑으며 의존성 관계를 분석하고, 결과물을 출력합니다.


0) 빌드가 시작될 때: 엔트리 확인부터

보통 npm run build 같은 스크립트를 실행하면(내부적으로 webpack/vite/rollup 등 호출), 번들러는 설정 파일에서 Entry Point(진입점) 를 확인하고, 그 지점부터 import를 추적하며 작업을 시작합니다.


1) 엔트리 결정 (Entry)

번들러는 시작 파일(들) 을 기준으로 출발합니다.

  • “내 앱은 어떤 파일을 시작점으로 잡고 있지?”
  • “페이지/라우트가 여러 개면 엔트리가 여러 개인가?”
    Webpack은 엔트리에서 시작해 의존성 그래프를 만든다고 명시합니다.
    엔트리가 결정되면, 번들러는 이 지점을 기준으로 import를 따라가며 다음 단계로 넘어갑니다.

2) 모듈 해석/리졸브 (Resolve)

엔트리에서 import/require를 따라가며 파일 위치를 찾는 규칙(확장자, alias, node_modules 우선순위 등)을 적용합니다.

점검 포인트:

  • alias는 어디서 정의되어 있지?
  • import "./style.css" 같은 것도 모듈로 취급하도록 설정되어 있나?
    이 단계가 꼬이면 흔히 “모듈을 찾을 수 없음” 류의 에러로 이어집니다.

3) 의존성 그래프 구성 (Module/Dependency Graph)

리졸브가 끝나면 번들러는 프로젝트에 필요한 모든 모듈을 모아 의존성 그래프를 구성합니다.

점검 포인트:

  • “왜 이 라이브러리가 결과물에 포함됐지?”
    → 의존성 그래프에 들어왔기 때문

  • “안 쓰는 코드가 왜 섞여 있지?”
    → 그래프에는 포함됐지만, 최적화(트리 셰이킹)가 충분히 적용되지 않았을 수 있음

즉, 번들러 입장에서 의존성 그래프는 “무엇을 포함할지”를 결정하는 핵심 단위입니다.


4)변환(Transform) 단계: 로더/플러그인/트랜스파일

여기서부터는 “브라우저가 바로 실행/처리하기 어려운 형태”를 브라우저 친화적인 형태로 바꾸는 단계입니다.

예시를 들어보면:

  • TypeScript / JSX / Vue SFC → 브라우저가 실행 가능한 JS로 변환
  • 최신 JS 문법(환경에 따라) → 타깃 브라우저에서 동작하도록 변환
  • CSS 전처리/후처리(예: PostCSS) → 최종 CSS로 변환
  • 이미지/폰트 같은 정적 자원 → 빌드 결과물로 포함(경로 재작성, 인라인 처리 등)

역할 구분을 깔끔하게 잡으면:

  • Loader(또는 Transform 단계): 특정 파일을 “어떻게 변환할지”
  • Plugin: 빌드 파이프라인 전체 흐름(컴파일 라이프사이클)에 개입해 확장

점검 포인트:

  • TS/JSX는 어디서 JS로 바뀌지? (Babel/SWC/esbuild 등)
  • CSS/이미지는 어떤 규칙으로 처리되지?
  • 내가 추가한 플러그인은 변환(Transform) 쪽인가, 출력/최적화 쪽인가?

5) 번들 생성: 청크/코드 스플리팅 (Bundling & Code Splitting)

의존성 그래프를 기반으로 “어떻게 파일을 나눠서(또는 합쳐서) 출력할지” 결정합니다.

  • Rollup은 동적 import / 다중 엔트리 같은 경우 자동으로 청크를 나누고, manualChunks로 제어할 수 있다고 설명합니다.
  • Vite도 build.rollupOptions.output.manualChunks로 청크 전략을 제어한다고 안내합니다.

점검 질문:

  • “내 앱은 지금 한 파일로 뭉쳐지나, 라우트별로 쪼개지나?”
  • “동적 import를 쓰고 있나? 있다면 어떤 경계에서 나뉘지?”
  • “벤더(react 등)는 별도 청크로 분리돼 있나?”

6) 최적화 (Optimization)

대표적으로:

  • Tree Shaking: 안 쓰는 export를 제거(ESM일수록 유리)
  • Minify: 공백/이름/구조를 압축
  • 중복 제거 / 스코프 호이스팅 등: 번들 구조 최적화
    Rollup/webpack이 코드 스플리팅과 연관된 방식으로 번들을 구성한다는 점은 공식 튜토리얼 흐름에서 확인할 수 있습니다.
    (“트리셰이킹” 자체의 세부 알고리즘은 번들러/옵션/모듈 형식에 따라 달라서, 여기서는 단계만 잡는 게 안전합니다.)

점검 질문:

  • “dev 빌드와 prod 빌드에서 결과물 크기가 크게 달라지나?”
  • “CommonJS 의존성이 많으면 트리셰이킹이 잘 안 되는 구간이 있나?”

7) 출력(Emit): 파일명, 해시, 매니페스트, 소스맵

마지막으로 번들(및 정적 자산)을 디스크에 씁니다.

  • Webpack의 output 설정은 “어디에/어떤 이름으로 번들과 에셋을 내보낼지”를 정의합니다.
  • 캐싱 관점에서 콘텐츠가 바뀔 때만 파일명이 바뀌게(content hash) 구성하는 가이드를 제공합니다.

점검 질문:

  • “파일명에 해시가 붙나? 해시 전략은 content 기반인가?”
  • “브라우저 캐시 때문에 배포 후 이전 파일을 물고 있는 문제는 없나?”
  • “소스맵은 어떤 환경에서만 켜지나?”

8) (도구별로 흔한 추가 단계) 의존성 프리번들/개발 서버

특히 Vite같은 도구는 “개발 중”에는 번들을 매번 다시 만들기보다 빠른 피드백을 위해 다른 접근을 하고, 프로덕션 빌드는 Rollup 기반으로 간다고 공식 문서에서 설명합니다.

점검 질문:

  • “dev 서버에서 빠른 이유는 ‘전체 번들 재생성’을 피하기 때문인가?”
  • “prod 빌드에서만 터지는 문제(경로/폴리필/청크 로딩)는 없는가?”

그럼 저는 위와 같은 과정을 정리하면서 이런 의문이 들었습니다.

해당 과정 즉 변환 -> 출력 의 단계에서의 비용이 더 비싸지 않을까 라는 생각을 했습니다, 한번 번들링으로 파일들을 묶고 브라우저에서 렌더링하기 위해 다시 그 묶음을 풀어야 한다고 생각이 들었습니다.

위 질문을 AI에게 물었고 이에 따른 비용은 누가/언제 내는 비용으로 쪼개면 결론이 난다는 답변을 받으면서 아래와 같이 정리해주었습니다.


1) “번들링(변환→출력) 비용”은 확실히 존재한다

빌드 시점에 번들러가 의존성 그래프를 만들고, 로더로 변환하고, 청크를 나누고, 최적화하고, 결과물을 emit 하는 만큼 CPU/시간 비용이 추가됩니다. 그래서 Webpack도 “빌드/컴파일 성능”을 개선하는 가이드를 따로 둡니다.

즉, “번들링을 안 하면(=빌드 단계에서 그런 작업을 생략하면) 빌드 시간은 더 싸질 수 있다”는 직감은 빌드 비용만 놓고 보면 대체로 맞습니다.


2) 그런데 “배포된 앱 전체 비용”으로 보면 비교가 바뀐다

번들링은 보통 CI/배포 파이프라인이 1회 내는 비용이고,
그 결과물은 모든 사용자가 다운로드/실행/캐싱 비용에 영향을 받습니다.

즉, 번들링은 “빌드 비용을 조금 더 내는 대신” 사용자 측 비용을 더 통제하기 쉬운 형태로 바꿉니다.

A. 번들링을 안 하면 사용자가 내는 비용이 늘기 쉬운 이유

“번들링 안 함”을 보통 (1) 파일이 더 잘게 쪼개져 있고 (2) 최적화가 약한 상태로 해석하면,

  • 요청할 파일 수가 늘면 네트워크 왕복/요청 처리 오버헤드가 생깁니다. MDN도 성능을 위해 “HTTP 요청 수를 줄이는” 접근을 언급합니다.
  • HTTP/2가 다중 요청을 효율화하긴 하지만(멀티플렉싱 등), “요청이 많아도 공짜”는 아닙니다.
  • 번들러의 코드 스플리팅은 “초기 로드에 필요한 JS를 줄여서” 초기 상호작용까지 시간을 줄이려는 목적입니다.

또한 “장기 캐시” 관점에서, 번들러는 콘텐츠 해시로 내용이 바뀐 파일만 갱신되게 만들 수 있습니다. 이건 배포 후 트래픽 비용과 체감 성능에 직접 영향이 큽니다.

B. 현실적으로 React/Vue는 “번들링 0원”이 잘 안 된다

React/Vue 앱은 보통 TS/JSX/SFC, CSS 처리 등이 필요해서 어차피 변환(트랜스파일) 단계는 거의 필수입니다(완전한 “무가공 ESM 배포”가 어려움).
Vite도 개발에선 빠르게 하려고 “의존성 프리번들”을 따로 하고(요청 수 감소를 직접 예시로 듭니다), 프로덕션 빌드는 Rollup 기반으로 묶습니다.

C. 번들링의 목적은 “사용자 측 비용을 줄이기 쉬운 형태로 재구성”

번들러가 엔트리에서 import를 따라 의존성 그래프를 만들고, 필요한 모듈을 모아 번들로 묶는다고 웹팩에서 명시하고 있습니다.
여기서 "왜 묶냐?"는 보통 아래 3가지 사용자 비용을 겨냥합니다.

네트워크 대기 (요청 시작/대기) 비용 줄이기

  • Webpack 문서 자체가 “번들링은 특히 HTTP/1.1에서 요청 시작 대기를 최소화해 강력하다”고 설명합니다.
  • HTTP/2 환경에서는 무작정 “한 덩어리”가 최선이 아닐 수 있어서, 같은 문서에서 “HTTP/2에서는 코드 스플리팅을 활용”하라고 이어집니다.

즉, 번들링의 1차 목적은 “파일을 합쳐서”가 아니라 요청/대기 모델을 관리하기 쉽게 만드는 쪽입니다.

초기 JS 처리 비용(파싱/컴파일/실행) 줄이기

  • code splitting의 목적을 web.dev가 “초기 시작 시간을 줄이기 위해, 시작 시점에 보내는 JS를 줄여 인터랙티브를 빠르게 한다”라고 설명합니다.
  • Webpack도 코드 스플리팅을 “필요할 때/병렬로 로드 가능한 여러 번들로 나눠 로드 시간에 큰 영향을 줄 수 있다”라고 안내합니다.

여기서 핵심은: 번들링=무조건 한 파일이 아니라, “사용자에게 지금 필요한 것만 먼저 보내기”를 가능하게 하는 청크 전략(스플리팅)입니다.

캐싱 효율(재방문/업데이트) 개선

  • Webpack 캐싱 가이드는 “콘텐츠가 바뀌지 않으면 결과 파일을 캐시에 유지”하는 구성을 다룹니다.

  • contenthash는 “콘텐츠가 바뀌면 해시도 바뀐다”는 점을 한국어 가이드가 명시합니다.

즉 번들링 결과물은 장기 캐시를 잘 먹게 만들어서, 사용자 재방문/부분 업데이트 비용을 줄이기 쉽습니다.


3) 그럼 번들링을 안 하면 사용자 비용이 항상 줄까?

여기서 스스로 판정 질문 2개만 던지면 됩니다.
1. 번들링을 안 하면 초기 로드 때 ‘요청 수’가 늘어나는가?

  • 늘면 네트워크 오버헤드/의존성 로딩이 늘 수 있습니다(특히 HTTP/1.1).

2. 번들링을 안 하면 초기 로드 때 ‘초기 JS 양’이 줄어드는가?

  • 줄어드는 방향이면 오히려 이득일 수 있는데, 이건 “번들링을 안 한다”가 아니라 보통 코드 스플리팅을 더 잘한다로 해결합니다.

현업에서 결론은 보통 이렇게 정리됩니다:

빌드 비용(변환→출력)은 CI/배포에서 “몇 번” 내는 비용이고,
사용자 비용(다운로드/처리/캐시)은 “모든 사용자”가 내는 비용이라서,
대부분의 서비스는 번들링/스플리팅/캐싱으로 사용자 비용을 관리하는 쪽을 택합니다. (Vite도 개발 단계에서조차 의존성 pre-bundling을 기본으로 둡니다.)


4) 결론: 번들링의 핵심 목적은 “사용자 비용 최적화를 위한 설계 가능성”

정리하면:

  • 번들링의 목적은 단순히 “합치기”가 아니라
  • 요청 수/로딩 순서/캐싱 전략/초기 로드 JS 크기를 더 유리하게 설계하는 데 있습니다.

그래서 현업에서 번들링은 “빌드 비용이 들더라도”, 사용자 경험과 운영 효율 측면에서 선택되는 경우가 많습니다.

profile
성공을 위해선 과정만 있을 뿐이다

0개의 댓글