본문으로 바로가기


JavaScript 표준을 위한 움직임 : CommonJS 와 AMD


범용적인 목적으로 JavaScript를 사용하기 위해 필요한 선결 조건은 모듈화입니다. Node.js도 이런 모듈화 작업때문에 탄생할 수 있었습니다. JavaScript 모듈화 작업의 선두 주자는 CommonJS와 AMD입니다.



이글에서는 CommonJS와 AMD의 JavaScript 모듈화에 대해 간략하게 설명합니다.




CommonJS

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


이 그룹은 JavaScript를 범용적으로 사용하기 위해 필요한 '명세(Specification)'를 만드는 일을 한다. 이 그룹에서 현재까지 정의한 명세로는 Module 명세 1.0, 1.1, 1.1.1 등이 있다. Node.js 모듈도 Module 명세 1.0을 따르고 있다.





탄생배경


1996년 JavaScript가 탄생한 후, JavaScript를 브라우저 밖에서도 사용하려는 노력이 끊임없이 이어져 왔다. 

대표적인 프로젝트로 Helma, AppJet, Jaxer, Persever, Cappucino, Rhino 등이 있지만 큰 성공을 거두진 못했다.


2005년 Ajax가 부상하면서 JavaScript의 중요성은 그전보다 더 부각되었다. Ajax의 활성화와 함께 JavaScript 연산이 증가했고, 자연스레 더 빠른 JavaScript 엔진이 필요하게 되었다. 이런 맥락에서 2008년 Google에서 공개한 V8 JavaScript 엔진은 많은 주목을 받았다. V8 엔진은 기존의 JavaScript 엔진보다 월등히 빨랐을 뿐만 아니라, 브라우저 밖에서도 충분히 쓸만한 성능을 자랑했다.


V8 엔진의 등장은 서버사이드 JavaScript 진영에도 활기를 불어넣었다. 2009년 1월 Kevin Dangoor는 자신의 블로그에 서버사이드 JavaScript에 대한 아이디어를 제시하고, 함께할 사람을 모으기 시작했다. 

Kevin은 서버사이드 JavaScript가 성공하려면 기술적인 맥락에 치중하는 것보다는 공동으로 표준을 정하고 표준을 지켜나가는 활동이 필요하다고 보았다. 이렇게 시작한 CommonJS 그룹은 3개월만에 CommonJS API 0.1을 발표한다.



참고:  http://www.commonjs.org/history/에서 간단히 정리한 CommonJS 프로젝트의 역사를 볼 수 있다.




서버사이드 JavaScript 의 주요 쟁점

Kevin은 JavaScript가 브라우저용 언어를 넘어 범용적으로 쓰이려면, Ruby나 Python과 같은 체계가 필요하다고 주장했다. Kevin이 제기한 핵심 문제를 정리하면 다음과 같다.


1.  서로 호환되는 표준 라이브러리가 없다.

2.  데이터베이스에 연결할 수 있는 표준 인터페이스가 없다.

3.  다른 모듈을 삽입하는 표준적인 방법이 없다.

4.  코드를 패키징해서 배포하고 설치하는 방법이 필요하다.

5.  의존성 문제까지 해결하는 공통 패키지 모듈 저장소가 필요하다.







핵심은 모듈화

앞에서 언급한 문제점들은 결국 모듈화로 귀결된다. 그리고 CommonJS의 주요 명세는 바로 이 모듈을 어떻게 정의하고, 어떻게 사용할 것인가에 대한 것이다.


모듈화는 아래와 같이 세 부분으로 이루어진다.


스코프(Scope): 모든 모듈은 자신만의 독립적인 실행 영역이 있어야 한다.

정의(Definition): 모듈 정의는 exports 객체를 이용한다.

사용(Usage): 모듈 사용은 require 함수를 이용한다.



먼저 모듈은 자신만의 독립적인 실행 영역이 있어야 한다. 따라서 전역변수와 지역변수를 분리하는 것이 매우 중요하다. 서버사이드 JavaScript의 경우에는 파일마다 독립적인 파일 스코프가 있기 때문에 파일 하나에 모듈 하나를 작성하면 간단히 해결된다. 즉 서버사이드 JavaScript는 아래와 같이 작성하더라도 전역변수가 겹치지 않는다.


fileA.js


var a = 3;
b = 4;


fileB.js

var a = 5; b = 6;

그리고 두 모듈(파일) 사이에 정보 교환이 필요하다면, exports라는 전역객체를 통해 공유하게 된다. 아래 예제에서는 fileA.js 파일의 sum 함수가 외부로 공개된다.



fileA.js


var a = 3;

b=4;

exports.sum = function(c, d) {

return a + b + c + d;

}



var a = 5;

b = 6;

var moduleA = require("fileA");

moduleA.sum(a,b); // 3+4+5+6 = 18


위의 예에서 CommonJS의 모듈 명세는 모든 파일이 로컬 디스크에 있어 필요할 때 바로 불러올 수 있는 상황을 전제로 한다. 다시 말해 서버사이드 JavaScript 환경을 전제로 한다.


하지만 이런 방식은 브라우저에서는 결정적인 단점이 있다. 필요한 모듈을 모두 내려받을 때까지 아무것도 할 수 없게 되는 것이다. 


이 단점을 극복하려는 여러 방법이 CommonJS에서 논의되었지만, 결국 동적으로 <script> 태그를 삽입하는 방법으로 가닥을 잡는다. <script> 태그를 동적으로 삽입하는 방법은 JavaScript 로더들이 사용하는 가장 일반적인 방법이기도 하다.






비동기 모듈 로드(load) 문제

JavaScript가 브라우저에서 동작할 때는 서버 사이드 JavaScript와 달리 파일 단위의 스코프가 없다. 또한 표준 <script> 태그를 이용해 앞에서 예로 든 fileA와 fileB를 차례대로 로드하면, fileB의 변수가 fileA의 변수를 모두 덮어쓰게 되는 전역변수 문제도 발생한다.


이런 문제를 해결하려고 CommonJS는 서버 모듈을 비동기적으로 클라이언트에 전송할 수 있는 모듈 전송 포맷(module transport format)을 추가로 정의했다. 이 명세에 따라 서버사이드에서 사용하는 모듈을 다음 예의 브라우저에서 사용하는 모듈과 같이 전송 포맷으로 감싸면 서버 모듈을 비동기적으로 로드할 수 있게 된다.



서버사이드에서 사용하는 모듈


// complex-numbers/plus-two.js

var sum = require("./math").sum;

exports.plusTwo = function(a){

return sum(a, 2);

};




브라우저에서 사용하는 모듈


// complex-numbers/plus-two.js

require.define({"complex-numbers/plus-two": function(require, exports){

//콜백 함수 안에 모듈을 정의한다.

var sum = require("./complex-number").sum;

exports.plusTwo = function(a){

return sum(a, 2);

};

},["complex-numbers/math"]);

//먼저 로드되어야 할 모듈을 기술한다.


브라우저에서 사용하는 모듈 부분에서 특히 주목해야 할 것은 require.define() 함수를 통해(함수 클로저) 전역변수를 통제하고 있다는 사실이다.



CommonJS 모듈 전송 포맷에 대한 자세한 내용과 명세는 http://wiki.commonjs.org/wiki/Modules/Transport 를 참조한다




CommonJS 를 따르는 사람들


CommonJS는 현재 실질적인 표준(de facto standard) 역할을 하고 있다. 따라서 많은 서드파티 벤더들이 CommonJS 모듈 명세에 따라 모듈을 만들거나 모듈 로드 시스템을 만들고 있다. 이 명세를 따르는 대표적인 프로젝트로는 Node.js가 있다. 


그 밖에도 다음과 같은 로더와 프레임워크가 CommonJS 모듈 명세를 따르고 있다.


@ 브라우저용

curl.js(http://github.com/unscriptable/curl)

SproutCore(http://sproutcore.com)

PINF(http://github.com/pinf/loader-js)

기타 등등



@ 서버사이드용

Node.js(http://nodejs.org)

Narwhal(https://github.com/tlrobinson/narwhal)

Persevere(http://www.persvr.org)

Wakanda(http://www.wakandasoft.com)


위의 목록만 보더라도 CommonJS가 꼭 서버사이드에 국한된 이야기가 아니라는 사실을 알 수 있다. 


하지만 CommonJS를 만든 목적이 서버사이드에서 JavaScript를 사용하는 것이었기 때문에 서버사이드 용으로 사용할 때에 장점이 많다.





AMD

JavaScript 표준 API 라이브러리 제작 그룹에는 CommonJS만 있는 것이 아니고, AMD(Asynchronous Module Definition)라는 그룹도 있다. AMD 그룹은 비동기 상황에서도 JavaScript 모듈을 쓰기 위해 CommonJS에서 함께 논의하다 합의점을 이루지 못하고 독립한 그룹이다.


본래 CommonJS가 JavaScript를 브라우저 밖으로 꺼내기 위한 노력의 일환으로 탄생했기 때문에 브라우저 내에서의 실행에 중점을 두었던 AMD와는 합의를 이끌어 내지 못하고 결국 둘이 분리되었다. CommonJS 공식 위키에도 AMD가 독립했다는 사실을 알리고 있다.





두 줄기의 표준화 움직임

AMD가 목표로 하는 것은 필요한 모듈을 네트워크를 이용해 내려받아야 하는 브라우저 환경에서도 모듈을 사용할 수 있도록 표준을 만드는 일이다. 따라서 현재 JavaScript 모듈화에 대한 논의는 크게 CommonJS 진영과 AMD 진영으로 나뉘게 되었다. 


둘 중에 무엇이 더 좋다고 이야기할 수는 없다. 


왜냐하면 AMD도 브라우저에서 동작하는 JavaScript만을 대상으로 모듈을 정의하지는 않았기 때문이다.



AMD를 지원하는 모듈 로더와 프레임워크는 아래와 같다.


@ 브라우저용

RequireJS(http://requirejs.org)


curl.js(http://github.com/unscriptable/curl)


bdLoad(http://bdframework.org/bdLoad/)


Yabble(http://github.com/jbrantly/yabble)


PINF(http://github.com/pinf/loader-js)


기타 등등




@ 서버사이드용

RequireJS(http://requirejs.org)


PINF(http://github.com/pinf/loader-js)





CommonJS와 AMD 비교

두 진영에서 정의하는 모듈 명세의 차이는 모듈 로드에 있다.


필요한 파일이 모두 로컬 디스크에 있어 바로 불러 쓸 수 있는 상황, 즉 서버사이드에서는 CommonJS 명세가 AMD 방식보다 간결하다. 반면 필요한 파일을 네트워크를 통해 내려받아야 하는 브라우저와 같은 환경에서는 AMD가 CommonJS보다 더 유연한 방법을 제공한다.





AMD의 모듈 명세

'Asynchronous Module Definition'이라는 말에서 알 수 있듯이, AMD에서는 비동기 모듈(필요한 모듈을 네트워크를 통해 내려받을 수 있도록 하는 것)에 대한 표준안을 다루고 있다. 물론 CommonJS도 비동기 상황을 고려한 모듈 전송 포맷을 제공하지만, 순수 AMD를 지지하는 사람들과 합의를 도출해 내지는 못했다. 이런 역사적 배경 때문에 AMD는 CommonJS와 많은 부분이 닮아 있거나 호환할 수 있는 기능을 제공한다. require() 함수를 사용할 수 있으며, exports 형태로 모듈을 정의할 수도 있다.


물론 AMD만의 특징도 있다. 대표적으로 꼽을 수 있는 것이 바로 define() 함수다. 브라우저 환경의 JavaScript는 파일 스코프가 따로 존재하지 않기 때문에 이 define() 함수로 파일 스코프의 역할을 대신한다. 즉, 일종의 네임스페이스 역할을 하여 모듈에서 사용하는 변수와 전역변수를 분리한다. 물론 define() 함수는 전역함수로 AMD 명세를 구현하는 서드파티 벤더가 모듈 로더에 구현해야 한다.


그 밖에 define.amd라는 프로퍼티도 있다. AMD에 대한 개념을 더 알고 싶다면 http://unscriptable.com/code/Using-AMD-loaders/#0 문서를 참조한다.






define() 함수


define()함수는 전역함수로 다음과 같이 정의한다.



define(id?, dependencies?, factory);


첫 번째 인수 id는 모듈을 식별하는데 사용하는 인수로, 선택적으로 사용한다. id가 없으면 로더가 요청하는 <script> 태그의 src 값을 기본 id로 설정한다. 특별히 명시할 필요가 없다면 사용하지 않는다. 만약 id를 명시한다면 파일의 절대 경로를 식별자로 지정해야 한다.


두 번째 인수는 정의하려는 모듈의 의존성을 나타내는 배열로, 반드시 먼저 로드돼야 하는 모듈을 나타낸다. 이렇게 먼저 로드된 모듈은 세 번째 인수인 팩토리 함수의 인수로 넘겨진다. 두 번째 인수 역시 선택적으로 사용하지만, 생략한다면 ['require', 'exports', 'module']이라는 이름이 기본으로 지정된다. 그리고 이 세가지 모듈은 CommonJS에서 정의한 전역객체와 동일한 역할을 하게 된다.


세 번째 인수는 팩토리 함수로, 모듈이나 객체를 인스턴스화하는 실제 구현을 담당한다. 만약 팩토리 인수가 함수라면 싱글톤으로 한 번만 실행되고, 반환되는 값이 있다면 그 값을 exports 객체의 속성값으로 할당한다. 반면에 팩토리 인수가 객체라면 exports 객체의 속성값으로 할당된다.





전역변수와 define.amd 프로퍼티


AMD 명세에서 정의하는 전역변수는 define과 CommonJS에서 사용하는 require 객체, exports 객체가 있다. 

그리고 전역 모듈을 명시적으로 가리킬 때 사용하는 define.amd 프로퍼티도 사용할 수 있다. 하지만 그밖에 다른 전역변수나 메서드(또는 프로퍼티)를 추가하면 안 된다.







AMD로 정의한 모듈 예시

다음 예제는 3가지 인수를 모두 사용하는 기본 AMD 모듈로, alpha라는 모듈을 정의하는데 beta라는 모듈이 필요하다는 것을 나타낸다.


define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {

exports.verb = function() {

// 넘겨받는 인수를 사용해도 되고

return beta.verb();

// 또는 require()를 이용해

// 얻어 온 모듈을 사용해도 된다.

return require("beta").verb();

}

});


두 번째 예제는 첫 번째 인수를 생략한 예제로, alpha라는 모듈을 필요로 하는 이름 없는 모듈을 만든다. 이때 require() 함수로 이 모듈을 사용하고 싶다면, 이 모듈이 정의된 파일의 경로를 지정해야 한다.


define(["alpha"], function (alpha) {
return {
verb: function(){
return alpha.verb() + 2;
}

};

});


// 위 모듈이
// http://somewhere.com/js/modelBeta.js
// 접근이 가능하다고 가정하면,

require(["/js/modelBeta.js"], function(moduelBeta)(){

// moduleBeta를 활용하는 실제 코드는 여기에..

});



다음 예제는 의존성이 없는 모듈을 정의한다. 이 모듈 역시, 첫 번째 id 식별자가 없으므로, 모듈이 정의된 파일의 경로가 자동으로 식별자로 지정된다.


define({
add: function(x, y){
return x + y;
}
});




마지막으로 CommonJS 형태의 모듈을 래핑할 수도 있다.


define(function (require, exports, module) {
var a = require('a'),
b = require('b');

exports.action = function () {};

});





AMD 의 장점

AMD 모듈 명세의 장점은 단연 비동기 환경에서도 매우 잘 동작할 뿐만 아니라, 서버사이드에서도 동일한 코드로 동작한다는 점이다. 그리고 CommonJS의 모듈 전송 포맷보다는 확실히 간단하고 명확하다.


AMD 명세는 define() 함수(클로저를 이용한 모듈 패턴)를 이용해 모듈을 구현하므로 전역변수 문제가 없다. 또한 해당 모듈을 필요한 시점에 로드하는 Lazy-Load 기법을 응용할 수도 있다.


성능 측면에서 보면, 확실히 구 버전의 Internet Explorer에서는 많은 이득을 볼 수 있지만, 그 외의 최신 브라우저에서는 성능이 비슷하다. 물론 최적의 성능을 보장하려면 하나의 파일로 머지해서 배포하는 것이 좋지만 AMD 로더를 사용해도 성능 차이가 그리 크지 않다.





RequireJS

RequireJS는 인지도 높은 JavaScript 로더 중 하나로, AMD 명세를 충실히 구현했을 뿐만 아니라 CommonJS 스타일의 포맷도 지원한다. RequireJS의 가장 큰 장점은 깔끔한 API와 문서를 갖추고 있고, 사용법 또한 쉽다는 점이다. 그리고 사이즈도 작다.


먼저 인덱스 페이지에 다음과 같이 require.js 라이브러리를 추가한다.


<script type="text/javascript" src="/js/require.js"></script>
<script type="text/javascript" src="/js/main.js"></script>



그리고 main.js 파일에서 동적으로 Jindo 프레임워크를 로드한다.


require(["js/jindo.min"], function(jindo) {

// 실제 구현은 여기에...

}



마지막으로, 로드할 Jindo 프레임워크(jindo.min.js)를 define() 함수로 래핑한다.


define(function(){
// Jindo 구현 코드 위치...

return jindo;
// 최종 Jindo 네임스페이스 객체 반환

}


위와 같이 작성하고 실행하면 다음과 같이 동적으로 jindo.min.js 파일이 로드된다. jindo.min.js 파일이 main.js 파일보다 늦게 로드됨에도 불구하고 정상적으로 동작한다.






모듈화와 HTML5

최근 모듈 로더에서는 HTML5의 로컬 스토리지(localStorage) 연동을 활용하는 사례가 나타나고 있다. 즉, 기존에 모듈을 원격 서버에서 불러오던 것을 로컬 스토리지를 활용해 성능을 극대화하는 것이다. 물론 버전에 따른 의존성은 모두 모듈 로더가 책임진다.


로컬 스토리지를 활용하는 AMD 호환 로더의 사례를 https://github.com/zazl/lsjs에서 볼 수 있다.


지금까지는 모든 것을 해결해주는 프레임워크가 대세였지만, 앞으로는 필요한 모듈만 가져다 사용하는 시대가 올 것으로 예상된다. 특히 모바일 환경에서 사이즈가 중요하기 때문에 모듈화는 더욱 중요해졌다. 특히 최신 브라우저가 대부분인 스마트폰에서 로컬 스토리지를 활용한 성능 극대화는 더욱더 필요해 보인다.




마치며

AMD 역시 핵심은 모듈화에 있다. AMD의 가장 큰 장점은 지금 당장 브라우저 환경에 적용할 수 있다는 점이다. 그리고 JavaScript 모듈화는 머지 않아 중요한 이슈로 다가올 것이다. 현재 JavaScript 표준화는 그 어떤 때보다 빠른 진전을 보이고 있고, ECMAScript Harmony도 모듈에 대한 준비를 하고 있다. 지금까지 정리된 내용만 보아도 JavaScript 표준화에 대한 논의가 상당히 많이 진행된 상태다. 물론 ECMAScript Harmony가 언제 현실로 다가올지는 가늠하기 어렵다.


하지만 ECMAScript Harmony를 기다릴 필요가 없다. 이미 시중에는 AMD를 구현한 쓸만한 로더들이 많이 있을 뿐만 아니라 현재의 JavaScript만으로도 서버와 클라이언트 모두 개발이 가능해진 상황이다. 심지어 데스크톱 애플리케이션은 물론 모바일 앱 개발도 가능하다.


머지않은 미래에 더 범용적인 언어로, 그리고 보다 발전된 언어로 JavaScript가 자리매김할 날이 올 것을 기대해 본다.






댓글을 달아 주세요