이 글은 Modules, a Future Approach to JavaScript Libraries – Tuts+ Code Article의 글을 한국어로 옮긴 것입니다. (참고로, 지금까지 이곳에 올린 글 중에 가장 긴 게 아닌가 생각되네요.)

jQuery와 같은 JavaScript 라이브러리는 거의 지난 10여 년 동안 웹 브라우저에서 실행될 JavaScript를 작정할 때마다 당연히 가장 먼저 추가해야만 되는 것으로 여겨지고 있습니다. 이러한 현상은 각종 브라우저 간 차이와 구현 문제로 어쩔 수 없었으며, 그래서 지금까지 그 임무를 훌륭히 수행하고 있습니다. jQuery는 이러한 브라우저 버그와 각자 기괴하게 꼬인 것들을 알아서 말끔하게 덮어주면서 가령 이벤트 처리나 Ajax 그리고 DOM 조작과 같은 작업을 손쉽게 만들어 주었습니다.

이땐, jQuery가 거의 모든 문제를 해결해 주었기에, 자연스레 추가해서 얻은 막강한 힘을 빌려 그냥 작업에만 바로 집중할 수 있었지요. 이것은 마치 브라우저가 제대로 작동하려면 있어야 하는 블랙박스와 같았습니다.

하지만 지금, 웹은 진보하였고, API도 많이 개선되었으며 마찬가지로 웹 표준도 그만큼 더 많이 구현되기 시작하면서 아주 빠르게 변화하는 환경이 되었습니다. 그래서 과연 미래의 브라우저에도 이런 거대한 라이브러리들이 필요로 할지 의문을 가질 수밖에 없게 되었습니다. 결국 모듈(Module)에 적합한 환경으로 변화하고 있는 것입니다.

모듈 소개

모듈은 어떤 하나의 특화된 기능을 아주 잘 수행하는 어떤 독립된 집합체라 할 수 있습니다. 예를 들어, 모듈은 어떤 특정 엘르먼트에 클래스를 추가하는 기능을 수행하거나, 혹은 Ajax를 이용한 HTTP 통신을 하는 등 그 가능성은 무한합니다.

모듈은 그 모양과 크기가 여러 가지 있는데, 일반적인 목적은 하나같이 작업 환경에 불러오기만 하면 알아서 원래 가지고 있는 기능을 잘 수행하는 데 있습니다. 보통 모듈은 기본적으로 각각의 개발자 문서와 설치 과정 그리고 브라우저나 서버와 같은 실행 환경의 정보를 제공하고 있습니다.

이러한 모듈은 프로젝트에 필요한 기능을 메꿔주면서 나중에 추가와 제거와 같은 의존성 관리가 훨씬 간편해지는 효과가 있습니다. 모듈이 제공하는 장점과 유연성을 생각해보면, 이젠 어떤 덩치 큰 라이브러리를 단순하게 추가하는 일은 점점 줄어들 것입니다. 다행스럽게도 jQuery와 같은 라이브러리들도 이점을 인식해서 필요한 기능만 내려받을 수 있는 온라인 도구를 제공하기 시작했습니다.

최근에 소개된 API들은 모듈로 작성될 수도 있는 영감을 북돋게 하는 커다란 촉진제 역할을 하였고, 이젠 브라우저의 구현 상황도 상당히 좋아져서, 대부분의 일상적 작업은 작은 기능의 모듈을 작성해서 대신 처리해 줄 수도 있게 되었습니다.

모듈의 시대는 이미 도래했으며, 앞으로도 오랫동안 그 소임을 계속 맡아주리라 기대되고 있습니다.

첫 번째 모듈의 영감

최근 공개된 API 중 처음 소개되고 나서 모듈로 삼을 만한 것으로 항상 관심이 있었던 것이 바로 classList API입니다. jQuery와 같은 라이브러리에서 영감을 얻어서, 이젠 다른 라이브러리 없이도 기본적으로 브라우저에서 엘르먼트에 CSS 클래스를 더하는 길이 생긴 겁니다.

classList API가 소개된 지는 벌써 몇 년이 지났지만, 많은 개발자가 그 존재를 아직 모르고 있습니다. ​그래서 이 classList API을 활용해서, 이를 지원하지 않는 브라우저에선 이 기능을 대신하는 모듈을 한 번 만들어보기로 하였습니다.

코드를 바로 설명하기 전에, 우선 jQuery가 엘르먼트에 클래스를 어떻게 더해주는지 살펴보겠습니다.:

$('elem').addClass('myclass');

이런 조작 기법이 브라우저가 지원하는 기본 기능에 추가된 것이 바로 앞서 언급된 classList API인데, classList API는 엘르먼트의 className에 저장된 값에 해당하는 공백으로 분류된 DOMTokenList Object를 조작하는 몇 가지 방법을 제공하고 있으며, 이는 jQuery의 처리 방식과 많이 닮았습니다. 아래는 classList API를 이용해서 클래스를 추가해주는 예제입니다.:

$('elem').addClass('myclass');

여기서 우리가 얻을 수 있는 교훈은 무엇일까요? 특정 라이브러리의 기능이 기본 플랫폼 언어로 자리 잡는 것은 대단한 일이고, 그렇지 않더라도 어떤 영감을 줄 수도 있을 것입니다. 이것이야말로 열린 웹 플랫폼의 장점이고, 앞으로 나아가는 데 누구나 조력자가 될 수도 있다는 사실입니다.

그럼 본론으로 들어가 볼까요? 이제 모듈에 대한 이해를 마쳤고, 또 classList API도 마음에 들지만, 안타깝게도 아직 모든 브라우저가 이것을 지원하지는 않습니다. 그렇지만 적어도 비슷한 대체 기능을 작성할 수는 있겠지요. 브라우저가 지원하면 classList를 쓰고, 그렇지 않다면 자동으로 대체 기능을 구현해주는 모듈을 작성하는 건 좋은 아이디어라고 생각됩니다.

첫번째 모듈 작성: Apollo.js

대략 약 여섯 달 전에, 일반 JavaScript로 엘르먼트에 클래스를 더해주는 아주 간단하고 독립적인 모듈을 작성했었는데, 나중엔 이름을 apollo.js라고 붙여 주었습니다.

모듈을 작성하기로 마음먹은 이유는 훌륭한 classList API를 쓰기 시작하면서, 이런 간단하고 일반적인 작업에 어떤 라이브러리를 끌어들이진 말자고 하는 것이 주된 이유였습니다. jQuery는 전에도 그랬고 지금도 아직 classList API를 사용하고 있지 않아서, 새로운 기술을 실험해 볼 수 있는 아주 좋을 기회라고 생각했습니다.

어떻게 만들었는지의 설명과 함께 결과적으로 간단한 모듈이 탄생하기까지의 각 과정을 공개합니다.

classList 사용

이미 보셨듯이, classList는 매우 멋진 API라서 “jQuery에 익숙한 개발자”라도 적응하기가 아주 쉽습니다. 다만, 한 가지 마음에 안 드는 것은 API가 제공하는 매소드(method)를 쓸려면 항상 classList Object를 우선 지칭해서 써야 한다는 점입니다. 그래서 저는 apollo를 작성할 때 이런 불필요하게 반복된 작업을 없애려 했고, 다음과 같이 API를 디자인하기로 하였습니다.:

apollo.addClass(elem, 'myclass');

물론 좋은 클래스 조작 모듈이라면 hasClass, addClass, removeClass 그리고 toggleClass 매소드를 가지고 있어야겠지요. 이 모든 메소드는 “apollo” 네임스페이스에서부터 시작되어 사용됩니다.

위에 있는 “addClass” 메소드를 자세히 살펴보면, 첫 번째 argument로 엘르먼트를 건네주는 것을 확인하실 수 있습니다. 하나의 커다란 커스텀 Object에 묶여서 사용되는 jQuery와는 다르게, 이 모듈은 하나의 DOM 엘르먼트를 받게 되는데, 이 엘르먼트를 기본 메소드를 쓰든 혹은 선택자 모듈을 사용해서 건네주든 그건 개발자 마음입니다. 두 번째 argument는 String 값으로 개발자가 원하는 클래스 이름을 던져주면 됩니다.

아래는 제가 만들고자 했던 모든 클래스 조작 매소드들로, 다음과 같은 사용 형태를 띠게 됩니다.:

apollo.hasClass(elem, 'myclass');
apollo.addClass(elem, 'myclass');
apollo.removeClass(elem, 'myclass');
apollo.toggleClass(elem, 'myclass');

자, 그럼 어디서부터 시작해야 할까요? 먼저, 모든 메소드를 갖다 붙일 Object가 필요하고, 또 모든 내부적 작동과 변수/매소드들을 포함할 function closure를 구현해야 할 겁니다. immediate-invoked function expression(IIFE)을 써서, apollo라는 이름의 Object를 감싸고 classList 기능을 구현하는 몇 개의 매소드들을 포함해서 모듈을 정의해 보았습니다.

(function() {
  var apollo = {};
 
  apollo.hasClass = function(elemclassName) {
    return elem.classList.contains(className);
  };
 
  apollo.addClass = function(elemclassName) {
    elem.classList.add(className);
  };
 
  apollo.removeClass = function(elemclassName) {
    elem.classList.remove(className);
  };
 
  apollo.toggleClass = function(elemclassName) {
    elem.classList.toggle(className);
  };
 
  window.apollo = apollo;
 
})();
 
apollo.addClass(document.body, 'test');

이제 classList 기능 구현이 완성되었으니, 오래된 버전의 브라우저 지원을 고려해 볼 차례죠. 간단히 말해, apollo 모듈의 목적은 브라우저에 상관 없이 클래스 조작에 대한 작지만 독립적이고 일관된 API를 가지고 기능을 구현하는 것입니다. 여기엔 아주 간단한 기능 탐지 기법이 유용하게 쓰일 수 있습니다.

classList 기능의 존재 여부를 판단하는 쉬운 방법은 다음과 같은 것이 있습니다.:

if ('classList' in document.documentElement) {
  // 기능을 지원함 
}

in 연산자를 써서 classList 기능의 존재 여부를 Boolean 값으로 확인할 수 있습니다. 다음 단계는 조건부로 우선 classList를 지원하는 브라우저에게만 API 제공하는 것이지요.:

(function() {
 
  var apollo = {};
  var hasClass, addClass, removeClass, toggleClass;
 
  if ('classList' in document.documentElement) {
    hasClass = function() {
      return elem.classList.contains(className);
    };
    addClass = function(elemclassName) {
      elem.classList.add(className);
    };
    removeClass = function(elemclassName) {
      elem.classList.remove(className);
    };
    toggleClass = function(elemclassName) {
      elem.classList.toggle(className);
    };
  }
 
  apollo.hasClass = hasClass;
  apollo.addClass = addClass;
  apollo.removeClass = removeClass;
  apollo.toggleClass = toggleClass;
 
  window.apollo = apollo;
 
})();

오래된 브라우저를 지원하는 방법은 여러 가지가 있는데, className String을 읽고 나서 모든 이름을 루프로 돌려가며 대체하거나 추가하는 등의 과정을 예로 들 수 있습니다. jQuery가 이런 코드 형태의 복잡한 과정을 거치는데, 전 이런 복잡한 과정을 적용해서 저의 새롭고 단순한 모듈을 불필요하게 부풀리고 싶지 않았습니다. 그래서 그냥 정규식을 써서 더 추가적인 비용 없이 비슷한 작업을 구현하였습니다.

아래는 이렇게 해서 제가 도출해 낸 가장 깔끔한 구현 방법입니다.:

function hasClass(elemclassName) {
  return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
}
 
function addClass(elemclassName) {
  if (!hasClass(elem, className)) {
    elem.className += (elem.className ? ' ' : '') + className;
  }
}
 
function removeClass(elemclassName) {
  if (hasClass(elem, className)) {
    elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
  }
}
 
function toggleClass(elemclassName) {
  (hasClass(elem, className) ? removeClass : addClass)(elem, className);
}

그럼 이것을 모듈에 적용해서, 미지원 브라우저를 위해 else 구문에 추가해줍니다.:

(function() {
 
  var apollo = {};
  var hasClass, addClass, removeClass, toggleClass;
 
  if ('classList' in document.documentElement) {
    hasClass = function() {
      return elem.classList.contains(className);
    };
    addClass = function(elemclassName) {
      elem.classList.add(className);
    };
    removeClass = function(elemclassName) {
      elem.classList.remove(className);
    };
    toggleClass = function(elemclassName) {
      elem.classList.toggle(className);
    };
  } else {
    hasClass = function(elemclassName) {
      return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function(elemclassName) {
      if (!hasClass(elem, className)) {
        elem.className += (elem.className ? ' ' : '') + className;
      }
    };
    removeClass = function(elemclassName) {
      if (hasClass(elem, className)) {
        elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
      }
    };
    toggleClass = function(elemclassName) {
      (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
  }
 
  apollo.hasClass = hasClass;
  apollo.addClass = addClass;
  apollo.removeClass = removeClass;
  apollo.toggleClass = toggleClass;
 
  window.apollo = apollo;
 
})();

지금까지의 작업은 jsFiddle에서 확인하실 수 있습니다.

이제 원래 구상했던 모든 기능을 구현했으므로, 여기서 마무리 짓겠습니다. apollo 모듈은 한꺼번에 여러 개의 클래스를 추가하는 등의 몇 가지 추가 기능들이 있는데, 더 관심이 있으시다면 여기에서 확인할 수 있습니다.

자, 그럼 지금까지 과정을 다시 짚어볼까요? 하나의 독립된 단위의 코드를 작성했는데 이놈은 오로지 한 가지의 작업에만 전념해서 그 기능을 잘 수행하게 하였습니다. 이런 모듈은 매우 단순해서 그냥 훑어봐도 이해하기 쉬우며 수정도 간편해서 유닛 테스트를 통한 기능 유효성 체크도 쉽습니다. 또한, jQuery가 필요치 않은 프로젝트에는 그냥 이 작은 덩치의 apollo 모듈만 불러오면 해당 기능을 충분히 활용할 수 있을 겁니다.

의존성 관리: AMD와 CommonJS

모듈의 개념은 새로운 게 아니어서, 우리는 항상 사용하고 있습니다. 이미 JavaScript는 브라우저를 통해야만 하는 것이 아니어서 서버 그리고 심지어 TV에서도 실행된다는 걸 아실 겁니다.

새로운 모듈로서 어떠한 형태의 것을 선택해서 사용해야 할까요? 그리고 또 어느 환경에서 사용할 수 있을까요? 고려해 볼 수 있는 게 “AMD”와 “CommonJS”라 불리는 두 가지의 개념이 있는데, 아래에서 살펴보도록 하겠습니다.

AMD

Asynchronous Module Definition(보통 AMD라 칭함)는 비동기식으로 읽히는 모듈을 정의하는 JavaScript API로서, 동기식으로 읽힘으로써 생기는 성능상의 비용과 함께 사용성, 디버깅 그리고 크로스 도메인 접근 문제 등을 피하는 수단으로 보통 브라우저에서 실행됩니다. AMD는 개발상 도움을 제공하는 동시에, 서러 다른 파일에 있는 JavaScript 모듈의 독립성을 보장해줍니다.

AMC는 define이라 불리는 function을 사용하는데, 모듈 자신과 함께 다른 export Objects를 정의합니다. 또한 AMD를 가지고 의존성을 지닌 임포트할 다른 모듈을 지정할 수도 있습니다. 아래는 AMD GitHub 프로젝트에 예시된 간단한 사용법입니다.:

define(['alpha'], function(alpha) {
  return {
    verbfunction() {
      return alpha.verb() + 2;
    }
  };
});

만약에 AMD 용법을 apollo에 적용한다면 다음과 같이 사용할 수 있습니다.:

define(['apollo'], function(alpha) {
  var apollo = {};
  var hasClass, addClass, removeClass, toggleClass;
 
  if ('classList' in document.documentElement) {
    hasClass = function() {
      return elem.classList.contains(className);
    };
    addClass = function(elemclassName) {
      elem.classList.add(className);
    };
    removeClass = function(elemclassName) {
      elem.classList.remove(className);
    };
    toggleClass = function(elemclassName) {
      elem.classList.toggle(className);
    };
  } else {
    hasClass = function(elemclassName) {
      return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function(elemclassName) {
      if (!hasClass(elem, className)) {
        elem.className += (elem.className ? ' ' : '') + className;
      }
    };
    removeClass = function(elemclassName) {
      if (hasClass(elem, className)) {
        elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
      }
    };
    toggleClass = function(elemclassName) {
      (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
  }
 
  apollo.hasClass = hasClass;
  apollo.addClass = addClass;
  apollo.removeClass = removeClass;
  apollo.toggleClass = toggleClass;
 
  window.apollo = apollo;
});

CommonJS

Node.js는 최근 몇 년 동안에 많은 주목을 받음과 동시에 의존성 관리 도구와 그 형태 또한 관심의 대상이 되고 있습니다. Node.js는 CommonJS라 불리는 것을 채용하고 있는데, 이것은 모듈의 내용을 정의할 때 “exports” Object를 사용하고 있습니다. 정말 단순하게 CommonJS를 구현해 놓은 예는 아래와 같습니다. (다른 곳에 사용될 것을 “export 한다”는 의미가 담겨 있습니다.)

// someModule.js 
exports.someModule = function () {
  return "foo";
};

위 코드는 자신의 모듈 파일 안에서 작성되어 집니다. 코드에선 someModue.js라는 이름을 가지고 있습니다. 다른 곳에서 해당 모듈을 가져와서 사용하려면, CommonJS에선 “require”라는 이름의 함수를 사용해서 지정해 놓으면 해당하는 각 의존 모듈을 불러오게 됩니다.:

// 'myModule'에 불러와서 사용 
var myModule = require('someModule');

만약에 Grunt나 Gulp를 써보셨다면, 많이 눈에 익은 형태일 겁니다.

이 형태를 apollo에도 적용해 본다면 다음과 같이 작성할 수 있는데, window 대신에 exports Object에다 모듈을 붙이게 됩니다. (코드 마지막에 있는 exports.apollo = apollo):

(function() {
 
  var apollo = {};
  var hasClass, addClass, removeClass, toggleClass;
 
  if ('classList' in document.documentElement) {
    hasClass = function() {
      return elem.classList.contains(className);
    };
    addClass = function(elemclassName) {
      elem.classList.add(className);
    };
    removeClass = function(elemclassName) {
      elem.classList.remove(className);
    };
    toggleClass = function(elemclassName) {
      elem.classList.toggle(className);
    };
  } else {
    hasClass = function(elemclassName) {
      return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function(elemclassName) {
      if (!hasClass(elem, className)) {
        elem.className += (elem.className ? ' ' : '') + className;
      }
    };
    removeClass = function(elemclassName) {
      if (hasClass(elem, className)) {
        elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
      }
    };
    toggleClass = function(elemclassName) {
      (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
  }
 
  apollo.hasClass = hasClass;
  apollo.addClass = addClass;
  apollo.removeClass = removeClass;
  apollo.toggleClass = toggleClass;
 
  exports.apollo = apollo;
 
})();

Universal Module Definition (UMD)

AMD와 CommonJS 모두 다 훌륭한 접근 방법이지만, 만약에 모든 환경에서 적용할 수 있는 모듈을 작성하고 싶다면 어떻게 해야 할까요? AMD, CommonJS 그리고 브라우저 모두 다 포함해서 말입니다.

처음엔 그냥 ifelse 구문을 사용해서 AMD 혹은 CommonJS를 지원하는지 확인해서 해당 형식에 맞은 함수를 그냥 단순하게 전달하는 방법을 사용했었습니다. 이런 아이디어는 나중에 보편적 해법으로 “UMD“라는 이름으로 적용되어 사용되고 있습니다. 이놈은 그냥 과거 if/else 기법을 한데로 묶어서 우린 그냥 하나의 함수를 전달만 해주면 각 모듈 형식에 맞게 알아서 처리해 줍니다. 아래는 프로젝트 저장소에 나와 있는 사용 예입니다.:

(function (rootfactory) {
  if (typeof define === 'function' && define.amd) {
      // AMD. Register as an anonymous module. 
      define(['b'], factory);
  } else {
      // Browser globals 
      root.amdWeb = factory(root.b);
  }
}(this, function (b) {
  //use b in some fashion. 
 
  // Just return a value to define the module export. 
  // This example returns an object, but the module 
  // can return a function as the exported value. 
  return {};
}));

맙소사! 이게 무슨 소리냐. 여기선 IIFE 블락의 두번째 인자에다가 우리의 함수를 던져주면, factory라는 이름의 로컬 변수에 지정되고 상황에 따라 자동으로 AMD 혹은 브라우저 전역 객체에 지정되어 집니다. 물론, 이놈은 CommonJS를 지원하지 않습니다만, 다음과 같은 약간의 코드 수정으로 이 문제를 해결할 수 있답니다. (해석을 쉽게 하기 위해 이번엔 코멘트도 삭제하였습니다.):

(function(rootfactory) {
  if (typeof define === 'function' && define.amd) {
    define(['b'], factory);
  } else if (typeof exports === 'object') {
    module.exports = factory;
  } else {
    root.amdWeb = factory(root.b);
  }
}(this, function(b) {
  return {};
}));

여기서 중요한 곳은 module.exports = factory 부분인데, 우리의 모듈을 상징하는 factory를 CommonJS에 붙여 줍니다.

지금까지 설명된 것을 정리하는 차원에서 제 apollo를 가지고 UMD에 맞게 설정해서 CommonJS 환경이나 AMD 그리고 브라우저에서도 사용될 수 있도록 고쳐봅시다! 아래에서는 GitHub에 저장된 최신 버전의 apollo 스크립트를 모두 옮겨 놓은 것입니다. 참고로 몇 가지 기능이 더 추가되어서 지금까지 설명했던 것보다 약간 더 복잡해 보일 수도 있습니다.:

/*! apollo.js v1.7.0 | (c) 2014 @toddmotto | https://github.com/toddmotto/apollo */
(function(rootfactory) {
  if (typeof define === 'function' && define.amd) {
    define(factory);
  } else if (typeof exports === 'object') {
    module.exports = factory;
  } else {
    root.apollo = factory();
  }
})(this, function() {
 
  'use strict';
 
  var apollo = {};
 
  var hasClass, addClass, removeClass, toggleClass;
 
  var forEach = function(itemsfn) {
    if (Object.prototype.toString.call(items) !== '[object Array]') {
      items = items.split(' ');
    }
    for (var i = 0; i < items.length; i++) {
      fn(items[i], i);
    }
  };
 
  if ('classList' in document.documentElement) {
    hasClass = function(elemclassName) {
      return elem.classList.contains(className);
    };
    addClass = function(elemclassName) {
      elem.classList.add(className);
    };
    removeClass = function(elemclassName) {
      elem.classList.remove(className);
    };
    toggleClass = function(elemclassName) {
      elem.classList.toggle(className);
    };
  } else {
    hasClass = function(elemclassName) {
      return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function(elemclassName) {
      if (!hasClass(elem, className)) {
        elem.className += (elem.className ? ' ' : '') + className;
      }
    };
    removeClass = function(elemclassName) {
      if (hasClass(elem, className)) {
        elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
      }
    };
    toggleClass = function(elemclassName) {
      (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
  }
 
  apollo.hasClass = function(elemclassName) {
    return hasClass(elem, className);
  };
 
  apollo.addClass = function(elemclasses) {
    forEach(classes, function(className) {
      addClass(elem, className);
    });
  };
 
  apollo.removeClass = function(elemclasses) {
    forEach(classes, function(className) {
      removeClass(elem, className);
    });
  };
 
  apollo.toggleClass = function(elemclasses) {
    forEach(classes, function(className) {
      toggleClass(elem, className);
    });
  };
 
  return apollo;
 
});

지금까지 제가 작성한 모듈을 가지고 다양한 환경에서도 잘 작동할 수 있도록 말 그대로 다시 포장해 보았습니다. 작업 환경에 이런 새로운 의존성 관리 기법을 적용한다면 더 나은 융통성을 발휘할 수 있을 겁니다. 물론 이것은 JavaScript 라이브러리가 처음부터 작은 단위의 함수 모음으로 쪼개어 개발되지 않았다면 불가능한 일이었겠지요.

테스팅

일반적으로 모듈은 작은 단위의 유닛 테스트와 함께 배포되는데, 이렇게 하면 커다란 라이브러리와는 비교되게 다른 개발자가 프로젝트에 참여해서 오류나 기능 개선 요구하기도 훨씬 쉬워집니다. 그래서 커다란 라이브러리의 경우엔 새로운 기능의 추가나 오류 수정이 비교적 오래 걸리는 반면에, 작은 모듈은 빠른 속도로 자주 갱신되는 경향을 보입니다.

마무리

직접 모듈을 작성하고 다양한 개발 환경을 지원한다면 수많은 개발자에게 도움을 줄 수 있을 것이고, 이것은 아주 멋진 경험일 겁니다. 또한, 개발을 더 재밌고 관리하기도 편하며, 매일 사용하는 도구를 더 깊이 이해할 기회입니다. 보통 모듈과 함께 배포되는 문서를 참고하면 빠르고 쉽게 바로 자기만의 작업 환경에 도입해서 사용하실 수 있을 겁니다. 만약에 모듈이 적합한 것이 아니라면, 다른 것을 찾아보거나 혹은 직접 작성하실 수도 있겠지요. 이것이야말로 모듈이 가지고 있는 커다란 장점으로, 보통 하나의 거대 용량의 라이브러리에 묶여서 거의 모든 것을 의존하게 되는 상황에선 분명 쉽게 해결할 수 없는 문제입니다.

보너스: ES6 모듈

마무리 짓기 전에 덧붙이고 싶은 말이 있다면, 종전의 JavaScript 라이브러리들이, 예를 들어 클래스 조작과 같이, 원래의 랭귀지 자체에 어떤 영향을 끼치게 되는 현상을 살펴보면 흥미롭지 않으세요? 이번에도 역시나, 다음 세대의 JavaScript 랭귀지인 ES6에서도 또 다시 같은 현상이 일어나고 말았습니다! 랭귀지 자체에서 imports와 exports를 지원하게 된 것입니다!

모듈을 export 하는 법.:

/// myModule.js 
function myModule () {
  // module content 
}
export myModule;

이에 대응해서 import 하는 법.:

import {myModule} from 'myModule';

모듈 규약을 포함해서 자세한 ES6에 관한 내용은 여기에서 확인하실 수 있습니다.

관련된 주제의 글

댓글을 남겨 주세요