Search

나만의 작고 귀여운 번들러로 기초 쌓아가기

subtitle
Tags
자바스크립트
front-end
Created
2025/04/27
2 more properties

직접 번들러를 만들어보며 배운 것들

프론트엔드 개발 3년 차가 되고 나서, 기본기를 다시 다지고 싶다는 생각이 들었다.
특히 Webpack이나 Vite 같은 번들러를 매일 쓰면서도 "정확히 내부에서 무슨 일이 일어나는지"는 명확히 알지 못했다. 코드를 봐도 각 코드가 어떤 일을 하는지 이해가 되지 않았다.
그래서 직접 작은 번들러를 만들어보면서 vite 코드에 대해 공부해보기로 했다.
그 과정을 기록으로 남겨보려고 하고, 이번 포스트는 번들러를 만든 과정에 대해서 썼다.

번들러가 하는 일은 무엇일까?

번들러는 여러 개의 소스 파일(JS, CSS, 이미지 등)을 하나 또는 여러 개의 묶음(bundle)으로 변환해서 브라우저가 효율적으로 읽을 수 있게 만들어주는 도구다.
내가 직접 만든 간단한 번들러는 다음 기능을 목표로 했다.
진입점(Entry file)부터 시작해서 import로 연결된 파일들을 모두 찾는다.
각 파일을 트랜스파일(Babel 사용)하고 모듈 시스템 안에 등록한다.
하나의 최종 번들 파일(bundle.js)로 묶는다.

모듈 번들링

먼저 @babel/parser, @babel/traverse, @babel/core를 활용해서 각 JS 파일을 분석하고 import를 따라가는 재귀적인 모듈 그래프(graph)를 만들었다.
function createAsset(filename) { const content = fs.readFileSync(filename, 'utf-8'); const ast = parse(content, { sourceType: 'module' }); const dependencies = []; traverse.default(ast, { ImportDeclaration: ({ node }) => { dependencies.push(node.source.value); } }); const { code } = babel.transformFromAstSync(ast, null, { presets: ['@babel/preset-env'], }); return { id: id++, filename, dependencies, code }; }
JavaScript
복사
이렇게 만든 asset들을 모아 최종 번들 파일을 생성했다. 최종 번들은 단일 IIFE(즉시 실행 함수) 형태로 묶었다.

Dev Server 만들기

가장 간단한 번들러를 만든 뒤, 번들 파일을 직접 실행해볼 수 있도록 dev server를 만들었다.
http 모듈을 사용해 정적 파일(index.html, bundle.js 등)을 제공
파일 시스템 감시(fs.watch)를 통해 JS 파일이 수정되면 자동으로 번들 재생성
fs.watch('./', { recursive: true }, (eventType, filename) => { if (filename.endsWith('.js')) { execSync('node bundler.mjs'); console.log('✅ Rebuilt bundle.js!'); } });
JavaScript
복사

LiveReload 기능 추가

내용이 수정될 때마다 새로고침을 누르는 것은 귀찮은 일이기 때문에 live reload 기능을 추가했다. 그래서 WebSocket을 통해 브라우저와 서버를 연결하고, 수정 사항이 생기면 브라우저에 "reload" 신호를 보내 자동 새로고침이 일어나게 했다.
서버 측에서는 WebSocket 서버를 띄우고,
function broadcastReload() { wss.clients.forEach(client => { if (client.readyState === 1) { client.send('reload'); } }); }
JavaScript
복사
클라이언트 측에서는 livereload 스크립트로 메시지를 수신하고 window.location.reload()를 실행했다.
const socket = new WebSocket('ws://localhost:3000'); socket.addEventListener('message', (event) => { if (event.data === 'reload') { window.location.reload(); } });
JavaScript
복사

문제: LiveReload 스크립트 번들 인젝션 이슈

개발 중 한 가지 문제가 있었다.
livereload.js 내부에서 import { PORT } from './constants.js'처럼 ESM 문법을 사용했는데,
이걸 번들에 그냥 문자열로 이어붙이니 브라우저가 ESM 구문을 그대로 만나서 에러를 발생시켰다.
브라우저에 넘기는 번들은 무조건 실행 가능한 plain JavaScript 여야 한다.
→ 따라서 개발용 코드도 적절히 트랜스파일하거나,
→ 필요시 템플릿 치환 방식으로 상수를 삽입해야 한다는 사실을 배웠다.

개발/배포 구분

livereload는 클라이언트 인젝션으로 코드가 추가되는데, 문제는 개발 환경에서만 추가가 되어야 한다는 점이었다. 그래서 개발/배포 모드를 나누어서 개발 모드에서만 livereload 스크립트가 들어갈 수 있도록 했다.
개발(dev) 모드: LiveReload 스크립트를 번들에 주입
배포(prod) 모드: LiveReload 스크립트 없이, 최적화된 코드를 출력
NODE_ENV를 기준으로 번들러가 주입할지 말지를 결정하게 만들었다.
const isDev = process.env.NODE_ENV === 'development';
JavaScript
복사
client injection code
if (isDev) { let livereloadScript = fs.readFileSync( path.join(__dirname, "livereload.js"), "utf-8" ); livereloadScript = livereloadScript.replace("__PORT__", PORT); result += `\n${livereloadScript}\n`; }
JavaScript
복사

앞으로 해볼 것

HMR (Hot Module Replacement) 직접 구현하기
CSS 번들링 및 핫 리로드
Plugin System 도입해서 플러그인 기반 번들러 만들기
빌드 최적화를 위한 코드 Minify (Terser, esbuild 연동)
번들러의 기초적인 부분을 구현한 뒤 vite 코드를 보면서 어떻게 구현되었는지 분석해볼 예정이다.