최근 Webpacker에서 Vite Ruby 로 마이그레이션을 준비하고 있는데, 준비 과정에서 "ESM 기반"이라는 표현을 자주 마주하게 되었습니다. Vite 공식 문서에서도 "Native ESM based dev server"라고 소개하고 있죠.
ESM이란 정확히 무엇이고, 왜 Vite는 이를 핵심 기술로 내세우는 걸까요?
Vite Ruby 마이그레이션을 본격적으로 시작하기 전에, ESM의 개념과 그것이 프론트엔드 개발 경험에 어떤 변화를 가져오는지 살펴보겠습니다.
ESM을 이해하려면 먼저 JavaScript의 모듈 시스템이 어떻게 발전해왔는지 알아야 합니다.
2015년 이전, JavaScript에는 공식적인 모듈 시스템이 없었습니다. 여러 파일로 코드를 나누려면 HTML에 <script> 태그를 순서대로 나열해야 했습니다.
<script src="jquery.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>
이 방식에는 여러 문제점이 있었는데,
Node.js가 등장하면서 서버 사이드 JavaScript에는 CommonJS라는 모듈 시스템이 자리 잡았습니다.
// math.js
function add(a, b) {
return a + b
}
module.exports = { add }
// app.js
const { add } = require('./math')
console.log(add(1, 2))
require와 module.exports를 사용하는 이 방식은 Node.js 생태계의 표준이 되었습니다. 하지만 CommonJS는 동기적으로 동작하도록 설계되어 있어서 브라우저 환경에는 적합하지 않았습니다.
브라우저에서 모듈 시스템을 사용하기 위해 Browserify, Webpack 같은 번들러가 등장했습니다. 번들러는 여러 개의 JavaScript 파일을 분석하고, 하나의 큰 파일로 합쳐주는 역할을 합니다.
app.js ─┐
│
math.js ├─→ [Webpack] ─→ bundle.js
│
utils.js┘
개발자는 CommonJS나 다양한 모듈 문법으로 코드를 작성하고, 번들러가 이를 브라우저가 이해할 수 있는 형태로 변환해주었습니다.
Webpacker 역시 이러한 번들러 기반 접근 방식을 Rails에 통합한 것입니다.
ESM(ECMAScript Modules)은 2015년 ES6 표준에 포함된 JavaScript의 공식 모듈 시스템입니다.
import와 export 키워드를 사용합니다.
// math.js
export function add(a, b) {
return a + b
}
export function multiply(a, b) {
return a * b
}
// app.js
import { add, multiply } from './math.js'
console.log(add(1, 2))
ESM이 특별한 이유는 이것이 언어 차원의 표준이라는 점입니다. CommonJS가 Node.js 커뮤니티의 약속이었다면, ESM은 JavaScript 언어 명세 자체에 포함된 기능입니다.
ESM의 import와 export는 파일의 최상위 레벨에서만 사용할 수 있고, 조건문 안에 넣을 수 없습니다.
// ESM - 불가능
if (condition) {
import something from './module.js' // SyntaxError!
}
// CommonJS - 가능
if (condition) {
const something = require('./module') // 동작함
}
이러한 정적 구조 덕분에 코드를 실행하기 전에 모듈 간의 의존성을 분석할 수 있습니다. 이는 Tree-shaking 같은 최적화를 가능하게 합니다.
ESM은 비동기적으로 모듈을 로드할 수 있도록 설계되었습니다. 이는 네트워크를 통해 모듈을 가져와야 하는 브라우저 환경에 적합합니다.
어떤 모듈에서 무엇을 가져오는지 코드만 보고 명확하게 알 수 있습니다.
ESM의 진정한 의미는 현대 브라우저가 이를 네이티브로 지원한다는 점입니다. 별도의 번들러 없이도 브라우저가 직접 모듈을 이해하고 로드할 수 있습니다.
<script type="module" src="./app.js"></script>
type="module" 속성을 추가하면 브라우저는 해당 스크립트를 ESM으로 처리합니다.
// app.js
import { Button } from './components/Button.js'
import { Header } from './components/Header.js'
// 브라우저가 이 import 문을 보고 직접 해당 파일들을 요청합니다
브라우저의 동작 과정을 살펴보면:
이 과정에서 번들러의 개입이 필요 없습니다. 브라우저가 모듈 로더 역할을 직접 수행하는 것입니다.
이제 Webpack과 Vite의 근본적인 차이를 이해할 수 있습니다.
Webpack은 개발 환경에서도 모든 파일을 번들링합니다.
개발 서버 시작
↓
전체 애플리케이션 의존성 분석
↓
모든 모듈을 하나의 번들로 합침
↓
메모리에 번들 유지
↓
서버 준비 완료 (수십 초 소요)
파일이 수정되면 해당 부분을 다시 번들링하고 HMR(Hot Module Replacement)을 통해 브라우저에 전달합니다. 프로젝트 규모가 커질수록 초기 번들링 시간과 수정 반영 시간이 늘어납니다.
Vite는 개발 환경에서 번들링을 하지 않습니다. 대신 브라우저의 네이티브 ESM을 활용합니다.
개발 서버 시작
↓
서버 준비 완료
↓
브라우저가 페이지 요청
↓
필요한 모듈만 요청 시점에 변환하여 제공
Vite 개발 서버는 요청이 들어올 때 해당 파일만 변환합니다. 1000개의 파일이 있는 프로젝트라도 현재 페이지에서 100개만 사용한다면 100개만 처리하면 됩니다.
// 브라우저가 보내는 요청
GET /src/App.jsx
GET /src/components/Header.jsx
GET /src/components/Button.jsx
// ... 필요한 파일만
파일이 수정되면 해당 파일 하나만 다시 변환하면 됩니다. 프로젝트 전체 크기와 관계없이 HMR 속도가 일정하게 유지되는 이유입니다.
네이티브 ESM이 이렇게 좋다면 왜 Webpack이 오랫동안 표준이었을까요?
ESM을 지원하는 브라우저가 충분히 보급되기까지 시간이 필요했습니다. 2020년 이후에야 주요 브라우저들이 모두 ESM을 안정적으로 지원하게 되었습니다.
과거에는 HTTP 요청 하나하나가 비용이 컸습니다. 수백 개의 모듈 파일을 개별 요청하는 것보다 하나로 합쳐서 요청하는 게 효율적이었습니다. HTTP/2의 멀티플렉싱 덕분에 이 문제가 완화되었습니다.
node_modules 안의 라이브러리들은 ESM이 아닌 경우가 많았고, 수천 개의 작은 파일로 구성되어 있습니다. Vite는 이 문제를 esbuild를 사용한 사전 번들링으로 해결합니다. 개발 서버 시작 시 node_modules의 의존성을 빠르게 하나로 묶어둡니다.
Vite가 ESM을 활용하는 것은 개발 환경에서의 이야기입니다.
프로덕션 빌드에서는 여전히 번들링이 필요합니다. 수백 개의 파일을 개별 요청하는 것은 프로덕션 환경에서 비효율적이기 때문입니다.
Vite는 프로덕션 빌드 시 Rollup을 사용하여 최적화된 번들을 생성합니다.
개발: ESM 그대로 제공 → 빠른 서버 시작, 빠른 HMR
프로덕션: Rollup으로 번들링 → 최적화된 배포 파일
이것이 Vite의 핵심 전략입니다. 개발과 프로덕션에서 서로 다른 최적화 전략을 적용하여 각 환경에서 최선의 경험을 제공합니다.
ESM은 JavaScript의 공식 모듈 표준이며, 현대 브라우저는 이를 네이티브로 지원합니다.
Vite는 이 사실을 활용하여 개발 환경에서 번들링 과정을 생략하고, 브라우저가 직접 모듈을 로드하게 합니다. 그 결과 프로젝트 규모와 관계없이 빠른 개발 서버 시작과 즉각적인 HMR이 가능해집니다.
Webpacker에서 Vite로 마이그레이션한다는 것은 단순히 도구를 바꾸는 것이 아니라, 번들러 중심의 개발 방식에서 브라우저 네이티브 기능을 활용하는 방식으로 패러다임을 전환하는 것입니다.
다음 글에서는 실제 Rails 프로젝트에서 Webpacker를 Vite Ruby로 마이그레이션 하게된 사유와 전략에 관해 살펴보겠습니다.
- Vite 공식 문서 - Why Vite: https://vitejs.dev/guide/why.html
- MDN - JavaScript Modules: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
- Node.js - ESM과 CommonJS 차이: https://nodejs.org/api/esm.html