최근에 사이드 프로젝트에 하나 합류하게 되었습니다. 초기 세팅 단계가 이미 다 끝나 있는 프로젝트였기에 우선 현재 작업된 부분까지의 코드들을 둘러보다 Vite를 통해 프로젝트 빌드를 진행하고 있다는 사실을 알게 되었습니다. Vite를 한번도 사용해본 적이 없어서 Vite에 대해 알아보려 합니다.
Vite에 대해 얘기하기전에 우선 자바스크립트와 module에 대해서 알아보아야 합니다. 그 이유는 뒤에 나오는 얘기를 통해 알 수 있습니다.
자바스크립트는 웹에서 대형 어플리케이션을 만들거라고 생각하지 않았고 간단한 동작만을 위한 목적을 가지고 만들어져서 처음 나올 당시 Module
이 없었기에 파일을 여러개로 만들어서 개발을 할 수 없었습니다.
자바스크립트의 사용성이 넓어짐에 따라 별도의 Module로 분할하기 위한 메커니즘 제공을 위해 생각하기 시작했습니다. Node.js가 만들어짐으로써 require 함수
와 module.export
를 사용하는 CommonJS
라는 방식의 모듈 방식이 만들어졌습니다.
// CommonJS 방식
const test = require("test")
module.export = function () {}
CommonJS
는 브라우저 외의 환경에서도 동작하는 범용적인 JavaScript를 위한 모듈 시스템이기 때문에 모든 디펜던시가 로컬 디스크에 존재해서 필요한 모듈을 바로 사용할 수 있는 환경을 전제로 합니다. 따라서 동기적(synchronously)으로 모듈을 호출하는 방식을 선택했습니다.
CommonJS의 모듈 시스템 사용 빈도는 높아졌지만 처음부터 비동기
로드를 고려하지 않은 설계 때문에 브라우저에서는 CommonJS를 사용할 수 없다는 문제가 있었습니다. 즉, CommonJS 모듈 시스템은 Node.js 프로젝트에는 훌륭한 해결책이였지만 브라우저에게는 그렇지 않았다는 의미입니다.
AMD(Asynchronous Module Definition)
는 비동기 상황에서도 JavaScript 모듈을 쓰기 위해 CommonJS에서 함께 논의하다 합의점을 이루지 못하고 독립한 별도의 그룹입니다. 브라우저에서의 모듈 실행을 우선적으로 여겼기에 브라우저에서 필요한 모듈들을 네트워크를 통해 비동기적으로 다운로드 받고 나서야 사용할 수 있었습니다.
// AMD
define([
'jquery',
'underscore',
], function () {
return {};
});
require([], function(){});
CommonJS
에 비해 복잡한 문법이긴 하지만 비동기적으로 모듈을 호출하는 특성 때문에 CommonJS보다 나은 성능을 보였습니다. 또한 브라우저와 서버 사이드에서 모두 호환되는 방식이였습니다. 대표적인 모듈 로더 라이브러리로는 RequireJS
가 있습니다.
CommonJS와 AMD는 서로 지향하는 목적이 달랐기 때문에 뭐가 더 옳다고는 할 수 없습니다. 하지만 이처럼 통일되지 않은 규격들의 차이는 결국 서로 간의 호환성 문제로 이어졌습니다.
이로 인해 등장한 것이 바로 UMD(Universal Module Definition)
패턴입니다. CommonJS와 AMD 방식을 모두 호환할 수 있도록 조건문으로 분기하고 이를 팩토리 패턴으로 구현했습니다.
(function (root, factory) {
if(typeof define === "function" && define.amd){
// AMD 방식
define(["jquery", "underscore"], factory);
} else if (typeof exports === "object"){
// CommonJS 방식
module.exports = factory(require("jquery"), require("underscore"));
} else {
root.foo = factory(root.$, root._);
}
})(this, function ($, _) {
// 모듈 정의
var foo = {};
return foo;
});
코드는 훨씬 복잡해졌지만 CommonJS와 AMD 방식을 모두 호환하면서 window 객체를 통해 전역적으로도 접근할 수 있습니다. 임시 방편으로라도 두 방식의 코드를 호환할 수 있었기 때문에 라이브러리를 만들 때 자주 사용되는 패턴이었습니다.
이후에 나오는 Webpack과 Rollup 같은 몇몇 JS 번들러들은 ES6 방식으로 모듈 로드에 실패했을 때, 대안책으로 UMD 패턴으로 로드하는 방식을 아직 사용하는 곳도 있다고 합니다.
UMD는 CommonJS와 AMD의 호환성만을 해결했을 뿐 모듈 시스템의 체계를 확립하고 만들어나간게 아니었습니다. 그렇기에 JavaScript 언어 자체에서 모듈 시스템을 지원해줘야한다는 목소리가 많아졌습니다.
2015년, 현재 흔히들 아는 ES6라고 불리는 ECMAScript 6 사양에서 JavaScript의 표준 모듈 시스템이 명세되었습니다. 이를 ES6 Module(ES Module)
이라고 부릅니다.
// ES6
import foo from "foo";
export default foo;
여러가지 시행착오를 겪은 후 만들어진 JavaScript 공식 모듈 시스템이기에 위에서 언급한 문제점들을 해결하기 위해 많은 노력을 기울였습니다.
동기/비동기 로드
를 모두 지원하고 문법이 간단합니다. 또한 실체 객체/함수를 바인딩하기에 순환 참조 관리
도 편하고 정적 분석(static analyze, 코드를 실행하지 않더라도 분석이 가능)
이 가능하기 때문에 트리 쉐이킹(Tree Shaking)
역시 쉽게 가능했습니다.
하지만 ES6 문법을 공부해보신 분이라면 알 수 있듯 최근 문법이기에 IE 같은 구형 브라우저에서는 제대로 동작하지 않는다는 문제점이 있었습니다. 그러다보니 SystemJS
처럼 CommonJS, AMD, ES Module 모든 모듈 시스템을 지원하는 또 따른 형태의 모듈 로더가 나오는 헤프닝도 생겼습니다.
ES Module도 결국 완벽하게 해결하지 못했기에 ES Module을 포함하여 CommonJS, AMD, UMD모두 무엇을 쓰는게 맞는지 명확히 얘기할 수 없었습니다. 또한 CommonJS, AMD, UMD 모두 방식의 차이와 장단점이 존재하지만 결론적으로 JS 파일들을 따로 작성하여 로딩해야하기에 다음과 같은 문제점들이 발생했습니다.
결국 대규모 협업 개발에 유리하게 module과 같은 방식으로 여러개의 파일을 나눠서 개발을 하고 모든 파일을 하나로 합쳐서 하나의 번들(bundle)
로 만들어내는 번들러
라는 개념이 탄생합니다.
번들러는 대표적으로 webpack
, parcel
, rollup
이 존재합니다. 주로 webpack이 사용되며 해당 글은 번들러에 대한 글이 아니기에 각각의 번들러가 무엇인지 간단히만 다루겠습니다.
webpack은 모던 JavaScript 애플리케이션을 위한 정적 모듈 번들러입니다. webpack이 애플리케이션을 처리할 때, 내부적으로는 프로젝트에 필요한 모든 모듈을 매핑하고 하나 이상의 번들을 생성하는 디펜던시 그래프를 만듭니다. - webpack 공식 문서 -
webpack
은 프론트엔드 개발자라면 누구나 들어봤을 정도로 오래된 안전성이 뛰어난 번들러입니다. 서브파티 라이브러리 관리나 CSS 전처리, 이미지 에셋 관리 등에 있어서 타 번들러보다 강점을 보입니다.
라이브 리로딩(Live reloading)
기능과 새로고침 없이 런타임에 브라우저의 모듈을 업데이트하는 핫 모듈 교체(HMR, Hot Module Replacement)
기능 지원트리 쉐이킹(Tree Shaking)
을 지원하기는 하지만 CommonJS 방식으로 모듈을 로드한 부분을 ES6 문법으로 교체하고 타 플러그인 등을 설치해야하기에 번거로움 존재Rollup은 작은 코드 조각을 라이브러리나 애플리케이션과 같이 더 크고 복잡한 것으로 컴파일하는 JavaScript용 모듈 번들러입니다. CommonJS 및 AMD와 같은 이전의 특이한 솔루션 대신 JavaScript의 ES6 개정판에 포함된 코드 모듈에 대해 새로운 표준화된 형식을 사용합니다. - Rollup 공식 문서 -
Rollup
은 webpack과 유사한 모듈 번들러이지만 가장 큰 차이점으로 ES6 모듈 형식으로 빌드 결과물을 출력할 수 있어서 라이브러리나 패키지 개발에 활용할 수 있다는 점이 있습니다.
해시 캐스캐이딩(hash cascading, 한 파일의 해시가 바뀌면 그것을 참조한 파일의 해시도 알아서 바뀜)
이 약하다는 약점 존재Parcel은 개발 경험에서 차이를 느낄수 있는 웹 애플리케이션 번들러입니다. 멀티코어 프로세싱으로 빠른 성능을 제공하고 어떤 설정도 요구하지 않습니다. - Parcel 공식 문서 -
Parcel
은 별도의 설정 파일 없이도 동작합니다. 설치하면 하면 설정 파일 없이 빌드 명령어를 입력해 바로 사용할 수 있습니다. webpack과 달리 JavaScript 엔트리포인트를 지정해주는 것이 아니라 애플리케이션 진입을 위한 HTML 파일 자체를 읽기 때문입니다.
트리 쉐이킹(Tree Shaking)
에 강점을 보임 이와 같은 번들러들은 기존의 모듈 방식의 문제점을 해결할 수 있었지만 가장 문제점은 속도가 느리다는 것이였습니다. 기존에는 JS파일들을 작성하면 바로 브라우저에서 실행했지만 이제는 파일을 하나로 만들어주는 선행 작업이 생겼기에 수정시 매번 새로운 빌드가 필요했고 이 빌드 속도가 매우 느리기에 속도적으로 큰 문제가 발생했습니다.
그러다 기존의 빌드 속도보다 100가 빠른 빌드도구가 만들어집니다. 바로 esbuild
입니다. esbuild는 기존 번들러들과 다르게 JavaScript가 아닌 go언어로 작성되어 훨씬 더 빠른 빌드가 가능해졌습니다.
현재 웹용 빌드 도구들은 esbuild보다 10~100배 느립니다. esbuild 번들러 프로젝트의 주요 목표는 빌드 도구 성능의 새로운 시대를 열고 그 과정에서 사용하기 쉬운 최신 번들러를 만드는 것입니다. - esbuild 공식 문서 -
하지만 esbuild가 나왔는데도 다들 webpack을 사용했습니다. esbuild가 나온 당시 webpack은 DevServer, 트랜스파일링, 코드 스플리팅, 트리 쉐이킹, HMR, CSS, HTML, asset 지원 등 그저 빌드 도구가 아닌 개발을 도와주는 통합 툴이였지만 esbuild는 빌드 도구에 불과했습니다.
esbuild를 이용하여 가능한 개발은 바닐라 스크립트 형태의 라이브러리만 가능했으며 프레임워크를 기반으로 하는 웹 개발은 타 번들러들을 사용해야 했습니다.
esbuild의 속도는 그냥 버리기에는 아쉬웠고 사용 방안을 고민하다 Snowpack이 나오게 됩니다.
Snowpack은 최신 웹용으로 설계된 매우 빠른 프론트엔트 빌드 도구입니다. 이는 webpack 또는 paarcel과 같은 무겁고 복잡한 번들러의 대안이며 불필요한 작업을 피하고 프로젝트 규모가 아무리 커지더라도 빠른 속도를 유지합니다. - Snowpack 공식 문서 -
Snowpack
은 기존에 파일을 하나 수정 할 때마다 전체를 빌드해서 결과를 만들어내는 방식이 아니라 각각의 모듈을 별도로 빌드하고 수정이 발생하면 해당 파일만 다시 빌드를 해서 업데이트하는 방식을 채택하고 빌드는 esbuild를 사용했습니다.
이러한 방식을 통해서 파일 수정시 새로고침을 하지 않고 수정된 파일의 내용만 반영할 수 있는 HMR(Hot module replace)
기능을 매우 강력하게 제공할 수 있었습니다.
당시 Svelte
는 공식 번들로 Rollup을 사용했었는데 이는 Svelte를 개발한 리치 해리스가 만든 번들툴이 Rollup이였기 때문입니다.
Rollup은 HMR을 지원하지 못했기에 Svelte는 Rollup을 버리고 Snowpack을 공식 번들로 변경하게 됩니다.
이렇게 esbuild + 브라우저 표준 모듈로 개발을 진행하고 실제 릴리즈는 기존 번들 툴로 진행하면서 HMR, DevServer 등을 제공하게 되었지만 snowpack이 그다지 안정적이지는 않았습니다.
snowpack은 가장 좋은 대체안으로 만들어졌지만 안정화되는데 많은 시간이 필요했습니다. 그렇기에 기존의 좋은 컨셉을 본인것으로 만들어 더 간결하고 낫게 만드는게 특기인 에반유
가 snowpack을 기반으로 Vite
를 만들어냅니다.
Vite(프랑스어로 "빠른"이라는 뜻)는 최신 웹 프로젝트에 더 빠르고 효율적인 개발 환경을 제공하는 것을 목표로 하는 빌드 도구입니다. 두 가지 컨셉을 중심으로 하고 있습니다. - Vite 공식 문서 -
Vite
라는 esbuild와 브라우저 모듈을 이용한 DevServer, ProxyServer, 번들 툴, 코드 스플리팅, HMR 등 앞에 나왔던 모든 기능들을 모은 프론트엔트 번들 도구가 탄생하게 됩니다.
Vite는 최종 번들 도구로 Rollup을 선택했고 이로 인해 Svelte는 webpack이 최종 번들 도구인 snowpack보다는 기존에 사용하던 rollup을 최종 번들 도구로 쓰는 Vite가 더 낫다고 판단 Vite와 통합을 하게 됩니다.
Vite는 개발 시 반드시 Native ESM dynamic import를 지원하는 브라우저를 이용해야 합니다.
Vite 프로젝트 생성은 다음과 같은 명령어를 사용해 생성이 가능합니다.
# NPM
$ npm create vite@latest
# Yarn
$ yarn create vite
# PNPM
$ pnpm create vite
명령어를 통해 Vite를 사용하는 각종 프로젝트들도 생성이 가능합니다.
# npm 6.x
npm create vite@latest my-react-app --template react-ts
# npm 7+, '--'를 반드시 붙여주세요
npm create vite@latest my-react-app -- --template react-ts
# yarn
yarn create vite my-react-app --template react-ts
# pnpm
pnpm create vite my-react-app -- --template react-ts
이외에도 create-vite는 vanilla
, vanilla-ts
, vue
, vue-ts
, react
, react-ts
, react-swc
, react-swc-ts
, preact
, preact-ts
, lit
, lit-ts
, svelte
, svelte-ts
, solid
, solid-ts
, qwik
, qwik-ts
등 다양한 템플릿을 제공하고 있습니다.
react-ts 템플릿을 사용하여 vite 프로젝트를 만들고 실행시켜보았습니다.
실제로 CRA로 생성한 프로젝트와 Vite로 생성한 프로젝트를 각각 실행시켜보면 본인도 놀랐지만 눈에띄게 차이나는 빌드 속도를 경험하실 수 있습니다. 또한 Live reloading 또한 빠른 속도를 자랑하는 것을 알 수 있었습니다.
이번 사이드 프로젝트를 통해 Vite를 경험함으로써 앞으로의 프로젝트들은 Vite를 사용하여 진행하지 않을까 생각이 듭니다.
이번 글은 Vite의 배경과 개념에 대해 알아보았지만 추후에 프로젝트가 마무리되면 프로젝트의 Vite 코드를 둘러보면서 Vite의 상세 기능 및 내용과 사용법들을 다뤄보겠습니다.
자바스크립트 모듈 공식문서
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Modules
webpack 공식문서
https://webpack.kr/concepts/
rollup 공식문서
https://rollupjs.org/introduction/
esbuild 공식문서
https://esbuild.github.io/
snowpack 공식문서
https://www.snowpack.dev/
Vite 공식문서
https://ko.vitejs.dev/guide/why.html
JavaScript 번들러로 본 조선시대 붕당의 이해
https://wormwlrm.github.io/2020/08/12/History-of-JavaScript-Modules-and-Bundlers.html
테오님 Vite 이야기 Velog
https://velog.io/@teo/vite#%EC%B0%B8%EA%B3%A0%EC%9E%90%EB%A3%8C