본문 바로가기

0/javascript

JavaScript 표준의 선구자들 : CommonJS와 AMD

반응형

범용적으로 JavaScript를 사용하기 위해 필요한 조건은 모듈화입니다. 이 모듈화 작업의 선두 주자는 CommonJS와 AMD입니다. CommonJS와 AMD의 JavaScriot 모듈화에 대해 간략하게 살펴봅시다.

CommonJS

JS를 브라우저에서뿐만 아니라, 서버사이드 애플리케이션이나 데스크톱 애플리케이션에서도 사용하려고 조직한 자발적 워킹 그룹.

CommonJS의 'Common'은 JS를 브라우저에서만 사용하는 언어가 아닌 일반적인 범용 언어로 사용할 수 있도록하겠다는 의지를 나타내고 있는 것이라고 이해할 수 있다.

이 그룹은 JS를 범용적으로 사용하기 위해 필요한 '명세(Specification)'를 만드는 일을 한다.

탄생 배경

1996년 JS가 탄생한 후, JS를 브라우저 밖에서도 사용하려는 노력이 끊임없이 이어져 왔다. 대표적인 프로젝트로 Helma, AppJet, Jaxer, Persever, Cappucino, Rhino 등이 있지만 큰 성공을 거두진 못했다.

V8 엔진의 등장은 서버사이드 JS 진영에도 활기를 불어넣었다. 2009년 1월 Kevin Dangoor는 자신의 블로그에 서버사이드 JS에 대한 아이디어를 제시하고, 함께할 사람을 모으기 시작했다. 이렇게 시작한 CommonJS 그룹은 3개월만에 CommonJS API 0.1을 발표한다.2005년 AJAX가 부상하면서 JS의 중요성은 그 전보다 더 부각되었다. AJAX의 활성화와 함께 JS 연산이 증가했고, 자연스레 더 빠른 JS 엔진이 필요하게 되었다. 이런 맥락에서 2008년 Google에서 V8 JS 엔진을 공개! 엄청난 주목을 받았다. 이 엔진은 기존의 JS 엔진보다 월등히 빨랐을 뿐 아니라, 브라우저 밖에서도 충분히 쓸만한 성능을 자랑했다.

참고 : CommonJS Project History

서버사이드 JavaScript의 주요 쟁점

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

  • 서로 호환되는 표준 라이브러리가 없다.
  • 데이터베이스에 연결할 수 있는 표준 인터페이스가 없다.
  • 다른 모듈을 삽입하는 표준적인 방법이 없다.
  • 코드를 패키징해서 배포하고 설치하는 방법이 필요하다.
  • 의존성 문제까지 해결하는 공통 패키지 모듈 저장소가 필요하다.

핵심은 모듈화

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

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

  • 스코프 (Scope) : 모든 모듈은 자신만의 독립적인 실행 영역이 있어야 한다.
  • 정의 (Definition) : 모듈 정의는 exports 객체를 이용한다.
  • 사용 (Usage) : 모듈 사용은 require 함수를 이용한다.

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

// A.js
var a = 3;
b = 4;

// B.js
var a = 5; b = 6;

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

// A.js
var a = 3;
b = 4;
exports.sum = function(c, d) {
	return a + b + c + d;
}

// B.js
var a = 5;
b = 6;

이렇게 공개된 함수를 다른 모듈에서 사용하려면, 다음과 같이 require() 함수를 이용한다.

// A.js
var a = 3;
b = 4;
exports.sum = function (c, d){
	return a + b + c + d;
}

// B.js
var a = 5;
b = 6;
var moduleA = require("A");
moduleA.sum(a, b); // 3+4+5+6 =  18

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

하지만 이런 방식은 브라우저에서는 결정적인 단점이 있다. 필요한 모듈은 모두 내려받을 때까지 아무것도 할 수 없게 되는 것이다. 이 단점을 극복하려는 여러 방법이 CommonJS에서 논의되었지만, 결국 동적으로 <script> 태그를 삽입하는 방법으로 가닥을 잡는다. <script> 태그를 동적으로 삽입하는 방법은 JS 로더들이 사용하는 가장 일반적인 방법이기도 하다.

비동기 모듈 로드 문제

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

이런 문제를 해결하려고 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() 함수를 통해(함수 클로저) 전역 변수를 통제하고 있다는 사실이다.

참고: Modules/Transport

CommonJS를 따르는 사람들

CommomnJS는 현재 실질적인 표준 역할을 하고 있다. 따라서 많은 서드파티 벤더들이 CommonJS 모듈 명세에 따라 모듈을 만들거나 모듈 로드 시스템을 만들고 있다. 이 명세를 따르는 대표적인 프로젝트로는 Node.js가 있다. 그 밖에도 다음과 같은 로더와 프레임워크가 CommonJS 모듈 명세를 따르고 있다.

위의 목록만 보더라도 CommonJS가 꼭 서버사이드에 국한된 이야기가 아니라는 사실을 알 수 있다. 하지만 CommonJS를 만든 목적이 서버사이드에서 JS를 사용하는 것이었기 떄문에 서버사이드 용으로 사용할 때에 장점이 많다.

AMD

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

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

두 줄기의 표준화 움직임

AMD가 목표로 하는 것은 필요한 모듈을 네트워크를 이용해 내려받아야 하는 브라우저 환경에서도 모듈을 사용할 수 있도록 표준을 만드는 일이다. 따라서 현재 JS 모듈화에 대한 논의는 크게 CommonJS 진영과 AMD 진영으로 나뉘게 되었다. 둘 중에 무엇이 더 좋다고 이야기 할 수는 없다. 왜냐면 AMD도 브라우저에서 동작하는 JS만을 대상으로 모듈을 정의하지는 않았기 때문이다.

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

CommonJS와 AMD 비교

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

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

AMD의 모듈 명세

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

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

참고 : Using AMD loaders to write and manage modular javascript 

define() 함수

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

define(id?, dependencies?, factory);

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

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

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

AMD로 정의한 모듈 예시

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

define("alpha", ["require", "exports", "beta"], function(require, exports, 
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(moduleBeta){
	// 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와 문서를 갖추고 있고, 사용법 또한 쉽다는 점이다. 그리고 사이즈도 작다.

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

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

require(["js/something.min"], function(something){
	// 구현
})

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

define(function(){
	// something 구현 코드 위치
    ...
    return something;
    // 최종 something 네임스페이스 객체 반환
})

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

모듈화와 HTML5

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

참고

로컬 스토리지를 활용하는 AMD 호환 로더의 사례를 Github 에서 볼 수 있다.

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

마치며

AMD 역시 핵심은 모듈화에 있다. AMD의 가장 큰 장점은 지금 당장 브라우저 환경에 적용할 수 있다는 점이다.  

 

이 글은 네이버 D2를 참고하여 정리한 글입니다.

반응형