[누가 시키지도 않았는데 번들러 만들기] 1. 개념과 도구: 번들러의 등장 배경
1. 개념과 도구: 번들러의 등장 배경
<script> 태그를 나열하여 자바스크립트를 로드했습니다. 애플리케이션의 규모가 커짐에 따라 이러한 방식은 관리의 어려움과 성능 저하를 야기했고, 이는 '빌드' 과정이 필요한 주된 이유가 되었습니다.이 글에서는 자바스크립트 모듈 시스템의 발전 과정과 번들러가 필수적인 이유, 그리고 우리가 구현할 번들러의 주요 도구들을 살펴봅니다.
모듈 시스템의 발전
1. 전역 스코프 문제
a.js에서 선언한 변수는 전역 객체 window에 할당되어 b.js에서도 접근 및 수정이 가능했습니다.html<script src="jquery.js"></script> <script src="slider.js"></script> <script src="main.js"></script>
서로 다른 파일에서 동일한 변수명을 사용할 경우, 나중에 로드된 스크립트가 이전 값을 덮어쓰는 문제가 발생했습니다. 이를 방지하기 위해 개발자들은 네임스페이스 객체를 사용하거나 복잡한 변수명 규칙을 적용해야 했습니다.
2. IIFE 패턴
전역 스코프 오염을 막기 위해 IIFE 패턴이 널리 사용되었습니다.
javascript(function () { var privateVar = 'I am safe'; window.MyModule = { sayHello: function () { console.log(privateVar); }, }; })();
함수 스코프를 활용하여 내부 변수를 격리하고, 외부에서 필요한 기능만 명시적으로 노출하는 방식입니다. 우리가 구현할 번들러의 결과물 또한 이 패턴을 사용하여 모듈 간 격리를 보장합니다.
3. CommonJS
2009년 Node.js의 등장과 함께 CommonJS 시스템이 도입되었습니다.
javascript// math.js const add = (a, b) => a + b; module.exports = add; // main.js const add = require('./math');
require 함수가 존재하지 않고 동기 로딩 방식으로 인한 성능 문제가 있어 클라이언트 사이드에서 직접 사용하기에는 제약이 있었습니다.4. ESM
<script type="module">을 통해 모듈 시스템을 네이티브로 지원하게 되었습니다.번들러의 필요성
최신 브라우저가 ESM을 지원함에도 불구하고 Vite나 Webpack 같은 번들러가 필요한 이유는 다음과 같습니다.
- 성능 최적화: 수많은 모듈 파일을 개별적으로 로드하면 네트워크 요청 오버헤드가 발생합니다. 번들링을 통해 파일을 하나 또는 소수 로 병합하여 로딩 성능을 개선할 수 있습니다.
- 생태계 호환성: npm 생태계의 많은 라이브러리는 여전히 CommonJS로 작성되어 있습니다. 번들러는 이를 브라우저가 이해할 수 있는 형태로 변환하고 통합하는 역할을 수행합니다.
우리가 구현할 번들러는 모듈 탐색, 의존성 그래프 구성, 파일 병합 과정을 수행합니다.
기반 기술 1: ESM과 정적 분석
우리가 만들 번들러는 ESM 문법을 기반으로 동작합니다.
정적 분석
require는 런타임에 실행되는 함수이므로 조건부 호출이 가능합니다. 이는 코드를 실행하기 전에는 의존성을 완벽히 파악하기 어렵게 만듭니다.반면 ESM의 import 구문은 정적입니다. 파일 최상단에 위치해야 하며, 코드를 실행하지 않고도 의존성 관계를 파악할 수 있습니다. 이는 번들러가 의존성 그래프를 정확하게 구성하고, 사용하지 않는 코드를 제거하는 트리 쉐이킹을 구현하는 기반이 됩니다.
Live Binding
ESM은 값을 복사하지 않고 참조합니다.
- CJS: 모듈 호출 시점의 값을 복사합니다.
- ESM: 원본 변수에 대한 참조를 유지합니다.
이러한 특성은 순환 참조 해결에 유리합니다. CJS에서는 실행 순서에 따라 미완성된 객체를 참조하여 에러가 발생할 수 있지만, ESM은 참조가 유지되므로 모듈 실행이 완료되면 올바른 값에 접근할 수 있습니다.
기반 기술 2: Magic String
import 구문을 제거하거나 변환하고, 코드를 하나의 파일로 합치는 작업이 필요합니다.단순한 문자열 치환은 원본 코드의 인덱스를 변경시키므로, 소스맵 생성이나 추가적인 코드 분석을 어렵게 만듭니다.
Magic String 라이브러리 활용
magic-string 라이브러리는 원본 문자열의 인덱스를 유지하면서 변경 사항을 관리합니다.typescript// packages/@package/bundler/src/Module.ts 예시 private transformImportDeclaration(node) { // 1. 모듈 ID 조회 const depId = this.mapping.get(node.source.value); const requireCall = `require('${depId}')`; // 2. 변환 코드 생성 // import { a } from './file' -> const { a } = require('file') const replacement = `const { ${specifierStr} } = ${requireCall};\n`; // 3. 원본 인덱스 기반으로 덮어쓰기 this.magicString.overwrite(node.start, node.end, replacement); }
이 방식을 사용하면 여러 변환 작업이 중첩되더라도 원본 소스 코드의 위치 정보를 정확하게 추적하여 신뢰성 있는 소스맵을 생성할 수 있습니다.
실습 구조
이 시리즈의 실습 코드는 다음 경로에서 확인할 수 있습니다.
textpackages/@package/bundler/ ├── src/index.ts # 진입점: 설정 -> 빌드 -> 생성 ├── src/Graph.ts # 의존성 그래프 구축 및 번들 생성 └── src/Module.ts # 개별 파일 AST 파싱 및 변환
단계별 참조 파일
- Step 1:
src/index.ts전체 파이프라인 이해 - Step 2:
src/Module.ts파일 분석,src/Graph.ts관계 연결 - Step 3:
Graph.generate()번들링,Module.transform()코드 변환 - Step 4:
dist/index.js.map소스맵 확인
실습: 최소 기능 번들러 구현
번들러의 기본 구조를 이해하기 위해, 파일을 읽고 출력하는 간단한 코드를 작성해봅니다.
javascriptimport fs from 'node:fs'; import MagicString from 'magic-string'; // 1. 파일 읽기 const content = fs.readFileSync('./src/index.js', 'utf-8'); // 2. MagicString 인스턴스 생성 const bundle = new MagicString(content); // 3. 코드 변환 및 추가 bundle.prepend('/* Bundled by Custom Bundler */\n'); // 4. 결과물 출력 fs.writeFileSync('./dist/bundle.js', bundle.toString()); console.log('Build completed.');
이 코드는 단순한 파일 복사에 가깝지만, 추후 3번 단계에 AST 분석, 의존성 그래프 탐색, 코드 변환 로직을 추가하여 완전한 번들러로 발전시킬 것입니다.
다음 단계
다음 글에서는 코드를 데이터로 변환하여 분석하는 AST 분석과 의존성 그래프 구현에 대해 다룹니다.
Coming Soon