[Module System] Module Loader 와 Bundler에 대하여 - 1
프론트앤드 자바스크립트 개발이 점점 복잡해 짐에 따라 모듈 패턴으로 코드를 작성하고 단일 책임 원칙(Single Responsibility Principle)을 지키는 것이 좋다. 모듈 코드를 작성한 후 모듈을 로딩하고 배포(번들링)하는 다양한 방법들이 존재한다. 먼저 Module Loader에 대해 살펴보고 다음 글에서 Module Bundler에 대해 정리해 본다.
모듈 패턴
모듈패턴을 사용하는 이유
- 유지보수성(Maintainability): 단일 책임 원칙에 따라 필요한 기능을 담고 있으면서 별도 폴더와 파일로 유지하면 변경이나 확장 발생시 찾고 수정하기 쉽다. 전제 조건은 외부에 노출하는 API를 일관되게 유지하는 것이 중요하다.
- 이름공간(Namespacing): 자바스크립트에서 전역변수를 통한 개발을 하지 않는다. 이를 위해 즉시실행함수표현(IIFE)를 사용하여 전역변수의 오염을 방지하는데, 모듈 패턴 또한 전역변수 오명을 방지한다.
- 재사용성(Resuability): 모듈의 성격을 잘 나누어 놓으면 다음 프로젝트에서 그대로 사용해 쓸 수 있다. 보통 SDK나 Base Framework을 만들어 놓으면 초기 구축 비용을 최소로 할 수 있다.
JohnPapa Angular 스타일 가이드를 보면 특별히 모듈을 지원하는 라이브러리의 도움없이 자바스크립트 모듈 패턴 방식으로 앵귤러 v1 코드를 작성토록 가이드하고 있고, 앵귤러 팀에서도 공식적으로 추천하고 있다.
(function () {
'use strict';
angular
.module('a3.common.action')
.factory('currentAction', currentAction);
/* @ngInject */
function currentAction(ActionType, stateManager) {
return {
setDashboard: setDashboard,
setWorkspace: setWorkspace
};
function setDashboard(dashboardId) {
var action = {
...
};
stateManager.dispatch(action);
}
function setWorkspace(workspaceId, taskerId) {
var action = {
...
};
stateManager.dispatch(action);
}
}
})();
순수 자바스크립트로 모듈 단위로 만든 후 상호 운영은 어떻게 해야할까? 일단 index.html에 설정을 하고 사용하는 순서에 index.html에 script 태그를 통해 로딩을 하는 간단한 방식을 생각해 볼 수 있다. 하지만 필요한 시점에 자바스크립트에서 로딩을 해서 사용하는 방식을 명시적으로 하려면 별도 로더의 도움이 필요하다.
CommonJS & AMD & UMD
CommonJS는 지정한 코드를 동기적으로 로딩하는 방식으로 서버 사이드의 Node.js에서 사용한다.
- Object 만을 대상으로 한다.
- module.exports 구문으로 Object를 export 한다
- require 구문으로 Object를 import 한다.
// 파일명: module.js
function module() {
this.hi = function () { return 'hi'; }
}
module.exports = module;
// 사용하는 파일: test.js
var module = require('module');
var m = new module();
m.hi();
위에서 require를 차례로 호출하면 동기적으로 하나씩 로딩을 한다. 즉, 비동기적이지 않기 때문에 로딩이 전부 되어야 수행이 된다. 브라우져에서 동기적으로 모듈 파일을 로딩하게 되면 모든 파일이 로딩된 후 화면이 실행되므로 성능 이슈를 야기할 수 있다. 따라서 CommonJS는 Node.js에서 주로 사용하고 브라우져에서는 사용하지 않는다.
AMD(Asynchronous Module Definition)은 비동적으로 모듈을 로딩한다.
- Object, function, constructor, string, JSON 등 다른 타입들도 로딩이 가능한다.
- define 구문을 사용한다.
define(['jquery', 'angular'], function($, angular) {
...
});
jquery, angular 파일에 대해 비동기적으로 로딩한다. AMD대표적 구현체로는 RequireJS가 있다.
UMD(Universal Module Definition)은 AMD와 CommonJS의 기능을 둘다 지원하는 것이다.
- AMD, CommonJS를 고려한다
- 구현체로 SystemJS를 들 수 있다. SystemJS는 Universal dynamic module loader로 AMD, CommonJS뿐만 아니라 브라우져의 global scripts와 NodeJS 패키지 로딩을 하고 Traceur 또는 Babel 과 같이 작동할 수도 있다. 특히, Angular 2에서 사용한다.
- 아래와 같이 CommonJS와 AMD를 체크하여 사용할 수도 있다. 구현 방식에 대한 다양한 예를 참조한다.
(function (d3, jQuery) {
'use strict';
var Sankey2 = { ... };
....
// Support AMD
if (typeof define === 'function' && define.amd) {
define('Sankey2', ['d3'], Sankey2);
}
// Support CommonJS
else if ('undefined' !== typeof exports && 'undefined' !== typeof module) {
module.exports = Sankey2;
}
// Support window
else {
window.Sankey2 = Sankey2;
}
})(window.d3, window.$);
브라우져에서 ES6 module loader가 아닌 SystemJS (Universal module loader)를 사용할 경우 System.import 호출로 AMD, CommonJS, ES6 모듈 형식을 로딩할 수 있게 API를 제공하고 패키지 메니져로 JSPM을 사용할 수도 있다. JSPM은 무저항 브라우져용 모듈 패키지 메니져 (frictionless browser package management)로써 ES6 module loader가 작동하지 않는 곳에서 사용하는 Polyfill 이면서 AMD, CommonJS, Globals 자바스크립트 모듈 형식을 로딩할 수 있다.
Native JS
자바스크립트 ES2015 (ES6)에서 모듈 로더를 공식지원한다. ES2015는 모듈의 importing과 exporting을 제공한다. (참조) 간결관 syntax와 비동기 로딩과 cyclic dependencies에 대해 보다 잘 지원을 한다.
- import, export 를 사용한다.
// app.js
export let count = 1;
export function hi() {
return 'hi-' + count++;
}
// test.js
import * as app from './app';
console.log(app.hi());
console.log(app.count);
모듈로더에 대해 정리를 해보자. 자바스크립트 개발시 모듈 패턴에 입각하여 개발할 때 다양한 방식을 사용할 수 있으나
- NodeJS 기반 서버 사이드 개발은 CommonJS 이고
- Browser 기반 클라이언트 사이드 개발은 AMD구현체 중 하나인 RequireJS 를 사용한다.
ppt에서 r.js는 RequireJS에서 제공하는 모듈 번들러이다. 모듈 로더에 맞는 모듈을 개발한 후에 모듈 파일을 운영 배포하기 위해 번들링 즉, 묶는 과정을 거친다. 번들링 방법은 대해 다음 글에서 살펴보자.
<참조>
- 모듈 번들링: Browserify & Webpack
- UMD 구현 예
- SystemJS: Universal dynamic module loader
- Rollup.js: 차세대 Javascript module bundler
- ES6 Module Loader Polyfill: Top Level SystemJS