다메다메의 기술블로그

RequireJS – JavaScript 파일 및 모듈 로더 본문

JavaScript

RequireJS – JavaScript 파일 및 모듈 로더

다메 2016. 7. 27. 12:38

RequireJS?

RequireJS는 JavaScript 파일과 모듈 로더이다.
RequireJS는 브라우저에 최적화되어 있지만 Rhino나 NodeJS등의 환경에서도 사용할 수 있다. RequireJS같은 모듈 로더를 사용하면 당신의 코드의 성능과 품질이 좋아질 것이다
Java나 Python 같은 잘 정립된 언어에서는 객체의 모듈화를 언어 자체에서 잘 지원하지만 JavaScript는 그렇지 않다. 

또한, 프로그램이 커질수록 스크립트가 중복되어 발생하는 경우가 발생할 수 있고 그런 경우에 코드를 common.js, event-binder.js 등의 코드로 나누어 관리하는경우가 많아지는데, 이 경우에도 분리한 코드를 잘 로딩하지 않으면 코드간 의존성이 망가지며 프로그램 자체의 구성이 엉성해지는 경우도 많다. 

RequireJS는 자바스크립트 파일을 동적으로 로딩할 수 있고, AMD 모듈화를 적용한 코드라면 모듈로서도 사용할 수 있으며 (그렇지 않아도 사용할 수 있는 방법이 있다 아래에서 더 살펴보자) 자바스크립트 코드간의 의존성도 줄 수 있다. 

또, 앞서 말했듯이 어떠한 자바스크립트 코드가 실행되려면 다른 스크립트가 먼저 로딩되어야 한다거나 하는 경우가 있는데, 자칫 스크립트 로딩의 순서가 꼬일 경우 에러를 뱉어내며 동작하지 않을 수 있다. 

RequireJS를 사용하면 코드간 의존성을 줌으로서 아예 그러한 경우를 막을 수 있고, 좀 더 체계적인 소스코드를 만들어낼 수 있다. 장점만을 죽 늘어놓았는데 자세한건 좀 더 살펴보자.  

JavaScript에게 모듈이란

먼저 모듈에 대해 간단히 짚고 넘어가보자. 

모듈의 개념은 Divide and Conquer 로 설명되는 각 기능(알고리즘)의 분할과 그 분할의 결합으로 생각해볼 수 있다. 

보통의 성숙된 언어에서는 이러한 모듈화를 언어 차원에서 지원하고 있는데, 예를 들어  java의 경우에는 모듈이 instance로 생성되어지며 모듈끼리의 구분은 package로 구분된다. 

그리고 모듈의 구현은 접근 제어자(private, public 등)의 사용으로 캡슐화를 보장하며, 필요한 것만 공개해서 그 모듈을 사용하려는 사용자가 쓸데없이 많은 지식을 가질 필요 없이 부담없이 사용할 수 있다.

JavaScript는 이러한 모듈화나 접근 제어를 언어레벨에서 명시적인 지원을 하지 않으며 package 등의 구분도 없기에... 파일간의 변수 충돌이나 전역공간 변수 난립(global scope pollution)등의 문제가 발생할 요지가 많다. 

보통 이러한 경우를 고려한 JavaScript의 일반적인 모듈 구현은 다음과 같다
(function(exports) {
  "use strict";

  // private 변수
  var name = "I'am a module";

  // 외부에 공개할 public 객체
  var pub = {};

  pub.getName() {
    return name;
  }

  // 외부 공개
  exports.aModule = pub;

})(exports || global);

(exports || global).aModule.getName(); // I'm a module
이러한 구현은 변수를 private 화 할 수 있으며 그로 인한 캡슐화로 모듈 사용이 쉬운 장점이 있지만, 여러개의 모듈을 선언하면서 exports 객체에 프로퍼티가 겹칠 경우 앞서 선언된 공개 속성은 덮어써지는 문제가 있고,  모듈간 의존성이 있을때 의존성을 정의하기가 매우 어렵다. 

그리고 익명 함수와 exports 객체를 사용하는 애매한 코드로 인해 눈에 잘 들어오지 않는다. 이러한 경우를 해결하기 위해 여러 Module Loader가 공개되어 있는데,  그 중 하나가 RequireJS이다. RequireJS에서는 모듈의 고유성과 의존성을 잘 지원하고 있다. 

RequireJS는 AMD 모듈 로딩 표준을 따르기에 기본적으로 모든 모듈이 비동기적이다.

모든 모듈은 비동기적으로 명시적으로 요청하거나 의존성이 있을 때 로딩(Lazy Loading) 된다. 필요한 자바스크립트 파일을 어플리케이션 시작 전 전부 로딩하지 않고, 실제 필요한 때 (사용자의 입력이나 메소드 호출 등의 특별한 경우) 에 로딩하게 할 수 있어서 전체적인 페이지의 속도 향상에도 도움이 된다.  

 

설치법

설치법은 간단하다. 아래와 같이 그냥 스크립트 태그를 쓰면 끝이다.
<script type="text/javascript" data-main="main" src="/js/lib/requirejs.js"></script>
위 태그를 주의깊게 보면 data-main 이라는 속성에 main 이라는 값이 할당되어 있는데, 이 속성은 옵션으로 이 속성을 주게 되면 requirejs가 전부 로딩되면 저 경로의 속성 이름에 해당하는 파일을 자동으로 로딩한 뒤 실행한다. 

경로는 절대 경로가 아니면 requirejs 기준의 상대 경로를 따른다. 모든 모듈의 경로 또한 requirejs가 로딩되는 경로에 상대적이나, 나중에 설정을 통해 바꿀 수 있다. 
  

모듈 이름과 File Scope

RequireJS에서 모든 모듈은 이름이 주어지며, 모듈 이름은 보통 파일 경로가 된다. 

파일 경로는 사실 FileSystem에서 유일하게 존재할 수 있으므로 같은 이름을 가진 모듈이면 모듈끼리 덮어써지거나 충돌할 일이 없어진다. 

예를 들어보자. 

  • 모듈 파일이 /js/controller/main.js 에 존재한다면 모듈 이름은 "/js/controller/main" 이 된다.
  • 모듈 파일이 /js/controller/list.js 로 새로운 모듈을 생성하여 저장하면 역시 모듈 이름은 "/js/controller/list" 가 된다. 
(JavaScript 파일이고, 상대 경로일 경우 .js가 생략된다. )

이제 실제 모듈을 정의하고 사용하는 방법을 알아보자.  

 

RequireJS 모듈 정의 및 사용

이러한 문법을 사용한다.
// 일반적인 RequireJS 모듈 정의
define(function() {

  "use strict";

  var exports = {
    version: "1.0"
  };

  var private1 = "private1";
  var private2 = "private2";

  exports.doSomething = function() { /* ... do something ... */ };
  exports.stopSomething = function() { /* ... stop something ... */ };

  return exports;
});
모듈의 정의에는 define 이라는 글로벌 함수를 사용하며 인자로 함수 하나를 받는데 그 함수에서 반환하는 객체가 모듈이 된다. 

인자 함수는 일종의 객체 팩토리인 셈이다. 

JavaScript는 함수 자체가 스코프를 생성하므로 이 안에서 필요한 만큼의 private 를 선언하고, 외부 공개가 필요한 객체나 함수는 return 으로 반환하면 된다. 다수를 공개하고 싶다면 객체 형식으로 묶어 반환하면 된다. 정리해보면 다음과 같은 코드이다.
// 의존성이 없는 모듈의 정의
define([팩토리 함수]);
그런데 잊은게 있다. 

분명 앞에서 RequireJS는 모듈간의 의존성을 정의하는 방법을 제공한다고 했다. 이대로는 아까 순수하게 JavaScript로 만든 모듈 코드와 별반 다를게 없어 보인다. 

물론 의존성을 줄 수 있다. 

팩토리 함수 앞 인자로 생성될 모듈이 "의존하고 있는 모듈 이름을 문자열로 담은 배열" 을 주면 된다.
// 의존성이 있는 모듈 정의
define([의존 모듈 이름 배열], [팩토리 함수]);
한번 실제 모듈 코드를 만들어 보자. 일단 파일 구조는 다음과 같다고 가정한다.
  • /main.js
  • /App.js
  • /sub/Logger.js
  • /sub/MainController.js
먼저 만들 것은 간단한 로그 모듈이다. 이 코드는 Logger.js 이다
// @file Logger.js
define(function() {

  "use strict";

  var console = global.console || window.console;
  var exports = {
    version: "0.1.0",
    author: "javarouka"
  };

  exports.log = function() {
    var args = Array.prototype.unshift.call(arguments, new Date().toLocaleString());
    console.log.apply(console, arguments);
  };

  return exports;

})
로그 모듈로 일반적인 로그에 앞에 로그가 찍히는 현재 날자를 추가해준다. 

팩토리 함수 안에 var로 선언된 것들은 private 으로 내부에서만 사용하며, exports 객체의 속성으로 지정된 log 함수만 공개되어 외부에서는 log를 통해 이 모듈을 사용할 수 있다. 

이제 이 로그 모듈에 의존성이 있는 모듈을 만들어보자.
// @file MainController.js
// @dependency Logger.js
define(["sub/Logger"], function(logger) {

  "use strict";

  var exports = {
    type: "controller",
    name: "Main"
  };

  var bindEvent = function() {
    logger.log("bind event...")
    // do something
  };

  var view = function() {
    logger.log("render ui");
    // do something
  };

  exports.execute = function(routeParameters) {
    logger.log(exports.name + " controller execute...");
    // do something
  }

  return exports;

});
일단 팩토리 함수 앞에 인자로 의존성 모듈의 이름 배열을 주는데 상대 경로일 경우 .js 확장자를 생략해야 한다. "sub/Logger" 라고 주면 된다. 

주게 되면 팩토리 함수의 인자로 전달되는데 배열에 지정한 순서대로 전달된다. 

이 모듈에서는 logger 라는 변수로 사용하고 있다. 마지막으로 App.js.
// @file ApplicationContext.js
// @dependency sub/MainController.js
define(["sub/MainController"], function(main) {

  "use strict";

  // do something...

});
App.js 는 MainController.js에 의존성이 있다. App.js 가 로딩되려면 의존성에 따라
  1. Logger.js
  2. MainController.js
  3. App.js
순서대로 로딩되게 될 것이다. 

이제 실제 코드를 사용할 main.js 를 작성하자
// @file main.js
// @dependency App.js, sub/Logger.js
require([ "App", "sub/Logger" ], function(app, logger) {

  "use strict";

  app.start();
  logger.log("Application start");

}, function(err) {
  // ERROR handling
});
모듈 정의가 아닌 단순히 코드를 실행할 때는 require 함수를 사용한다. 

require 는 define과 비슷하게 첫번째 인자로 의존성, 두번째 인자로 실행 코드 함수, 세번째 인자는 옵션으로 에러 핸들러이다. 

실행 코드 함수에서 코드상에서 잡을 수 있는 오류가 나거나,  로딩에 실패할 경우 실행된다.  

여기서 짚고 넘어갈 것이 있다. Logger.js 는 두 부분에서 의존성이 있다. 

이 경우에는 먼저 로딩되는 모듈이나 코드에서 한번 로딩되면, 그 다음에는 모듈을 다시 로딩하지 않고 로딩된 모듈을 다시 사용한다. 

Java Spring의 싱글톤 레지스트리와 살짝 비슷하다. 

정리하면 다음과 같다.
  • define: 모듈을 정의할 때
  • require: 정의된 모듈에 의존성을 주는 코드를 작성할 때
이제 대략적인 사용법은 다 정리된 것 같다. 다음은 설정법을 살펴보자 
  

환경 설정

RequireJS는 몇가지 설정을 통해 사용자의 환경에 더욱 잘 조정해 맞출 수 있다. 

문법은 다음과 같다.
// 이 코드를 RequireJS가 로딩된 뒤 기타 모듈을 로딩하기 전에 둔다.
require.config({
  baseUrl: "/js/some/path", // 모듈을 로딩할 기본 패스를 지정한다.

  // 모듈의 기본 패스를 지정한다
  // 모듈의 이름과 실제 경로를 매핑할 수 있어 별칭(alias) 기능도 할 수 있다
  paths: {
    "javarouka": "modules/javarouka", // 이 모듈은 /js/some/path/module/javarouka.js 경로.

    // 모듈 패스를 배열로 주게 되면 먼저 앞의 URL로 로딩해보고 안되면 다음 경로에서 로딩한다.
    // CDN 등을 사용할 때 좋다.
    "YIHanghee": [
      "https://cdn.example.com/YIHanghee",
      "modules/YIHanghee"
     ]
  },
  waitSeconds: 15 // 모듈의 로딩 시간을 지정한다. 이 시간을 초과하면 Timeout Error 가 throw 된다
});
이러한 설정 파일은 다음과 같이 별도의 분리된 파일로 나눌 수 있다
<script type="text/javascript">
var require = {
  // 설정 내용
};
</script>
<script type="text/javascript" src="/js/requirejs.js"></script>
다음에는 RequireJS 형식으로 작성되지 않은 다른 코드를 RequireJS에 포함시켜 사용하며 코드간 의존성을 주는 방법을 알아보겠다.

 

구 소스코드(global-traditional)와 같이 사용하는 법 - Shim

그런데 ReuireJS를 실무에 바로 적용하려면 어려움이 따른다. 바로 하위 호환성 문제이다.

기존의 구(global - traditional) 코드 대부분은 아마도 define이나 require를 사용하지 않은 소스코드가 많다. 그리고 그것들도 나름의 의존성을 가지고 있을 수 있다. 

이것들을 RequireJS에서도 온전히 사용하려면 그냥은 안된다. 이것을 지원하기 위해 RequireJS에는 shim 이라는 기능이 있다. 설정 파일에 shim 속성으로 미리 구 코드의 의존성을 정의할 수 있다. 

문법은 다음과 같다.
requirejs.config({
  paths: {
    // jquery 로딩 시 필요한 경로를 지정한다.
    'jquery': [ 
      '/js/jquery', 
      '//ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min' 
    ],
    // underscore 도 마찬가지.
    'underscore': [ 
      '/js/underscore.min', 
      '//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.2/underscore-min' 
    ],
    // backbone
    'backbone': [
      'js/backbone.min',
      '//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone-min'
    ]
  },
  shim: {
    // underscore에 대한 shim 속성을 정의한다.
    'underscore': {
      // 반환될 객체는 exports속성으로 정의한다.
      // underscore는 아래와 같은 _ 이름으로 공개되는 것이 사용이 직관적이다.
      exports: function() { return _; }
    },
    // shim 속성의 키로 모듈 이름을 지정하고 내부 속성을 나열한다.
    // backbone은 underscore와 jquery 에 의존성이 있다.
    'backbone': {
      // 백본이 의존하는 underscore와 jquery를 deps 속성에 정의
      // 이름은 위에 이미 지정한 별칭(키 이름)으로 해도 된다.
      deps: ['underscore', 'jquery'],
      // 반환될 객체를 정의한다.
      // 문자열로 줄 경우,
      // 글로벌 객체에서 저 속성을 키로 하는 객체가 모듈이 된다.
      // 함수를 줄 경우,
      // 함수의 실행 결과로 리턴되는 객체가 모듈이 된다.
      exports: 'Backbone'
    }
  }
});
먼저 모듈이 로딩될 경로를 paths 속성의 키로 정의한 뒤 shim 속성에서 정의한 코드에 대한 의존성을 정의한다. 


"backbone" 키로 지정된 deps 속성에는 앞에서 했던 define 처럼 배열 형태로 의존성을 정의하고, exports 속성으로 팩토리 함수를 정의했다. 

이렇게 설정해두면 "backbone" 이라는 모듈 이름으로 RequireJS 모듈처럼 사용을 할 수 있다.
exports 속성에 문자열을 주면 그 문자열에 해당하는 전역의 속성이 define에서 팩토리 함수에서 리턴하는 객체가 되며, 함수를 줄 경우 반환되는 객체를 지정해줄 수 있다. 

exports에 함수를 지정하는 경우는 팩토리 함수와 동일하게 이 shim 모듈이 반환하는 모듈을 조정할 때 유용하다. 

가령 prototype.js와 jQuery를 같이  사용할 경우에는 $ 변수 충돌이 일어나므로 반드시 jQuery에서 prototype.js를 로딩하기 전에 jQuery.noConflict 를 호출해야 한다. 

이럴 경우 RequireJS에서는
"jquery": {
  exports: function() {
    var jQuery = jQuery.noConflict();
    return jQuery;
  }
},
"prototype": {
  deps: [ "jquery" ],
  exports: "$"
}
과 같은 방식을 적용할 수 있겠다. 두 모듈이 안전하게 로딩될 것이다.

 

마치며

JavaScript 는 사용하기 편한만큼 편함에 너무 의존하다보면 돌이킬 수 없는 스파게티코드나 중복 코드 발생이 많아질 수 있다. 

RequireJS는 이러한 경우의 한 대안이 될 수 있으며, 모듈 프로그래밍을 통해 좀 더 체계적인 프로그래밍을 가능하게 해 주며, 브라우저 지원도 IE6 이상부터 지원하는 괜찮은 호환율을 보여준다. 

지금 자신의 프로젝트를 보라. 

혹시 스파게티, 반복 코드가 보인다면, 바로 RequireJS를 한번 고려해볼 때다.


출처


http://blog.javarouka.me/2013/04/requirejs-javascript.html

'JavaScript' 카테고리의 다른 글

javascript library(라이브러리) 모음  (0) 2016.05.18
배열안의 데이터 삭제에 대해  (0) 2015.04.03
Comments