관련 지식
javascript, AMD, CommonJS, UMD, module

UMD 모듈을 만들기 위해서는 AMD 와 CommonJS 에 대해 대략적으로 이해할 필요가 있습니다. 네이버 D2에 매우 잘 정리되어 있으므로 제가 매우 간단하게 설명드리고 네이버 참고 자료를 읽어보시면 될것 같스니다. 소스 예제도 일부 있으나 소스보다는 개념 위주로 보시는게 좋습니다.

모듈의 필요성

아래 소스는 자바에선 불가능하지만 자바스크립트에서 동작하는 소스입니다.

정상적으로 동작하는 소스)

  1. function myFunc() {
  2. //var key = 1;
  3. //var key = "a";
  4. var key = 23;
  5. switch(key) {
  6. case 1:
  7. return 100;
  8. case "a":
  9. return "alpha";
  10. default:
  11. return;
  12. }
  13. }
  14. console.log(myFunc());

자바는 문법적으로 엄격한 규칙이 있어 자연스럽게 모듈화가 이루어졌지만 자바스크립트는 좋은 표현을 하면 언어의 자유도가 굉장히 높아 일정한 포맷을 가진 모듈화가 어려웠습니다.

옛날 방식)

  1. <script type="text/javascript" src="https://www.abc.com/a.js" charset="UTF-8"></script>
  2. <script type="text/javascript" src="https://www.abc.com/b.js" charset="UTF-8"></script>
  3. <script type="text/javascript" src="https://www.abc.com/c.js" charset="UTF-8"></script>

웹페이지를 만들때 위와 같은 형태를 지금도 많이 사용할 것입니다. 그런데 브라우저에서는 파일 단위의 스코프가 없기 때문에 js 파일이 분리가 되어있어도 사용하는 스코프는 동일합니다. 때문에 서로 다른 파일에서 사용하는 함수나 변수의 이름이 중복되어 서로에게 영향을 주는 경우가 있었습니다. 그리고 각기 다른 스타일로 만들어진 JS는 의존성 체크도 적용하기 힘들었습니다.

AMD 와 CommonJS

따라서 모듈화를 위해 정책을 만들고자 하는 단체가 생기기 시작했고 그중 대표적인 두가지가 CommonJS와 AMD 입니다.

네이버 참고자료

네이버 참고자료의 소스 샘플에서 exports 와 define 에 주목하면 됩니다. 그 두가지는 자바스크립트의 문법에 존재하는 키워드가 아니라 각각 CommonJS와 AMD에서 모듈로딩을 하기 위해 정해둔 약속 입니다. Node.js를 개발 해본적이 있다면 exports 방식이 익숙하게 느껴질 것입니다. Node.js는 CommonJS를 지원하는 대표적인 프로젝트이기 때문입니다.

CommonJS와 AMD 중 어떤것이 더 좋다 나쁘다를 구분할수 없지만 CommonJS는 서버쪽에서, AMD는 브라우저쪽에서 좀 더 유리하게 동작한다고 합니다. 특히 AMD 방식으로는 해당 모듈을 필요한 시점에 로딩하는 lazy-load 기법을 사용할수 있어 모바일 웹페이지에서 특히 유용하게 이용할 수 있는데 이를 잘 표현한 대표적인 프레임워크로는 RequireJS가 있습니다.

UMD 란?

모듈 스펙이 하나로 통일되지 않다보니 모듈 개발자는 여러 스펙을 지원할수 있도록 만들어야 했습니다. 예를 들어 날짜 처리를 매우 쉽게 해주는 moment.js가 있습니다. 어떠한것에도 의존성이 없는 모멘트는 브라우저에서도 쓰고 싶고 Node.js 에서도 쓰고 싶을 것입니다. 브라우저에서도 단순히 스크립트 호출을 하고 싶을 수도 있고 AMD 스펙으로 적용하고 싶을수도 있습니다. 그러한 다양한 모듈 스펙을 지원하도록 만든것을 UMD(Universal Module Definition) 라고 합니다.

아래는 moment.js의 소스 일부 입니다. 이와 같은 형태가 UMD를 적용한 것입니다.

  1. ;(function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  3. typeof define === 'function' && define.amd ? define(factory) :
  4. global.moment = factory()
  5. }(this, (function () { 'use strict';
  6. var hookCallback;
  7. ... 생략 ...
  8. })));

UMD 적용해보기

이전에 다뤘던 ‘디폴트 값을 이용한 코딩’ 의 소스를 이용하여 UMD를 적용해보도록 하겠습니다.

먼저 실행함수를 하나 만듭니다.

  1. (function() {
  2. })();

다음으로 모듈화를 시킬 소스를 체크해봅니다.

  1. var defaultOption = {
  2. group1 : {
  3. key1 : 'aaa',
  4. key2 : 'bbb'
  5. },
  6. group2 : {
  7. key3 : 82,
  8. key4 : function defaultFn() {
  9. },
  10. key5 : 'ccc'
  11. },
  12. group3 : {
  13. group4 : {
  14. key6 : 100,
  15. key7 : 'ddd'
  16. }
  17. }
  18. };
  19. function combine(def, usr) {
  20. function isEnd(v, k) {
  21. return ((v instanceof Function) || !(v instanceof Object));
  22. }
  23. function _(dst, def, usr) {
  24. for(var prop in def) {
  25. if(prop == 0) {
  26. return;
  27. }
  28. if(isEnd(def[prop], prop)) {
  29. dst[prop] = usr[prop] || def[prop];
  30. }
  31. else {
  32. dst[prop] = {};
  33. _(dst[prop], def[prop], usr[prop]);
  34. }
  35. }
  36. }
  37. var dst = {};
  38. _(dst, def, usr);
  39. return dst;
  40. }

위의 소스에서 변수 defaultOption과 함수 combine()은 글로벌 스코프를 사용하고 있습니다. 모듈화를 위해 함수로 한번 감싸서 독립된 스코프를 가지게 합니다.

  1. function() { //익명함수로 묶기
  2. var defaultOption = {
  3. ... 생략 ...
  4. };
  5. function combine(def, usr) {
  6. ... 생략 ...
  7. }
  8. }

위 소스를 처음 만들었던 익명 함수의 파라미터로 넣어줍니다.

  1. (function() {
  2. })(function() { //함수화 한 소스를 그대로 파라미터로 전달
  3. var defaultOption = {
  4. ... 생략 ...
  5. };
  6. function combine(def, usr) {
  7. ... 생략 ...
  8. }
  9. });

파라미터로 this 도 넘겨줍니다. 그리고 넘긴 파라미터를 각각 global과 factory로 받겠습니다.

  1. (function(global, factory) { //파라미터를 global, factory로 받음
  2. })(this, function() { //this도 파라미터로 전달
  3. var defaultOption = {
  4. ... 생략 ...
  5. };
  6. function combine(def, usr) {
  7. ... 생략 ...
  8. }
  9. });

브라우저 환경일 경우 window 객체가, Node.js 환경일 경우 global 객체가 this 를 통해 변수 global 이 될 것입니다. 변수 factory 는 모듈화를 시키고 싶었던 함수가 들어갑니다. 이제 이 두 가지를 가지고 모듈 로더 스펙에 맞게 코딩할 것입니다.

  1. (function (global, factory) {
  2. //moment.js에서 사용하던 방식을 적용
  3. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  4. typeof define === 'function' && define.amd ? define(factory) :
  5. global.abc = factory()
  6. }(this, function () {
  7. var defaultOption = {
  8. ... 생략 ...
  9. };
  10. function combine(def, usr) {
  11. ... 생략 ...
  12. }
  13. });

첫번째 조건부터 보겠습니다.

  1. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory()

CommonJS 스펙이 적용된 로더는 상위 스코프에서 exports와 module 객체를 만들기 때문에 그 두 가지가 있다면 CommonJS 를 지원하는 환경이라고 생각하는 것입니다. CommonJS 환경에서는 module.exports = factory(); 와 같이 호출합니다. factory 함수를 실행한 결과값을 넣는것에 주목해야 합니다.

  1. typeof define === 'function' && define.amd ? define(factory)

AMD 스펙이 적용된 환경에서는 상위 스코프에 define 객체가 존재합니다. 따라서 위 조건이 참일 경우 AMD 환경이라고 생각하고 define(factory); 와 같이 호출하면 됩니다. factory를 직접 실행하지 않고 파라미터로 넘기는것에 주목해야 합니다.

  1. global.abc = factory()

위 두가지 조건이 아니라면 다른 스펙의 환경일 것입니다. <script src="..."></script> 형태로 호출 한 경우도 포함됩니다. 그럴땐 전역 객체에 직접 넣어줘야 합니다. 그런데 위의 예제에선 global.abc = factory() 로 작성 되어있습니다. AMDCommonJS 방식에서는 호출하는 쪽에서 사용할 이름을 정하게 되지만 이 경우엔 모듈쪽에서 이름을 정하게 됩니다.(예제에서는 abc로 정함) 따라서 모듈을 만들때 이 이름이 다른 모듈과 겹치지 않도록 주의 하셔야 합니다.

위 규칙을 간단하게 정리하면 아래와 같습니다.

  • exports 와 module 객체가 있으면 CommonJS
    module.exports = factory()
  • define 객체가 있으면 AMD
    define(factory)
  • 그 외
    global.사용할모듈이름 = factory()

여기까지 하면 UMD 모듈로 만들어진 것입니다.

정리

가장 간단하게 UMD 모듈을 적용하는 방법을 말씀드렸습니다. Webpack 이나 Parcer 같은 번들러를 사용하면 빌드 시 UMD를 지원하는 결과물로 만들어져 별로 신경 쓸 것이 없지만 번들러를 사용하지 않는 환경에서는 직접 UMD 모듈을 만들어야 하므로 만드는 방법을 알아두면 좋습니다.
간단해 보이지만 의존성이 있을경우 생각보다 안풀리는 부분이 있으니 직접 간단한 기능을 만들어 시험 해보는것이 좋을것 같습니다.