👑
JavaScript 번들러로 본 조선시대 붕당의 이해

JavaScript 모듈 표준화의 제안부터 현대 모듈 번들러 등장까지의 역사를 정리해봅니다.

August 12, 2020


JavaScript


며칠 전, 회사 팀원 분을 통해 구글에서 진행한 온라인 컨퍼런스인 web.dev LIVE를 알게 되었습니다. 슬쩍 둘러보니 재밌어 보이는 주제가 몇 개 보여서, 주말에 시간을 내서 세션을 시청했습니다. 저는 특히 그 중에서 Building better in the world of build tools!를 인상 깊게 봤습니다. 웹팩과 같은 JavaScript 번들링 툴이 왜 필요하고, 어떤 종류가 있으며 어떤 장단점을 갖고 있는지에 대해서 간단하게 요약한 영상이었습니다.

그래서 사실 이 영상을 번역해서 포스트를 써보고자 했는데, 영상이 설명보다는 개발자들끼리 썰 푸는(?) 것 위주라 정리하기가 쉽지 않았습니다. 그래서 영상 자체 내용보다는 영상에 나온 키워드 위주로 관련 자료들을 리서치했죠.

필수 개념을 설명하려다보니 번들러와 모듈 시스템의 역사까지 설명해야 했기에… 요거를 어떻게 하면 재미있게 설명할 수 있을까 를 생각하다가 문득 붕당정치 짤이 생각이 났습니다. 그래서 거기에 맞게 글을 쓰면 재미있을 것 같다는 생각이 들어서 한 번 만들어 봤습니다.

JavaScript 모듈과 번들러로 본 조선시대 붕당의 이해 약간(?)의 각색이 포함되어 있습니다

고증을 최대한 반영하고 싶긴 했는데 딱 맞아떨어지는 예시가 많이 없어서 골치가 좀 아팠네요. 어느 정도까지 이해가 가시나요? 아는 만큼 보일텐데, 그래서 지금부터는 이해를 돕기 위해 모듈과 번들러의 역사에 대한 간략한 설명을 해볼까 합니다. 조선시대 역사까지는 알 필요 없어요ㅎㅎ; 저도 그냥 대충 끼워 맞춘 거라서…

아무튼 이 포스트를 통해 JavaScript 모듈 시스템에 대한 이해가 필요한 분들, 혹은 JavaScript 번들러 생태계에 대한 이해가 필요한 분들께 도움이 되기를 바랍니다.


모듈 시스템

최초의 JavaScript는 아주 간단한 모듈 시스템만을 제공했습니다. 바로 HTML에서 JavaScript 원본 소스를 제공하고, 브라우저에서 이것을 순서대로 로드하는 방식이었죠.

<html>
  <script src="/src/foo.js"></script>
  <script src="/src/bar.js"></script>
  <script src="/src/baz.js"></script>
  <script src="/src/qux.js"></script>
  <script src="/src/quux.js"></script>
</html>

이 방법은 그럴 듯 해 보였지만, 스크립트를 로드한 전역 컨텍스트에서 각 모듈 간의 충돌이 발생한다는 문제가 있었죠. foo.js에서 선언한 변수와 같은 이름을 가진 변수가 bar.js에 선언되어 있다면, 나중에 호출된 bar.js의 변수로 재정의가 되면서 foo.js의 파일이 제대로 동작하지 않게 되었습니다.

즉 모듈 간의 스코프가 구분이 되지 않아 다른 파일을 오염시키는 경우가 발생했다는 이야기죠. 그래서 개발자들은 이런 오류가 발생하지 않게 하기 위해서, 다른 모듈과 변수 이름이 겹치지 않으면서도 모듈 로드 순서를 지정하는데에 불필요하게 많은 시간을 쏟곤 했습니다.

시간이 흐르고, 브라우저에서 웹페이지의 역할이 단순히 문서를 보여주는 역할에서 벗어나 JavaScript를 활용한 어플리케이션에 가까워지게 되다보니, 상황은 더욱 복잡해졌습니다. 특히 2008년은 브라우저 외부에서도 JavaScript를 실행시킬 수 있는 Google의 V8 엔진이 공개된 해였기 때문에, 이를 이용해 서버 사이드에서 JavaScript를 활용하자는 아이디어가 막 제시되던 시기였죠. 그러다보니 모듈화에 대한 필요성이 더욱 부각되었고, 결국 2009년을 기점으로 JavaScript의 모듈을 표준화하기 위한 움직임이 본격적으로 시작되었습니다.

바로 CommonJSAMD(Asynchronous Module Definition)의 등장입니다.

CommonJS

CommonJS

CommonJSCommon 이라는 이름에서 나와있듯, JavaScript를 브라우저에서 뿐만이 아닌 서버 사이드 어플리케이션이나 데스크톱 어플리케이션 등 좀 더 범용적인 용도로 사용하기 위한 모듈 시스템을 만들기 위해 조직된 자발적인 그룹입니다. 그래서 해당 조직에서 제안한 방법을 CommonJS, 또는 CJS 라고 부르죠.

사용 문법은 아래와 같습니다.

// CommonJS

// 모듈 정의
module.exports = foo;

// 모듈 사용
const foo = require("./foo");

CommonJS는 애초에 브라우저 외의 환경에서도 동작하는 범용적인 JavaScript를 위한 모듈 시스템이기 때문에, 모든 디펜던시가 로컬 디스크에 존재해서 필요한 모듈을 바로 사용할 수 있는 환경을 전제로합니다. 따라서 동기적(synchronously)으로 모듈을 호출하는 방식을 선택했습니다.

requireexports 키워드를 활용한 직관적이고 간단한 문법이 눈에 띕니다. 하지만 비동기 방식보다 느리고, 트리쉐이킹(tree shaking, 임포트 되었지만 실제로는 사용되지 않은 코드를 분석하고 삭제하는 코드 최적화 기술)이 어려운데다 순환 참조에 취약하다는 등의 단점 또한 명확했는데요, 초창기 JavaScript 생태계의 발전에 있어서 모듈 시스템은 반드시 필요했기 때문에, JavaScript 런타임인 Node.js는 CommonJS 방식의 명세를 채택하고 구현했습니다.

아시다시피 Node.js의 선택은 옳았고 NPM의 성장과 함께 JavaScript 생태계의 규모는 폭발적으로 증가했습니다. 이 때문에 CommonJS의 모듈 시스템의 사용 빈도 역시 높아졌지만, 처음부터 비동기 로드를 고려하지 않은 설계 때문에 브라우저에서는 CommonJS를 사용할 수 없다는 문제가 있었죠. 즉, CommonJS 모듈 시스템은 Node.js 프로젝트에 있어서는 훌륭한 해결책이 되었지만, 브라우저에게 있어서는 그렇지 않았다는 이야기입니다. 이 때문에 CommonJS 모듈 방식을 브라우저 단에서도 활용하기 위한 최초의 빌드 도구인 Browserify가 탄생하기에 이릅니다.

AMD

AMD AMD 방식의 대표적인 모듈 로더인 RequireJS

AMD(Asynchronous Module Definition)는 비동기 상황에서도 JavaScript 모듈을 쓰기 위해 CommonJS에서 함께 논의하다 합의점을 이루지 못하고 독립한 별도의 그룹입니다. 사실 초창기 AMD가 만들어진 이유 역시 브라우저에서의 모듈 실행을 우선적으로 여겼기 때문입니다. 브라우저에서는 필요한 모듈들을 네트워크를 통해 비동기적으로 다운로드 받고 나서야 사용할 수 있었던 탓이죠.

사용 문법은 아래와 같습니다.

// AMD

// 모듈 정의
define([
  'jquery',
  'underscore',
  // 의존 모듈들을 배열로 나열
], function ($, _) {
  // 의존 모듈들은 순서대로 매개변수에 담김

  return {
    // 외부에 노출할 함수들만 반환
  };
});

// 모듈 사용
require([
  ...
  // 사용할 모듈 배열로 나열
], function (...) {
  // 사용할 모듈들이 순서대로 매개변수에 담김
});

아무래도 복잡한 문법이 띄긴 하지만, 비동기적으로 모듈을 호출하는 특성 때문에 퍼포먼스 면에서는 CommonJS 보다 나은 성능을 보였습니다. 그리고 브라우저와 서버 사이드에서 모두 호환되는 방식이기도 했죠. AMD 명세로 구현된 대표적인 모듈 로더 라이브러리로는 RequireJS가 있습니다.

UMD

CommonJS와 AMD는 서로 지향하는 목적이 달랐기 때문에, 어떤 것이 더 낫다고 이야기하기는 쉽지 않습니다. 하지만 이처럼 통일되지 않은 규격들의 발생은 결국 서로 간의 호환성 문제로 이어졌죠.

이 때문에 등장한 것이 바로 UMD(Universal Module Definition) 패턴입니다. Universal 이라는 이름에서 알 수 있듯이, 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) 같은 몇몇 JavaScript 번들러들은 ES6 방식으로 모듈 로드에 실패했을 때, 대안책(fallback)으로 UMD 패턴으로 로드하는 방식을 아직도 사용하고 있다고 합니다.

ES6 Module

ES6

하지만 UMD 역시 CommonJS와 AMD의 호환성만을 해결할 뿐이었기 때문에, 모듈 시스템의 부재라는 근본적인 문제를 해결하지는 못했습니다. 때문에 JavaScript 언어 자체에서 모듈 시스템을 지원해야 한다는 필요성이 높아졌죠.

그래서 2015년에, ES6라고도 불리는 ECMAScript 6 사양에서 드디어 JavaScript의 표준 모듈 시스템이 명세되었습니다. 이를 ES6 Module(ES Module)이라고 부르죠.

// ES6
import foo from "bar";

export default qux;

위에서의 진통을 겪고 나서 만들어진 JavaScript의 공식 모듈 시스템인 만큼, 그 과정에서 나타나는 문제점들을 해결하기 위해 많은 노력을 기울였습니다.

우선 동기/비동기 로드를 모두 지원하고, 문법 역시 간단합니다. 또한 CommonJS와는 다르게 실제 객체/함수를 바인딩하기 때문에 순환 참조 관리도 편합니다. 또한 정적 분석(static analyze, 코드를 실행하지 않더라도 분석이 가능함)이 가능하기 때문에, 트리 쉐이킹 역시 쉽게 가능해졌습니다.

하지만 비교적 최근에 정의된 문법이다보니, IE 같은 구형 브라우저에서는 제대로 동작하지 않는다는 문제가 있었습니다. 그러다보니 SystemJS처럼 CommonJS, AMD, 그리고 최신 ES6까지의 모듈 시스템을 모두 지원하는 또 다른 형태의 모듈 로더가 나오는 웃지 못할 상황도 생기게 되었죠.

여기서 모듈 로더(module loader)라는 용어를 한 번 짚고 넘어가고 싶은데요, 모듈 로더는 JavaScript 모듈을 런타임에 로드할 수 있게 만드는 구현체라고 생각하시면 됩니다. 즉, AMD의 모듈 로더는 RequireJS이고 ES6 방식의 모듈 로더는 네이티브 브라우저가 될 수 있겠죠. SystemJS는 모든 모듈을 로드할 수 있는 만큼 ES6, CommonJS, AMD 방식의 모듈 로더입니다.

트랜스파일러

트랜스파일러

ES6에서 JavaScript 표준 모듈 문법이 정의되었음에도 불구하고, ES6 문법을 구형 브라우저에서 사용하지 못해서 SystemJS 같은 또 다른 라이브러리에 의존해야 하는 것은 꽤나 꺼림칙한 일이었습니다. 그래서 나온 아이디어가 바로 트랜스파일러(Transpiler), 즉 한 번 컴파일하면 구형 브라우저에서도 동작하는 JavaScript 코드가 나오게 만드는 도구를 생각하게 되죠.

가장 유명한 것으로는 바벨(Babel)이 있습니다. 개발할 때에는 최신 JavaScript 문법을 사용하되, 바벨로 컴파일을 하고 난 후에는 같은 동작을 하면서 구형 브라우저 호환이 되는 JavaScript 코드로 변환 되니 개발자들은 호환성 걱정 없이 생산성 높은 최신 문법을 사용할 수 있게 되었습니다.

CoffeeScript, TypeScript 같은 JavaScript의 슈퍼셋(Superset) 언어의 등장 역시 하나의 방법이 되었습니다. 꼭 JavaScript 문법을 쓸 필요가 없기도 하고, 귀찮은 모듈 시스템의 관리는 컴파일러가 대신 해 주는 것이죠. 덕분에 호환성에 대한 고민거리는 많이 사라지게 되었습니다.


지금까지의 얘기는 JavaScript 모듈화에 대한 이야기였고, 과거로 다시 돌아가서 프론트엔드 생태계 이야기를 해보려고 합니다. 갑자기 이 이야기가 왜 나오냐구요? 바로 웹팩과 같은 모던 모듈 번들러의 등장을 이야기하기 위함입니다. 아까 모듈 시스템이 갈라진 이유가 서버 사이드와 브라우저의 관점이 달랐기 때문이라고 얘기했는데, 지금부터는 좀 더 브라우저, 즉 프론트엔드 관점에서 이야기를 해 볼게요.

태스크 러너

태스크러너

우리가 지금가지 CommonJS와 AMD, UMD 방식으로 모듈 시스템에 대해 이야기한 것은 바로 스코프가 구분되는 모듈을 만들기 위함이었죠. 그렇다면 스코프가 구분되는 모듈을 만드는 목적은 무엇일까요? 바로 여러 모듈을 조합해 중복되는 코드를 줄이면서도, 생산성과 퍼포먼스가 뛰어난 어플리케이션을 만들기 위함입니다.

그러기 위해서는 개발 과정에서 코드를 작성하고, 컨벤션을 유지하기 위해 린트를 사용하며, Sass나 TypeScript처럼 전처리가 필요한 언어를 컴파일하고, 소스 코드를 축소(minify)하고 하나의 파일로 묶는(bundle) 일련의 과정들이 필수적으로 동반됩니다. 이러한 과정들의 반복은 이러한 특정 작업들을 자동화 할 수 있는 도구의 필요성으로 이어졌고, 그 결과물이 바로 태스크 러너(task runner)입니다. 즉, 태스크 러너는 태스크 러너는 프로덕트 개발 과정에서 필요한 일련의 과정들(린팅, 빌딩, 테스팅 등)을 자동화하기 위한 도구입니다.

이러한 태스크 러너들에 대한 필요성이 모듈 시스템의 등장 이후에 등장한 것은 아닙니다. 태스크를 코드로 자동화한다는 생각은 1977년 만들어진 Make라는 도구에서도 볼 수 있듯이, 이미 오래 전부터 생각된 것이죠. 하지만 프론트엔드 생태계에서는 크게 필요성을 느끼지 못해서인지, 당시까지만 하더라도 태스크 자동화에 대한 뚜렷한 성과가 있지는 않았습니다.

그러다가 Node.js와 웹 생태계의 성장으로 인해 프론트엔드에서도 이것저것 처리해야 할 일이 많아졌고, 그래서 한 번쯤은 들어봤을 GruntGulp가 등장하게 됩니다. Grunt와 Gulp는 테스트나 린트, 번들, 최적화 플러그인들을 제공해주면서 이 과정들을 자동화 할 수 있게 도와주었고 덕분에 많은 프론트엔드 개발자가 사용하게 되죠.

이 과정에서 빌드를 위한 태스크 러너의 한 과정이었던 번들을 좀 더 전문적으로 도와주는 도구가 필요해졌고, 이것이 바로 모듈 번들러의 등장입니다.

모듈 번들러

번들러 번들러는 여행 가방을 싸는 것과 같습니다. 꼭 필요한 짐만 넣어야 효율적이거든요.

모듈 번들러(Module Bundler)는 JavaScript 모듈을 브라우저에서 실행할 수 있는 단일 JavaScript 파일로 번들링하는데 사용되는 프론트엔드 개발 도구입니다. 모듈 로더와 유사한 부분이 있지만, 모듈 번들러는 코드를 프로덕션 환경에서 사용할 수 있도록 준비하는 것에 더 큰 목적이 있습니다. 또한 모듈 로더는 일반적으로 런타임에 모듈을 가져 오기 위한 목적인 반면, 번들러는 빌드 시 모듈을 묶어서 단일 번들 파일로 만들기 때문에 런타임에서 추가적인 로드를 할 필요가 없습니다.

번들러는 크게 세 가지 이유로 사용하는데, 아직까지 모든 브라우저가 모듈 시스템을 완전하게 지원하지 않고, 코드의 종속성 관계를 관리하는 데 도움이 되며, 종속성 순서, 이미지, CSS 에셋 등을 로드하는 데 도움이 되기 때문입니다.

이처럼 번들러가 처음 만들어진 이유는 JavaScript 모듈을 브라우저에서 실행할 수 있는 단일 JavaScript 파일로 번들링하기 위함이었지만, 단순히 번들하는 것을 넘어서서 사용하지 않는 코드를 제거하는 등의 최적화 작업에 대한 필요성도 높아졌습니다. 이에 구글에서는 2009년 Closure Compiler라는 전문적인 JavaScript 최적화 도구를 만들기에 이르죠.

하지만 최근에는 번들러 자체에서 개발과 빌드, 최적화를 위한 각종 플러그인을 제공하고 있기 때문에 굳이 별도의 태스크 러너나 최적화 도구를 쓰지 않아도 되게 되었습니다. 이것이 바로 모던 웹 어플리케이션을 작성할 때 일반적으로 별도의 추가적인 툴을 쓰지 않는 이유입니다.

이러한 변화를 이끌어낸 현대적인 모듈 번들러를 꼽자면, 가장 유명한 3대장이 떠오르죠. 바로 웹팩(Webpack), 롤업(Rollup), 그리고 파셀(Parcel) 입니다. 지금부터는 각각의 특징과 장단점에 대해 간단하게 알아볼게요.

Webpack

웹팩

프론트엔드 개발자라면 한 번 쯤은 들어봤을 웹팩은, 오래된 만큼 생태계가 풍부하고 안전성이 뛰어난 번들러입니다. 그래서 서브파티 라이브러리 관리나 CSS 전처리, 이미지 에셋 관리 등에 있어서 다른 번들러보다 강점을 보입니다.

코드 스플리팅에 있어서는 롤업과 파셀이 성능적으로는 더 뛰어나지만 아직 안정성 면에서는 웹팩이 더 낫습니다. 일부 리서치에서는 코드 스플리팅이 활성화된 단계에서 빌드 시간이 웹팩이 가장 빠르다고도 하더군요.

또 다른 웹팩의 특징 중 하나는 웹 어플리케이션에서 사용하는 CSS나 이미지 같은 에셋들을 JavaScript 코드로 변환하고, 이를 분석해서 번들하는 방식을 사용한다는 것입니다. 이 때문인지 웹팩의 구성은 다른 번들러에 비해 설정할 게 많고, 많이 복잡합니다.

웹팩에서 제공해주는 개발 서버 역시 다른 번들러에 비해 뛰어납니다. 특히 개발 중 변경사항을 자동으로 새로고침 해주는 라이브 리로딩(Live reloading) 기능과, 새로고침 없이 런타임에 브라우저의 모듈을 업데이트하는 핫 모듈 교체(HMR, Hot Module Replacement) 기능이 그러하죠. 웹팩은 기본적으로 해당 옵션이 활성화된 webpack-dev-server 플러그인만 설치하면 되지만, 롤업과 파셀은 별도의 디펜던시와 설정을 추가해주거나 특정 상황에서는 잘 동작하지 않는 경우를 보이기 때문입니다.

다만 웹팩에서도 트리 쉐이킹이 지원되지만, CommonJS 방식으로 모듈을 로드한 부분을 ES6 문법으로 교체해야 하고, package.json 파일에 SideEffects 플래그를 설정해야 하며, UglifyJSPlugin, Terser 같은 별도의 플러그인을 설치하는 것처럼 별도의 추가 작업이 필요해서 조금 번거로움이 있다고는 하네요.

그리고 ES6 모듈 형태로 빌드 결과물을 출력할 수 없고, 복잡한 문서가 진입장벽을 높인다는 것 역시 단점으로 꼽히고 있습니다.

Rollup

롤업

롤업은 웹팩과 유사한 모듈 번들러이지만, 가장 큰 차이점을 꼽으라면 ES6 모듈 형식으로 빌드 결과물을 출력할 수 있으므로 이를 라이브러리나 패키지에 활용할 수 있다는 것입니다. 웹팩과 파셀은 자체 로더가 있지만 롤업은 ES6 를 기본으로 따르기 때문입니다.

이로 인해 코드 스플리팅 측면에서 다른 번들러와 비교해 강점을 보입니다. 중복 제거에 특화되어 있는데, 특히 진입점(entry point)이 여러 개 있을 때 이 부분이 두드러집니다. 롤업은 진입점이 다르기 때문에 중복해서 번들될 수 있는 부분을 공통되는 알아내고 독립된 모듈로 분리해 낼 수 있습니다. 그래서인지 웹 워커(Web worker)와 메인 쓰레드 간에도 코드 스플리팅이 가능하다고 소개합니다.

다만 파일의 해시 캐스캐이딩(hash cascading, 하나의 파일의 해시가 바뀌면 그것을 참조한 파일의 해시도 알아서 바뀜)이 약하다는 점이 약점으로 꼽히고 있습니다.

Parcel

파셀

우선 파셀은 별도의 설정 파일 없이도 동작합니다(zero config). 즉, 설치만 하면 별도의 설정 파일 없이 빌드 명령어를 입력해 바로 사용할 수 있습니다. 왜냐하면 파셀은 웹팩과 달리 JavaScript 엔트리포인트를 지정해주는 것이 아니라, 어플리케이션 진입을 위한 HTML 파일 자체를 읽기 때문입니다. 즉 HTML 파일을 순서대로 읽어나가면서 JavaScript, CSS, 이미지 에셋 등을 직접 참조합니다. 웹팩이 에셋을 읽기 위해 JavaScript로 변환하기 위한 이런 저런 플러그인과 로더를 깔아야 된다는 것과 비교하자면 파셀은 굉장히 자연스러운 플로우를 타고 있죠.

사용되지 않는 코드를 제거하는 트리 쉐이킹에서도 강점을 보입니다. 파셀은 ES6 및 CommonJS 모듈 모두에 대해 트리 쉐이킹을 지원합니다. NPM의 라이브러리에있는 대부분의 코드가 여전히 CommonJS를 사용한다는 것을 감안하면 유용한 기능이죠.

또한 파셀은 트랜스파일러도 간편하게 설정할 수 있습니다. 일반적으로 번들러는 JavaScript 파일만 읽을 수 있기 때문에, CSS나 이미지 같은 에셋들의 종속성을 인식하고 번들에 추가하려면 트랜스파일러를 사용해야 합니다. 트랜스파일이 필요한 파일 유형을 일일이 설정해야 웹팩 및 롤업과 달리, 파셀은 트랜스파일에 대한 기본 제공을 지원합니다. 즉, .babelrc, .postcssrc, .posthtml 같은 설정 파일들을 프로젝트 루트 디렉토리에 만들기만 하면, 파셀이 자동으로 파일을 읽어와서 세팅을 다 해주죠.

게다가 .babelrc 로 사용자가 직접 설정한 트랜스파일 외에도, 파셀은 항상 모든 모듈에서 Babel을 사용하여 최신 JavaScript를 브라우저에서 지원하는 형식으로 컴파일합니다.

자기네들 말로 코드 스플리팅에도 강점을 보인다고는 하지만, 최근에도 깃헙에서 이슈가 있는 걸 보면, 그리 안정적이지는 않은 모양입니다.

그리고 아직까지 웹팩이나 롤업에 비해 좁은 생태계, 떨어지는 안정성 등이 단점으로 꼽히고 있습니다. 공식 문서가 잘 작성되어 있긴 하지만, 일반적인 케이스만을 다룰 뿐 커스텀한 설정이 필요하다면 설정 파일을 다시 작성해야 하죠.

결론

그래서 번들러는 뭘 써야 하는건데? 라고 생각하실 분이 있을 것 같네요. 제가 찾아본 자료에 의하면, 기본적으로 프로젝트의 목적에 따라 다르고

  • 많은 서드파티를 필요로 하는 복잡한 어플리케이션이라면 웹팩
  • 최소한의 서드파티로 라이브러리를 만들고 싶다면 롤업
  • 복잡한 설정을 피하고 비교적 간단한 어플리케이션을 만들고 싶다면 파셀

을 추천하더군요.

Tooling.Report web.dev에서 만든 tooling.report

이처럼 다양한 빌드 도구의 등장은, 번들러의 선택 과정을 더욱 어렵게 만들었습니다. 그래서 최근 구글에서는 각 빌드 도구간의 장단점을 비교할 수 있는 Tooling.Report라는 사이트를 만들었다고 하더군요. 각 번들러 별 상세한 장단점이 궁금하신 분들은 한 번 들어가서 구경해보시는 것을 추천합니다.

마무리

지금까지 JavaScript 모듈의 탄생에서부터, 모던 번들러의 비교까지에 대해 알아봤습니다. 글을 잘 읽으셨다면, 다시 처음의 짤에 적힌 설명들이 아마(?) 이해가 가실 거라고 생각합니다. 처음에는 번들러에 대한 비교에서 시작된 글이었는데 적다보니 깊게 파고 들어가버렸네요.

이렇게 비유를 하다보면 원래의 의도와는 다르게 해석 될 여지가 있어서… 잘못된 정보를 전달할 수 있는 위험이 있을까봐 많은 리서치를 했습니다. 그럼에도 불구하고 제 배경 지식이 그리 깊지가 않다보니, 잘못된 정보가 있을 수 있는데 혹시나 발견하신다면 코멘트를 남겨주세요.

참고자료