RequireJS - AMD의 이해와 개발 개발자Tip
2013.12.23 16:58
네이버 캘린더/주소록개발랩 이경내
JavaScript 개발은 웹 사이트의 규모가 커질수록 소스를 관리하고 배포하는 비용이 커지는 경향이 있습니다. 또한 오래된 소스의 의존성 파악이 어려워 섣불리 수정하지 못하는 상황에 처하기도 합니다. 더 나은 웹 사이트 혹은 웹앱을 위해서는 해결해야 할 과제이며, 이는 RequireJS를 사용하여 라이브러리 차원에서 보완할 수 있습니다.
이 글에서는 RequireJS의 바탕이 되는 AMD(Asynchronous Module Definition)의 기본 개념을 살펴보고 RequireJS를 이용한 개발 가이드를 제시합니다.
AMD
AMD는 동적 로딩, 의존성 관리, 모듈화가 톱니바퀴처럼 아름답게 맞물린 API 디자인을 제시한다. AMD의 자세한 배경과 연관 기술에 관해서는 "JavaScript 표준을 위한 움직임: CommonJS와 AMD"를 참고한다. 이 글에서는 AMD의 근간이 되는 3가지 개념을 살펴보겠다.
동적 로딩
var scriptEl = document.createElement('script');
scriptEl.type = 'text/javascript';
scriptEl.src = 'example.js';
document.getElementsByTagName('head')[0].appendChild(scriptEl);
function loadScript(url, callback) {
var scriptEl = document.createElement('script');
scriptEl.type = 'text/javascript';
// IE에서는 onreadystatechange를 사용
scriptEl.onload = function () {
callback();
};
scriptEl.src = url;
document.getElementsByTagName('head')[0].appendChild(scriptEl);
}
loadScript('example.js', function () {
// example.js가 로딩 완료한 시점에 실행
});
하지만 보통 파일이 여러 개 필요하고 각 파일의 삽입 순서를 지켜야 하기 때문에 위 함수만으로는 아래와 같은 콜백 지옥(?)에 빠질 수 있다.
loadScript('file1.js', function () {
loadScript('file2.js', function () {
loadScript('file3.js', function () {
loadScript('file4.js', function () {
// 콜백 지옥에 빠졌다.
});
});
});
});
의존성 관리
defineModule('util', {
trim: function () {
//
},
extend: function () {
//
}
});
var util = loadModule('util');
util.trim();
모듈화
var foo = (function () {
var i = 0;
function init() {
reset();
}
function reset() {
i = 0;
}
function increase() {
i++;
}
function decrease() {
i--;
}
function get() {
return i;
}
return {
init: init,
increase: increase,
decrease: decrease,
get: get
};
}());
foo.increase();
console.log(foo.get()); // 1
foo.decrease();
console.log(foo.get()); // 0
console.log(foo.i); // undefined
foo.reset(); // Error
위의 foo 모듈은 결과적으로 단순 객체를 반환하므로 싱글턴(Singleton)으로 볼 수 있는데, 이를 조금 응용하면 모듈을 일종의 클래스(Class)처럼 사용할 수도 있다.
var Foo = (function () {
var NAME = 'Foo';
// 생성자 함수
function Foo() {
this.i = 0;
}
Foo.prototype.getClassName = function () {
return NAME;
};
Foo.prototype.increase = function () {
this.i++;
};
Foo.prototype.decrease = function () {
this.i--;
};
return Foo;
}());
var foo = new Foo();
RequireJS
RequireJS는 AMD API 명세를 구현한 구현체 중 하나이다. 여기에 조금 더 편리하게 사용할 수 있도록 몇 가지 기능들을 추가했다. RequireJS의 자세한 사용법은 http://requirejs.org/docs/api.html#usage를 참고한다. 이 글에서는 실제 개발에 도움이 될만한 노하우를 설명한다.
모듈 정의와 사용
모듈을 정의하는 기본 형태는 다음과 같다.
/* js/foo.js */
// 모듈 정의의 기본 형태
define([ // 의존 모듈들을 나열한다. 모듈이 한 개라도 배열로 넘겨야 한다.
'js/util',
'js/Ajax',
'js/Event'
], function (util, Ajax, Event) { // 의존 모듈들은 순서대로 매개변수에 담긴다.
// 의존 모듈들이 모두 로딩 완료되면 이 함수를 실행한다.
// 초기화 영역
var i = 0;
function increase() {
i++;
}
function get() {
return i;
}
// 외부에 노출할 함수들만 반환한다.
return {
increase: increase,
get: get
};
});
/* js/main.js */
require([
'js/foo'
], function (foo) {
console.log(foo.get()); // 0
foo.increase();
console.log(foo.get()); // 1
});
모듈의 이름을 명시적으로 설정할 수도 있지만 이름 없는 모듈로 정의하는 것을 권장한다. 이름 없는 모듈은 호출될 때 모듈의 위치에 따라 이름을 결정한다. 개발할 때 파일의 이름이나 위치는 자주 변경되므로 유연한 상태로 둘 필요가 있다.
의존 모듈은 배열로 나열하긴 했지만 로딩 순서를 보장한다는 뜻은 아니다. 순서에 상관없이 병렬로 네트워크를 통해 다운로드되거나 브라우저의 캐시에서 꺼내진다. 어떤 모듈이 먼저 로딩되어 실행될지 모른다. 따라서 로딩 순서가 중요하다면 아래와 같이 require를 중첩해서 사용하는 방법이 있다.
require(['js/first'], function (first) {
require(['js/second'], function (second) {
//
});
});
모듈은 처음 호출할 때만 초기화된다. 모듈이 처음 호출되어 로딩 완료되면 모듈 정의 함수(위 코드에서는 두 번째 매개변수)를 실행하고 그 결과 값을 RequireJS 내부의 비밀 공간에 저장한다. 이후 어디에서건 같은 모듈을 호출할 때는 저장된 결과값을 반환하며, 모듈 정의 함수를 매번 실행하지 않는다. 그래서 모듈 정의 함수가 처음 생성한 클로저(Closure)로 초기화 영역 내의 변수, 함수들을 계속 사용할 수 있는 것이다. 즉 모듈의 상태는 유지된다. 위 예제의 foo 모듈을 사용하여 아래에서 확인해 보자.
/* js/first.js */
define([
'js/foo'
], function (foo) {
foo.increase();
return {
getFooValue: function () {
return foo.get();
}
};
});
/* js/second.js */
define([
'js/foo'
], function (foo) {
return {
getFooValue: function () {
return foo.get();
}
};
});
/* js/main.js */
require([
'js/first'
], function (first) {
console.log(first.getFooValue()); // 1
require([
'js/second'
], function (second) {
console.log(second.getFooValue()); // 1
});
});
모든 모듈이 foo 모듈처럼 싱글턴(Singleton) 구현은 아닐 것이다. 다음과 같이 인스턴스 객체를 생성할 수 있는 클래스(Class) 형태의 구현도 필요하다.
/* js/Layer.js */
define(function() {
function Layer(el) {
this.el = el;
}
Layer.prototype.open = function () {
//
};
Layer.prototype.close = function () {
//
};
// 객체가 아닌 생성자 함수를 반환한다.
return Layer;
});
/* js/main.js */
require([
'js/Layer'
], function (Layer) {
var someLayer = new Layer(document.getElementById('some-layer'));
someLayer.open();
});
모듈의 상태를 유지할 필요가 없다면 다음과 같이 객체 리터럴만으로 간단히 정의할 수도 있다.
/* js/util.js */
define({
trim: function () {
//
},
extend: funciton () {
//
}
});
설정 옵션
RequireJS는 여러 설정 옵션들을 제공한다. 대표적인 옵션은 다음과 같다.
<script>
// RequireJS 설정 객체
// require.js가 로딩되면 이 객체를 자동으로 읽어 들여 반영한다.
var require = {
// 모듈의 기본 위치를 지정한다.
baseUrl: '/js/app',
// 모듈의 단축 경로 지정 또는 이름에 대한 별칭(Alias)을 지정할 수 있다.
paths: {
'lib': '../lib' // "/js/lib" 과 동일하다. baseUrl 기준
},
// AMD를 지원하지 않는 외부 라이브러리를 모듈로 사용할 수 있게 한다.
shim: {
'modernizr': { // Modernizr 라이브러리
exports: 'Modernizr'
}
},
// 모듈 위치 URL뒤에 덧붙여질 쿼리를 설정한다.
// 개발 환경에서는 브라우저 캐시를 회피하기 위해 사용할 수 있고,
// 실제 서비스 환경이라면 ts값을 배포한 시간으로 설정하여 새로 캐시하게 할 수 있다.
urlArgs : 'ts=' + (new Date()).getTime()
};
</script>
<script src="/js/lib/require.js"></script>
<script>
//
</script>
모듈 위치
/* /index.html */
<script>
var require = {
baseUrl: '/js/app'
};
</script>
<script src="/js/lib/require.js"></script>
<script>
require([
'common/relative', // (1) 위치: "/js/app/common/relative.js"
'dotjs.js', // (2) 위치: "/dotjs.js"
'/js/lib/absolute.js', // (3) 위치: "/js/lib/absolute.js"
'http://another.com/foo.js' // (4) 위치: "http://another.com/foo.js"
], function (relative, dotjs, absolute, foo) {
//
});
</script>
모듈이 아닌 외부 라이브러리 사용
AMD를 지원하지 않는 외부의 좋은 라이브러리를 사용하려면 paths와 shim 설정 옵션을 사용한다. 외부 라이브러리를 모듈처럼 사용할 수 있게 한다.
// 설정
var require = {
paths: {
// 이 설정으로 모듈 이름을 호출하면 값의 위치를 요청한다.
// ".js"는 자동 추가
'jquery': 'http://code.jquery.com/jquery-1.10.2',
'modernizr': 'http://modernizr.com/downloads/modernizr-latest',
'jindo': '/js/lib/jindo_component'
},
shim: {
'modernizr': {
// Modernizr는 전역변수 "Modernizr"를 사용한다.
exports: 'Modernizr'
}
'jindo': {
deps: ['/js/lib/jindo.desktop.all.ns'],
// Jindo(네임스페이스 버전)는 전역변수 "jindo"를 사용한다.
exports: 'jindo'
}
}
};
// 사용
require([
'jquery',
'modernizr',
'jindo'
], function (jquery, modernizr, jindo) {
console.log(jquery); // (1) jQuery
console.log(modernizr); // (2) Modernizr
console.log(jindo); // (3) Jindo
});
(3) Jindo의 경우는 약간 복잡하다. Jindo 라이브러리는 코어 라이브러리와 컴포넌트 라이브러리의 두 부분으로 나눌 수 있다(jQuery & jQuery UI와 비슷하다). 코어 라이브러리만 사용해도 되겠지만 유용한 컴포넌트들이 많으므로 보통 같이 사용한다. 그래서 모듈을 호출할 때 'jindo'만으로 코어와 컴포넌트를 모두 가져오도록 설정했다. 먼저 paths 설정을 살펴보면 코어 라이브러리가 아니라 컴포넌트 라이브러리가 지정되어 있다. 하지만 shim 옵션이 지정되어 있기 때문에, RequireJS가 '/js/lib/jindo_component.js'를 가져오기 전에 deps에 설정된 코어 파일인 '/js/lib/jindo.desktop.all.ns.js'를 먼저 가져오게 된다. 정리하면 "코어 파일 가져오기 -> 컴포넌트 파일 가져오기 -> 전역변수 jindo 추출"의 방식으로 동작한다. 여담으로, 컴포넌트 라이브러리의 경우 수십 개의 객체로 구성되어 있기 때문에 각 객체의 의존성을 파악하여 사용하는 개별 컴포넌트만 호출하도록 세세한 설정이 가능하지만 버전을 업데이트할 때마다 매번 새로 파악해야 하므로 적절하지 않다.
위의 세 가지 경우를 활용하면 대부분의 외부 라이브러리들은 사용할 수 있을 것이다.
사실 이렇게 옵션을 지정하지 않더라도 외부 라이브러리는 전역변수로 접근할 수 있기 때문에 다음과 같이 파일 URL을 호출해도 된다.
require([
'http://code.jquery.com/jquery-1.10.2.js'
], function (dummy$) {
console.log(dummy$); // undefined
console.log($); // jQuery는 전역변수 "$"와 "jQuery"를 사용한다.
});
하지만 아름답지는 않은 것 같다.
텍스트 로딩과 HTML 템플릿 관리
RequireJS는 몇 가지 플러그인을 제공한다. 이 중 text 플러그인은 JS 파일 외에도 CSS와 HTML같은 텍스트 파일도 불러올 수 있게 한다. 간단히 다음과 같이 사용한다.
define([
'text!/template.html' // 플러그인은 플러그인 이름 뒤에 !를 붙인다.
], function (templateHTML) {
//
});
이를 사용하면 현재 페이지에 노출되진 않지만 갖고 있어야 되는 HTML 조각들(Fragments)을 동적으로 관리할 수 있다.
require([
'jquery',
'text!/sections/layer1.html',
'text!/sections/layer2.html',
'text!/sections/layer3.html'
], function ($, layer1html, layer2html, layer3html) {
var $body = $(document.body),
$layer1 = $(layer1html),
$layer2 = $(layer2html),
$layer3 = $(layer3html);
$body.append($layer1);
});
위 HTML에 해당하는 CSS 파일도 동적으로 가져올 수 있지만 CSS는 한 번만 정의하면 페이지에 적용할 수 있고 , 용량도 작은 편이기 때문에 전통적인 방법으로 관리하는 것이 더 합리적이다.
마치며
이들 조합은 JavaScript 개발의 대안을 제시하고 있고, RequireJS 라이브러리는 이를 충실히 구현하고 있다. 네이버 캘린더 서비스는 RequireJS를 사용하여 새로 개발했고, 개발하는 동안 기존 개발 방식과 다른 즐거움을 느낄 수 있었다. 그러나 명세를 읽는 것과 실제 사용하는 것은 많이 다르기 때문에 RequireJS를 사용하면서 정리한 노하우를 최대한 전달하려고 노력했다.
이 글이 RequireJS를 사용하고 싶은 개발자들에게 도움이 됐으면 좋겠다.
참고 자료
출처 - http://helloworld.naver.com/helloworld/591319
'Development > JavaScript' 카테고리의 다른 글
javascript - window.JSON 객체 (0) | 2014.03.27 |
---|---|
javascript - DocumentFragment (0) | 2014.02.01 |
javascript - CommonJS와 AMD (0) | 2014.01.19 |
javascript - url link 자동 인식(autoLink) 처리 (0) | 2014.01.08 |
Fancybox not working (0) | 2014.01.06 |