Development Environment

Why Vite? JS 모듈화의 역사, CJS, ESM, Webpack

남희정 2024. 8. 10. 19:42

요즘 일 때문에 굉장히 바쁜 나날을 보내고 있다. 몇 주 전부터 주 1회씩 주제를 정하여 기술 발표를 하고 있다.

기존에 공부한 부분인데도 제대로 설명을 할 수 없었을 뿐더러 잘못 알고 있는 부분이 많았다. velog 포함 현재 이 티스토리 블로그에 올렸던 부분도 있었기에 부끄러움을 무릅쓰고 재게시하려 한다. 굳이 그러지 않아도 되면서 이렇게 하려는 이유는 첫 번째로 항상 틀릴 수 있다는 것을 다짐하기 위해서이고, 두 번째로는 과거의 나보다 현재의 내가 이해할 수 있는 부분이 확장되었다는 확신으로, 원동력을 갖기 위해서이다. 

 

https://velog.io/@chocoallergie/F-Lab-%EB%AA%A8%EA%B0%81%EC%BD%94-%EC%B1%8C%EB%A6%B0%EC%A7%80-60%EC%9D%BC%EC%B0%A8-Vite

 

[F-Lab 모각코 챌린지 60일차] Vite

JS Module, ESM, Bundler, Webpack, Vite

velog.io

 

저번 주에 Vite를 쓰는 이유에 대해 발표했다. 이전 글을 톺아보며 다시 공부해보겠다.

 

Vite 이전의 세계, 태초의 브라우저

Vite를 쓰는 이유를 설명하기 위해선 Vite가 어떻게 탄생하게 되었는지를 알아야할 것이다. 태초에, 브라우저는 ES Modules를 지원하지 않았다. 즉, 스크립트 간 모듈을 내보낼 수 없었다. 현재는 import, export를 통해 공통적으로 사용되는 모듈을 손쉽게 불러오고 내보낼 수 있게 되었지만 말이다. 이런 상황에서 웹 서비스가 기하급수적으로 복잡해지면서 방대한 코드들을 관리하기 어려워졌다. 

 

html 파일에 script 파일을 불러와서 사용할 경우 전역 공간에 모든 변수가 노출될 수 있었고 여러 script를 불러왔을 때 script간 변수의 이름이 겹쳐 충돌이 발생하는 문제점도 있었다. 개발자들은 다양한 전략으로 여러 파일로 분리하였고 window, jQuery로 전역 객체를 활용하여 스크립트 파일간 모듈을 공유했다. 소스 모듈을 브라우저에서 실행할 수 있는 파일로 크롤링(Crawling), 처리 및 연결하는 번들링(Bundling)이라는 해결 방법을 사용해야 했다.

 

크롤링 Crawling

- 웹사이트에서 데이터를 수집하는 과정

- HTML, CSS, JavaScript 등의 내용을 분석하여 원한느 정보를 추출하는 작업

 

번들링 Bundling

- 모듈화된 소스 코드를 브라우저에서 실행할 수 있는 파일로 한데 묶어주는 작업 

 

사람들은 웹 브라우저에서 더 많은 활동을 하게 되었고 더 좋은 서비스 경험이 요구되었다. 사용자 환경에 따라 코드를 변환해주거나 용량을 최적화 시키거나 등의 전처리 요구사항이 많아졌고 이런 것들을 해결해줄 자동화 시스템을 필요로 하게 되었다. 

grunt, gulp 같은 자동화 도구가 하나 둘 등장했고 webpack의 시대가 도래하게 된다. 

 

출처 : npm trends

(웹팩은 아직 건재하다..!)

 

JavaScript 모듈화의 역사

CommonJS

CommonJS는 JavaScript를 브라우저에서뿐만 아니라, 서버사이드 애플리케이션이나 데스크톱 애플리케이션에서도 사용하려고 조직한 자발적 워킹 그룹이다. CommonJS의 ‘Common’은 JavaScript를 브라우저에서만 사용하는 언어가 아닌 일반적인 범용 언어로 사용할 수 있도록 하겠다는 의지를 나타내고 있는 것이라고 이해할 수 있다.

JavaScript 표준을 위한 움직임: CommonJS와 AMD
https://d2.naver.com/helloworld/12864

 

1996년 JavaScript가 탄생한 후, JS를 브라우저 밖에서도 사용하려는 노력이 끊임없이 이어져 왔다. 2005년 Ajax가 부상하면서 더욱 중요성이 부각되었고 2008년 Google에서 공개한 V8 엔진을 소개하면서 많은 주목을 받았다. 기존 JS 엔진보다 월등히 빨랐고 브라우저 밖에서도 충분히 쓸만한 성능을 자랑했다. 그렇게 서버사이드 JS 진영에도 활기를 불어넣게 되는데, 2009년  Kevin Dangoor는 자신의 블로그에 서버사이드 JavaScript에 대한 아이디어를 제시하고, 함께할 사람을 모으기 시작했고 그렇게 시작한 CommonJS 그룹은 3개월만에 CommonJS API 0.1을 발표한다. 

 

// foo.js

const math = require('./math.js')
console.log(math.sum(1, 2))


// math.js

const { PI } = Math

exports.sum = (a, b) => a + b

exports.circumference = (r) => 2 * PI * r

 

 

위 코드처럼 require를 통해 별도의 모듈을 참조할 수 있고, exports라고 하는 특별한 객체를 통해 모듈의 루트에 추가할 수 있게 된다.

Node.js가 대표적으로 CommonJS 모듈 명세를 따라 모듈 로드 시스템을 만들었다.

 

Node.js 13.2.0부터 ES Module도 안정적으로 지원하지만(Node.js v9이하에서는 ES Module 호환이 어렵다.) .js 파일 확장자의 가장 가까운 부모의 package.json 파일에 type 필드가 명시되어 있지 않은 경우 default로 commonJS 모듈 시스템을 사용한다.

commonjs 라이브러리로 대표되는 lodash의 경우 package.json에 type이 할당되어 있지 않다. 

 

CommonJS와 더불어 더욱 궁금하다면 AMD에 대해서도 알아보면 좋을 것이다!

 

Webpack

기존에 CommonJS에 대한 브라우저의 지원은 없었다. CommonJS가 Node.js 프로젝트에서는 뛰어난 솔루션이었지만 브라우저는 모듈을 지원하지 않았다. 때문에 CommonJS의 실행이 가능하게 하는 Browserfy, RequireJS, SystemJS 같은 번들러와 툴이 만들어졌다. 당시에 ES Module(ES6에서 출시)가 있었지만 브라우저별 지원이 불완전했다. 모듈을 작성할 수도 있고, 리소스와 에셋을 동시에 처리 가능한 것이 있으면 좋지 않을까?

 

2012년 Tobias Koppers에 의해 webpack v1.0이 효율적으로 js 파일을 통합해주는 도구로 공개됐다.

2016년 webpack v2.0이 릴리즈되고 ES6를 지원하여 커뮤니티의 큰 관심을 얻게 되었다.

2017년 webpack v3.0이 릴리즈되고 성능 개선, 기능 추가 되면서 사람들이 열광하기 시작했다.
2018년 webpack v4.0이 릴리즈되었는데 동시에 React, Anglular의 등장으로 SPA가 급부상하게 되면서 번들러의 역할 더욱 막중해지자 이른바 "대웹팩시대"가 도래하게 되었다.

 

webpack은 모던 JavaScript 애플리케이션을 위한 정적 모듈 번들러입니다. webpack이 애플리케이션을 처리할 때, 내부적으로는 프로젝트에 필요한 모든 모듈을 매핑하고 하나 이상의 번들을 생성하는 디펜던시 그래프를 만듭니다.
- Webpack, "Concepts" 중

 

Webpack의 정체성은 번들러의 목적인 "통합"에 있다. 번들을 통합해서 관리할 순 없을까?에 대한 고민이 webpack을 부상할 수 밖에 없게 만들어주었다. 

 

 

손 쉬운 사용

4.0.0 이후로는 프로젝트를 번들링하기 위한 설정 파일을 필요로 하지 않는다.  webpack은 프로젝트의 엔트리 포인트를 src/index.js으로 가정하고, 프로덕션을 위해 압축하고 최적화된 결과를 dist/main.js로 내보낸다.하지만 추가로 요구에 따라 설정이 가능하다. webpack.config.js파일을 생성하면 webpack은 자동으로 그 파일을 사용한다.

 

강력한 개발 커뮤니티

Webpack은 위의 통계로도 보았듯이 아직도 건재한데, 이는 강력한 개발 커뮤니티가 뒷받침해주고 있다. 풍부한 plugin과 loader들이 오픈소스로 개발되고 있다. 

https://github.com/webpack/webpack 

 

GitHub - webpack/webpack: A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting all

A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be...

github.com

 

HMR(Hot Module Replacement) & Code Splitting

Webpack의 대표적인 기능인데, HMR은 개발 중에 앱을 새로고침하지 않고도 변경된 모듈을 즉시 교체할 수 있는 기능이다. DevServer와 함께 사용되며 개발 서버에서 실행되는 동안 HMR이 변경된 모듈을 자동으로 교체하도록 한다.

Code Splitting은 웹 앱의 코드를 작은 Chunk로 나누어 필요할 때만 로드하는 기술이다. 성능 최적화와 사용자 경험 개선에 중요한 역할을 한다. 

 

추가로 알아보고 싶으신 분은 Webpack 공식 사이트를 참고하시라..! 문서화가 매우 잘되어있다. (아래에 링크)

ES Modules

ESM은 ECMAScript에서 지원하는 JavaScript 공식 모듈 시스템이다. ES6에서 도입되었다. 현재 자주 쓰이는 방식으로, import, export 방식..! 직관적이고 간단하다. 표준이 되었다는 것이 가장 강력한 부분이 아닐까 한다. 

 

CommonJS의 시대는 끝났나? 그렇..다고 할 수 있다. 최근 많은 라이브러리들이 네이티브로 지원하는 ESModule로 구현하는 추세다. 라이브러리 제작자들은 항상 commonJS와 ESModules 두 개를 모두 고려 해야 했고, 사이즈도 2배가 될 것이다. 아무리 dependencies가 가볍다 하더라도 node_modules의 크기가 크게 된다면 dual exports시에 큰 부담이 된다. 

 

또한 트리쉐이킹이 어렵다는 단점도 손에 꼽힌다. (Tree shaking이란 간단히 말하여 나무를 흔들어 낡은 잎을 털어내듯이 사용하지 않는 dead code를 제거하여 최종 번들 크기를 줄이는 기술이다) commonJS의 경우 모듈을 내보낼 때 module.exports를 통해 모듈 전체를 내보내기 때문에 특정 함수나 변수가 사용되지 않는 것을 정확하게 파악하기 어려워진다. 또한 require() 함수로 동적으로 로드되어 실행 시점에 로드되기에 빌드 도구가 코드의 종속성을 정적으로 분석하기가 어려워진다. 이에 대해선 따로 포스팅을 해보도록 하겠다.

 

require를 쓴 경험이 정말 없을 것이다. (나부터,,) Node.js v20.16.0 (2024년 현재 LTS버전) 에서도 import를 통해 commonJS 모듈과 ES Module 전부 불러올 수 있도록 지원한다. Webpack의 경우만 보아도 그러한데, commonJS를 내부적으로 사용했으나 버전 5까지 나온 현재, Module Methods로 ES6에서 나온  ES Moudules를 권장한다. 

 

Node.js 공식 홈페이지

 

webpack 공식 홈페이지

 

Rollup

Rollup.js

Webpack 시대 개막과 함께 2017년부터 개발이 시작된 module bundler. 확장성이 뛰어나 같은 소스코드로 다양한 환경에 맞춰 빌드를 해준다. 작은 코드 조각들을  거대하고 복잡한 앱 혹은 라이브러리로 만들어 준다고 소개한다.

프로젝트를 더 작은 개별 조각으로 나누면 소프트웨어 개발이 보통 더 쉽습니다. 이는 예상치 못한 상호 작용을 제거하고 해결해야 할 문제의 복잡성을 극적으로 줄이기 때문입니다.
... (중략)...
ES 모듈 사용을 활성화하는 것 외에도 Rollup은 가져오는 코드를 정적으로 분석하고 실제로 사용되지 않는 모든 것을 제외합니다. 이를 통해 추가 종속성을 추가하거나 프로젝트 크기를 늘리지 않고도 기존 도구와 모듈 위에 빌드할 수 있습니다.

- Rollup 공식 홈페이지

 

Webpack은 commonJS를 내부적으로 사용했으나 Rollup은 TypeScript(ES6)를 사용하며 ES Module 형태로 build가 가능했다. 그리고 Tree shaking에 대해 설명 했듯, rollup은 ES6 코드를 사용하여 불필요한 코드를 쉽게 제거할 수 있었다. 또한 공식 플러그인을 통해 CJS 코드를 ES6 코드로 변환할 수도 있었다. 이러한 부분들을 장점으로 인정받아 많은 차세대 bundler가 rollup을 벤치마킹하게 된다. 

 

ESBuild

현재 웹을 위한 빌드 도구는 10-100배 느립니다. ESbuild 번들러 프로젝트의 주요 목표는 빌드 도구 성능의 새로운 시대를 가져오고 그 과정에서 사용하기 쉬운 현대의 번들러를 만드는 것입니다.
- ESBuild

엄청난 esbuild의 속도

 

ESBuild는 기존 번들링 도구보다 10-100배 빠른 퍼포먼스를 보여준다. 빠른 이유는 무엇일까? 공식 사이트에선 몇 가지 이유를 제시 한다.

 

- Go로 작성되었고 네이티브 코드로 컴파일 된다. (위 번들러들은 전부 JS 기반으로 번들링한다)

- 병렬성이 많이 사용된다.

- 모든 것을 직접 작성했다.. (많은 번들러는 TS 컴파일러를 파서로 사용하지만 esbuild는 그마저 성능 이점을 방해하는 요인으로 보았다.)

- 메모리가 효율적으로 사용된다.

 

ESBuild로 파일을 통합하고 Rollup을 통해 번들링하는 Vite

위에서 rollup과 esbuild를 설명한 이유 중 하나인데, Vite는 단독적으로 움직이는 게 아니다. 매우 똑똑하게 ESBuild로 성능을 챙기고 Rollup을 통해 유연성까지 챙겼다. (많은 이들이 번들링에는 ESBuild를 왜 사용하지 않는지 의문을 제시 했는지 직접 답변한 부분이 있다.)

Vue.js 의 창시자인 Evan You가 만들었으며 차세대 FE 개발 툴이라고 소개한다. 공식문서로 왜 Vite를 써야하는지 굉장히 잘 설명해주고 있다. 

 

제일 인상 깊었던 부분은 최초로 개발 서버를 구동할 때, 번들러 기반 도구의 경우 앱 내 모든 소스 코드에 대해 크롤링, 빌드 작업을 전부 마쳐야 실제 페이지를 제공할 수 있었는데, Vite는 앱의 모듈을 dependenciessource code 두 가지 카테고리로 나누어 개발 서버 시작 시간을 개선했다. 

 

1. Dependencies (개발시 내용이 바뀌지 않을 Plain JS code) - 사전 번들링으로 ESBuild 사용

2. Soource code (JSX, CSS 또는 Vue/Svelte 컴포넌트처럼 수정이 잦은 Non-plain JS code) - Native ES Module 사용, 현재 화면에서 실제로 사용되는 경우에만 처리 되도록 함.

 

번들링된 파일 기반으로 작동하는 도구들 작동방식(Webpack 등)
ES Module 기반으로 작동하는 VIte 작동방식

 

즉 최종적으로 정리하자면, Vite는 사전 번들링으로 굉장히 빠른 ESBuild를 사용하고 ES Module 기반으로 작동하여 모듈 수정시 수정된 모듈과 관련된 부분만을 교체한다. 또한 배포시에 rollup을 사용하여 유연성에 최적화된 번들링을 진행한다. 

 

이렇게나 빠른 Vite를 사용하지 않을 이유가 있나요?
- Vite

 

 

 


JS 모듈화의 역사를 톺아보니 경이롭다. 빌드 최적화를 위해 걸어온 길을 모두 거쳐보았다. Vite처럼 기존의 것을 사용하되 각각의 단점을 보완할 수 있게 모아놓은 것을 알게 되니 새롭게 보인다. 이제 Vite를 왜 쓰는지? 어떻게 쓰게 되었는지를 잘 설명할 수 있게 되었다. 추가로 CommonJS와 ES Module의 Tree-shaking에 대해 좀 더 알아보고 싶다.. 역시 파도파도 끝이 없는 개발 공부...

 

🙌

https://ko.vitejs.dev/guide/why.html

https://bepyan.github.io/blog/2023/bundlers 

https://npmtrends.com/esbuild-vs-grunt-vs-gulp-vs-rollup-vs-vite-vs-webpack  

https://yceffort.kr/2023/05/what-is-commonjs  

https://d2.naver.com/helloworld/12864 

https://blog.logrocket.com/commonjs-vs-es-modules-node-js/  

https://ingg.dev/webpack/ 

https://webpack.js.org/concepts/hot-module-replacement/ (HMR)

https://webpack.js.org/guides/code-splitting/ (Code Splitting)

https://rollupjs.org/introduction/  

https://laravel-news.com/introduction-to-rollup-js  

https://github.com/vitejs/vite/discussions/7622  

https://esbuild.github.io/faq/#why-is-esbuild-fast