RequireJS 살펴보기

RequireJS는 자바스크립트 코드를 모듈화 하고, 모듈간의 종속성을 깔끔하게 해결해 줍니다. Java나 Python과 같은 일반적인 언어들의 경우 언어 측면에서 기본적인 모듈화 방안을 제공합니다만, 자바스크립트의 경우 기본적으로 글로벌 네임스페이스를 공유함으로 신경써서 관리하지 않으면 유지보수가 어려워 집니다. RequireJS는 비단 브라우저 환경 뿐만 아니라, NodeJS와 같은 환경에서도 활용될 수 있으므로, 스크립트 최적화와 관리에 큰 도움이 될 것입니다.

이 글은 requirejs.org의 문서 내용을 기반으로 정리하였습니다. 번역 과정에서 일부 생략하고 의역한 부분이 있습니다. 번역 자체가 목적이 아니라 정리와 학습을 목적으로 함으로 좀 더 상세한 내용을 알고 싶으신 분은 원문을 참고 하시기 바랍니다.

requirejs logo

자바스크립트 파일 로딩

RequireJS는 기존의 <script>태그 방식과 다르게 스크립트를 로딩합니다. RequireJS의 궁극적인 목적은 자바스크립트 코드를 모듈화 하는 것입니다. 아울러 lazy-loading을 통해서 스크립트 로딩을 최적화 합니다. 그렇기 때문에, RequireJS에서는 스크립트를 URL이 아닌 모듈ID를 사용해 로딩합니다.

RequireJS는 모든 코드를 baseUrl에 상대경로 표시합니다. baseUrl은 보통 data-main 속성에서 사용한 경로와 같습니다. data-main 속성은 Requirejs에서 사용하는 특수한 속성으로서 진입점 역할을 합니다.

1
<script data-main="scripts/main.js" src="scripts/require.js"></script>

위 코드에서 baseUrl은 결국 scripts 디렉토리가 됩니다.

baseUrl은 추후 config에서 변경할 수도 있습니다. 위 예는 일반적으로 사용하는 기본값입니다. RequireJS는 모든 종속성이 스크립트로 간주되므로 모듈ID에 “.js”확장자를 생략할 수 있습니다.

경우에 따라 위와 같이 “baseUrl + path” 규칙 대신 좀 더 유연한 방식으로 경로를 설정할 수도 있습니다. 예를들어, 사용자정의 스크립트 모듈들과 써드파티 스크립트 모듈들을 구별하고 싶을경우 아래와 같이 디렉토리 레이아웃을 설정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
www/
index.html
js/
app/
sub.js
lib/
jquery.js
canvas.js
app.js
require.js

디렉토리 구조가 위와 같이 구성되어 있고, index.html에 아래와 같이 스크립트 로딩이 정의되어 있을 경우

1
<script data-main="js/app.js" src="js/require.js"></script>

app.js가 아래와 같이 작성되어 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
requirejs.config({
baseUrl: 'js/lib',
paths: {
app: '../app'
}
});
requirejs(['jquery', 'canvas', 'app/sub'],
function($, canvas, sub) {
// jQuery, canvas, app/sub 모듈이 모두 로딩되고
// 이곳에서 부터 사용할 수 있습니다.
}
);

data-main 진입점

app.js는 스크립트 로딩의 진입점이자, 비동기로 모듈을 로딩합니다. 그러므로, index.html에 아래와 같이 스크립트 로딩을 정의할 경우 에러를 발생시킵니다.

1
2
<script data-main="scripts/main" src="scripts/require.js"></script>
<script src="scripts/other.js"></script>

main.js:

1
2
3
4
5
require.config({
paths: {
foo: 'libs/foo-1.1.3'
}
});

other.js

1
2
3
require( ['foo'], function( foo ) {
});

이 코드는 require.config()가 실행되기 전에 호출됩니다. 그러므로, requirejs는 ‘scripts/libs/foo-1.1.3.js’ 대신, ‘scripts/foo.js’를 로딩하려 합니다.

require.config

이런경우 require.config()를 require.js를 HTML페이지의 앞부분에서 로딩할수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<script src="scripts/require.js"></script>
<script>
require.config({
baseUrl: "/another/path",
paths: {
foo: 'libs/foo-1.1.3',
some: 'some/v1.0'
}
});
require( ['foo', 'my/module', 'some/module'], function( foo, myModule, someModule ) {
});
</script>
설정 옵션
  • baseUrl : 모듈을 검색하기 위한 루트경로 입니다. 위 예제의 경우 “my/module”의 script경로는 “/another/path/my/module.js” 가 됩니다.
  • paths :* 모듈 경로를 alias와 같이 제공합니다. 위 예제에서 require()의 인자에 포함된 ‘foo’는 script경로 “/another/path/libs/foo-1.1.3.js”를 가르킵니다. ‘some/module’은 실제 ‘/another/path/some/v1.0/module.js’를 가르킵니다.
  • shim : define()으로 정의되지 않는 기존의 <script> 스타일의 스크립트들에 대한 의존성 주입 설정 입니다.

아래 예제는 requirejs 2.1.0이상과 backbone.js, underscore.js,와 jQuery는 baseUrl디렉토리를 사용하게 될것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
requirejs.config({
shim: {
'backbone': {
deps: ['underscore', 'jquery'],
//module value.
exports: 'Backbone'
},
'underscore': {
exports: '_'
},
'foo': {
deps: ['bar'],
exports: 'Foo',
init: function (bar) {
return this.Foo.noConflict();
}
}
}
});

이후에 다른 js모듈에서 backbone을 다음과 같이 사용할 수 있습니다.

1
2
3
define(['backbone'], function (Backbone) {
return Backbone.Model.extend({});
});

map

프로젝트가 커질 경우 특정 모듈을 개선하면서, 의존하는 모듈의 버전이 변경되었다고 가정할 수 있습니다. 이경우, 기존 버전에 의존하는 모듈과 개선된 모듈이 의존하는 모듈의 버전이 달라질 수 있습니다. 하지만, 아직 이 두 모듈이 프로젝트 내에서 공존해야 할경우 config에서 map설정을 통해 해결할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
requirejs.config({
map: {
'*': {
'foo': 'foo1.2'
},
'some/oldmodule': {
'foo': 'foo1.0'
}
}
});

‘some/oldmodule’을 제외한 모든 모듈에 “foo1.2”가 사용됩니다.

config

모듈에 환경설정등 사용자 정의값이 필요할 경우에 config 속성을 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
requirejs.config({
config: {
'bar': {
size: 'large'
},
'baz': {
color: 'blue'
}
}
});

이경우, bar.js 모듈에서는 module.config().size를, baz.js 모듈에서는 module.config().color를 사용할 수 있습니다.

waitSeconds

스크립트 로딩에 대한 타임아웃 시간입니다. 0은 disable이며, 기본값은 7초 입니다.

deps

requre.js가 로딩되기전 config객체에서 의존성이 필요할 경우, 로딩할 의존성 목록을 배열로 나열합니다.

모듈 정의하기

모듈은 well-scoped한 객체를 선언하고 종속되는 모듈들의 리스트를 명시적으로 기술함으로써 글로벌 네임스페이스가 오염되는것을 방지합니다. 하나의 모듈당 하나의 js파일을 작성해야 합니다. 모듈을은 최적화 도구에 의해 하나의 번들(optimized bundles)로 그룹핑될 수도 있습니다.

단순한 이름/값 형태의 모듈 정의

어떤 종속성도 가지지 않으며 단순한 이름/값 형태의 컬렉션이라면, 객체 상수를 define()함수에 작성하기만 하면 됩니다.

my/shirt.js:

1
2
3
4
define({
color: "black",
size: "unisize"
});

함수 정의

모듈이 setup등의 사전 작업이 필요한 경우, 작업을 마친후 define()함수의 마지막 부분에서 함수를 리턴해 주면 됩니다.

my/shirt.js:

1
2
3
4
5
6
7
8
define(function () {
//Do setup work here
return {
color: "black",
size: "unisize"
}
});

종속성과 함께 함수 정의

모듈이 종속성을 가질경우, define()함수의 첫번째 인자로 종속될 모듈들을 배열형태로 서술합니다. 그리고, 전달한 배열 순서대로 정의한 함수객체의 인자로 받아서 해당 모듈을 사용할 수 있습니다. 이 경우 새로 정의한 모듈은 종속된 모듈을 모두 한번 로딩한 다음 함수객체를 리턴하게 됩니다. my/shirt.js: cart와 inventory라는 모듈에 의존합니다. 의존 모듈을은 모두 shirts.js와 같은 디렉토리에 존재합니다.

1
2
3
4
5
6
7
8
9
10
11
define(["./cart", "./inventory"], function(cart, inventory) {
//return an object to define the "my/shirt" module.
return {
color: "blue",
size: "large",
addToCart: function() {
inventory.decrement(this);
cart.add(this);
}
}
});

함수 모듈 정의

모듈의 함수는 어떤 객체든지 리턴할 수 있습니다. 다음 모듈은 모듈정의에서 함수를 리턴합니다. foo/title.js: 이 모듈은 cart와 inventory모듈에 의존성을 갖습니다. 이 모듈들은 다른 디렉토리(my/)에 존재하므로 의존성 주입시 “my/cart, my/inventory”를 명시해 줍니다.

1
2
3
4
5
6
7
8
9
10
define(["my/cart", "my/inventory"],
function(cart, inventory) {
//return a function to define "foo/title".
//It gets or sets the window title.
return function(title) {
return title ? (window.title = title) :
inventory.storeName + ' ' + cart.name;
}
}
);

CommonJS 모듈을 포함한 모듈 정의

CommonJS 모듈 형태로 작성된 코드는 단순히 CommonJS Wrapper를 사용해 정의할 수 있습니다.

1
2
3
4
5
6
7
8
define(function(require, exports, module) {
var a = require('a'),
b = require('b');
//Return the module value
return function () {};
}
);

이때, define()에 전달되는 함수의 인자(require, exports, module)의 순서는 항상 위와 동일하여야 합니다.

모듈에 이름 부여하기

define() 함수 호출시 첫번째 인자에 모듈명을 부여할 수 있습니다.

1
2
3
4
5
6
7
//명시적으로 "foo/title"이라는 이름을 부여함.
define("foo/title",
["my/cart", "my/inventory"],
function(cart, inventory) {
//Define foo/title object in here.
}
);

이런 모듈명은 통상 “최적화 도구”에 의해 생성됩니다. 명시적으로 모듈명을 부여하는 것은 파일의 이동등에 따른 호환성(portability)을 떨어트리게 할 수 있으므로 가급적 사용을 자제하고, 최적화 도구를 이용해 생성되도록 합니다. 명시적인 모듈명은 스크립트 로딩을 빠르게 합니다.

순환 의존(Circular dependency)

a는 b가 필요하고, b는 a를 필요로 하는 상황을 순환 의존이라고 합니다. 이경우 b모듈의 함수가 호출되면 a는 undefined가 됩니다.

1
2
3
4
5
6
7
8
9
//b.js:
define(["require", "a"],
function(require, a) {
// 여기서 인수로 전달받은 a객체는 null 입니다.
return function(title) {
return require("a").doSomething();
}
}
);

위와 같은 상호 순환하는 의존관계는 드문경우이며, 의도하지 않았다면 이런 경우 설계를 다시 생각해 보는것이 좋을것입니다. 하지만, 간혹 이런 경우가 필요할 수 도 있으므로 오류를 피하기 위해 require()메스드를 이용해 나중에 모듈을 가져올 수 있습니다.

JSONP 서비스 종속 기술하기

JSONP는 외부 서비스를 자바스크립트에서 호출하는 기법입니다. 이것은 script태그를 통해 HTTP GET방식으로 작동합니다. requirejs에서 JSONP를 사용하기 위해서는 아래 예와 같이 URL끝부분에 callback파라미터의 값으로 define을 전달합니다. 그러면, 결과값이 define()의 인자값으로 전돨되어 모듈로 리턴됩니다.

1
2
3
4
5
6
7
8
require(["http://example.com/api/data.json?
callback=define"],
function (data) {
//The data object will be the API response for the
//JSONP data call.
console.log(data);
}
);