본문으로 바로가기

모듈 시스템은 뭐고 왜 필요하게 되었는가?

패키지를 쓰다보면, 다양한 방식의 모듈 빌드 방식이 있습니다.
대표적으로 제가 애용하는 fuze 검색 라이브러리는 아래와 같이 다양한 방식으로 빌드되었다고 문서에서 알려주고 있습니다.

https://fusejs.io/getting-started/different-builds.html

우선, js를 쓰다보면 require문을 쓰는 것인 commonJS고, import문을 쓰는 것이 ESM이라고 알게됩니다.

그러나 근본적으로 이러한 모듈 시스템이 왜 필요하게 되었는지에 대해서는 묻어두고 사용하다가 이번에 패키지를 개발할 일이 생겨서 공부할 필요성을 느끼게 되었습니다.

 

(1) 브라우저는 모듈을 몰랐었다. (es6 이후는 ESM은 안다)

 

일단 html/css/js에서 javascript를 node와 같은 서버 사이드에서처럼 모듈화 해서 사용하려고 하면 에러가 발생하는 것을 볼 수 있습니다.

그래서 webpack, browserify와 같은 모듈 번들러를 이용하여 javascript를 한 파일로 집약해서 만들곤 했습니다.

 

 

(2) 서버 사이드에서의 javascript를 위한 모듈

 

근본적으로, 웹에서 사용되던 javascript는 모듈 개념 없이 한 파일 내에 전부 작성되었어야만 했습니다.

그런데 javascript를 서버 사이드에서 사용하려고 한다면, 한 파일에 모두를 작성한다면 이는 생산성 측면에서도 심각한 일일 것입니다.

어쨌거나 javascript를 모듈화 해야할텐데, 기준을 통일할 필요가 있었습니다. 여기서 CJS니 AMD니 하는 여러 모듈 시스템이 등장하기 싲가합니다.

 

 

그렇다면 무슨 모듈 시스템이 존재하는가?

  • CJS(CommonJS) => 서버 사이드에서 사용. 동기적인 작동
  • AMD(Asynchronous Module Definition) => 클라이언트 사이드에서 주로 사용 (서버 사이드도 가능). 비동기적 작동
  • UMD(Universal Module Definition) => CJS와 AMD를 둘 다 사용하기 위함. 
  • ESM(ECMAScript Module) => 언어 자체에서 모듈을 탑재. 편해짐.

 

 

(1) CJS

 

우리가 흔히 아는 require 방식의 삽입, module.export 로 익스포트 하는 방식입니다. 아시다시피 node에서 사용하는 방식입니다.

따라서 node에서 ESM인 import/export를 사용하기 위해서는 babel을 거쳐서 변환하는 과정이 필요했었죠.

다만 CJS 방식은 브라우저에는 이해할 수 없고, 서버 사이드에서만 사용할 수 있습니다.

CJS 방식으로 모듈 시스템을 사용한 후에 코드를 브라우저에서 구동하기 위해서는 browserify, webpack, rollup 등 모듈 번들러를 통한 모듈 번들링이 필요합니다.

 

// 아래와 같이 require을 통해 package/lib 모듈을 변수에 담을 수 있습니다.
var lib = require('package/lib');

// 가져온 모듈을 아래와 같이 사용할 수 있습니다.
function foo () {
  lib.log('hello world!');
}

// foo 함수를 다른 파일에서 사용할 수 있도록, 다른 모듈로 추출될 수 있습니다.
exports.foobar = foo;

 

CJS의 가장 큰 특징으로 '동기적'이라는 것을 들 수 있습니다. 동기란 것이 무엇입니까, 될 때까지 기다린다는 것이지않습니까?

그래서 모듈이 임포트되기 전까지 (require로 라이브러리를 가져오기 전까지) 하단의 코드가 실행되지 않습니다.

필요할 때 불러오지 않기 때문에 비동기에 비하면 느리다면 느리다고 할 수 있습니다. (저는 잘 모르겠던데...)

 

 

(2) AMD (Asynchronous Module Definition)

 

학교 홈페이지에선 Require.js를 사용하던데, 이는 AMD 방식의 모듈 임포트가 필요할 때 브라우저에서 사용하는 대표적인 패키지입니다.

생각해보면, 브라우저가 모듈을 지원하지 않아서 모듈 시스템을 어떻게 지원해야 하는 지에 대한 고민으로 모듈 시스템이 나온건데

CJS는 서버 사이드의 자바스크립트에서 모듈 시스템을 사용하기 위한 방법론이라서 문제를 해결하지 못한 셈인거죠.

AMD는 브라우저에서 모듈 시스템을 사용하기 위해 만들어진 녀석입니다. 

 

require.js

 

// 종속성을 갖는 모듈인 'package/lib'를 모듈 선언부의 첫 번째 파라미터에 넣으면,
// 'package/lib'은 콜백 함수의 lib 파라미터 안에 담깁니다.
define(['package/lib'], function (lib) {

  // 로드된 종속 모듈을 아래와 같이 사용할 수 있습니다.
  function foo () {
    lib.log('hello world!');
  }

  // 생성된 foo 함수는 리턴을 통해 foobar라는 이름의 다른 모듈로 추출될 수 있습니다.
  return {
    foobar: foo
  };
});
require(['package/myModule'], function (myModule) {
  myModule.foobar();
});

 

AMD는 이름부터 asynchronous 한 모듈 정의라서, 비동기적입니다. 네트워크 요청을 통해 모듈을 불러오는 방식이죠.

 

 

(3) UMD (Universal Module Definition)


CJS, AMD 따로 따로 쓰는 건 불편합니다. 같은 js인데 브라우저에서는 AMD쓰고, 서버에서는 CJS를 쓰려니 번거롭습니다.

그래서 UMD라는 모듈 시스템이 등장합니다.

사실 새로운 모듈 시스템이라기보다는 런타임을 확인하고 분기하는 방식입니다.

 

define이 함수이고 define.amd가 존재할 경우 AMD 방식으로 exports가 존재하면 CommonJS 방식으로

아무 것도 아니 window(root) 객체에 통으로 싣습니다.

(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;
});

 

Universal 한 모듈 시스템답게 CJS, AMD를 둘 다 사용할 수는 있다는 장점이 있습니다. 그런데 못생겼다는 느낌을 지울 수 없습니다.

 

 

(4) ESM (ECMAScript Module)

 

모듈을 사용하기 위해서 위와 같은 시스템을 위한 코드를 별도로 작성하는 것은 번거롭습니다.

javascript 자체에서 모듈을 지원하는 것이 좋겠고, ES6 이후부터는 언어 내에 모듈 시스템을 지원하기 시작했습니다.

 

우선 script를 아래처럼 type="module"을 준 후 자바스크립트 파일 명을 mjs로 지정합니다.

<script type="module" src="./index.mjs"></script>
<script type="module" src="./index2.mjs"></script>

 

import/export 문을 사용하여 빼낸 후 

const sayHello = () => 'hello';
export default sayHello;

 

그냥 사용하면 됩니다. wow!

import sayHello from './index2.mjs';

console.log(sayHello());

 

개발자 도구에서 Network 창을 열어서 확인해보면, index.mjs, index2.mjs와 같이 2개의 js가 로드된 것을 확인할 수 있습니다.

물론 모듈이 너무 많아지면 이런 네트워크 요청 자체가 병목 현상을 일으킬 수도 있긴 하겠죠.

 

더 나아가 es6+를 이해하지 못하는 브라우저에서는 동작하지 않아서 모듈 번들링을 통해서 하나의 파일로 묶어줄 필요가 있고, 추가적으로 es6+를 이해하지 못으로 바벨을 이용하여 트랜스코드까지 해줘야 합니다.

 

 

Conclusion

 

"우리는 ESM 지원 안하는 브라우저에서 우리 서비스할 생각 없음. 버릴거임"

"모듈 많아봐야 그렇게 많지도 않음. 번들링 안할거임"

이런 마인드면 모듈 번들링할 필요가 없긴합니다. 요새 어디서 듣기를 모듈 번들링없이 곧바로 모던 브라우저에서 배포하는 것이 외국에서 선호된다고 들은 것이 있습니다. 모듈 번들링에 대해서 너무 집착하지 않는 것도 좋겠습니다.

 

그러나 코드 스플리팅도 해야하고, 주석도 자동 제거해야 하고, 웹팩 같은 경우 핫 리로딩도 해주고, 사용한 라이브러리 트리 쉐이킹도 해줘야하니 모듈 번들링은 아예 모르는 건 또 그것대로 문젭니다.

 

결론은 공부를 하긴 해야 합니다. 웹 최적화와 경량화에 도움되는 건 사실이거든요.

 

 

ref)

글은 제가 썼지만 위의 예시로 첨부한 코드들은 모두 아래의 작성 글을 참고하여 작성되었습니다. 

https://beomy.github.io/tech/javascript/cjs-amd-umd-esm/

https://wormwlrm.github.io/2020/08/12/History-of-JavaScript-Modules-and-Bundlers.html


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체