AMD
Asynchronous module definition
AMD는 동적 로딩, 의존성 관리, 모듈화가 톱니바퀴처럼 아름답게 맞물린 API 디자인을 제시한다.
기능이 아니라 개념적인 내용이며, AMD를 구현한 라이브러리(requirejs 등)에서 제공하는 API를 사용한다.
AMD 개념을 가장 잘 충족한 것이 requirejs이고 define() 과 require()를 사용하여 모듈을 정의하고 로딩하는 API를 제공한다.
동적 로딩
script나 css,이미지같은 정적리소스는 페이지 렌더링을 방해한다.
script 태그의 HTTP 요청 -> 다운로드 -> 파싱(Parsing) -> 실행이 일어나는 동안 브라우저는 다른 동작을 하지 않는다.
브라우저 입장에서는 당연하고 안전한 동작 방식이지만 사용자 입장에서는 빨리 화면이 보이고 버튼이 동작하기를 바랄 뿐이다.
그래서 최적화 기법 중의 하나로 script 태그를 가능한 한 body 태그의 마지막에 배치하는 방법이 있다.
이렇게 하면 페이지의 나머지 부분이 렌더링된 후에 script 태그가 로딩되므로, 페이지의 첫 렌더링과 인터랙션이 빨라진다.
하지만 사용자의 첫 인터랙션(상호작용)이 가능할 때까지 걸리는 시간이 줄어들지는 않는다.
페이지를 더 빨리 렌더링할 수는 있어도 사용자가 버튼을 클릭하거나 입력 필드에 글자를 입력할 수 있는 시점은 여전히 script 태그가 로딩되고 실행된 후이기 때문이다.
화면이 복잡하고 AJAX로 이루어진 웹앱 수준의 페이지에서는 script 태그가 로딩되는 동안 사용자가 아무것도 할 수 없는 상황이 발생할 수 있다.
이런 문제를 해결하기 위해서 동적 로딩이 필요하다.
동적 로딩(Dynamic Loading, Lazy Loading 이라고도 부른다)은 페이지 렌더링을 방해하지 않으면서 필요한 파일만 로딩할 수 있다.
이를 구현하는 방법 중 하나로 script 태그의 동적 삽입이 있다.
이는 JavaScript로 script 태그를 생성하여 추가하는 방법이다.
이 외에도 XMLHttpRequest, document.write(), defer 같은 방법이 있지만,
범용적으로 사용하기에는 치명적인 단점이 하나씩은 있어서 script 태그의 동적 삽입이 제일 안전하고 합리적이다. 간단한 구현은 다음과 같다.
var scriptEl = document.createElement('script');
scriptEl.type = 'text/javascript';
scriptEl.src = 'example.js';
document.getElementsByTagName('head')[0].appendChild(scriptEl);
이를 응용하면 JavaScript 파일의 URL을 매개변수로 받아 범용적인 동적 로딩 함수를 만들 수 있다.
그리고 로딩 완료 이벤트 처리가 가능하므로 안전하게 해당 파일의 변수나 함수를 사용할 수 있다.
즉, 비동기로 동작하며 로딩 완료 후에 콜백 함수를 호출하게 하는 것이다.
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 () {
// 콜백 지옥에 빠졌다.
});
});
});
});
다행스럽게도 AMD는 이를 자연스럽게 해결했다. AMD는 모듈을 정의하고 의존성을 관리하는 API를 제공한다.
이 API는 모듈을 비동기적으로 로딩할 수 있는 방법을 제공하며, 모듈 간의 의존성을 명시적으로 관리할 수 있다.
requirejs같은 라이브러리는 는 AMD를 구현한 라이브러리로, 모듈을 정의하고 로딩하는 데 필요한 기능을 제공한다.
의존성 관리
JavaScript는 스크립트 간의 의존성을 파악하기 힘들다.
왜냐하면 JavaScript는 전역 공간에 변수를 선언하고, 다른 스크립트에서 이 변수를 참조하기 때문이다.
이런 방식은 스크립트가 많아지면 전역 공간에 변수가 남발되어 충돌이 발생할 수 있다.
이런 문제를 해결하기 위해 AMD는 모듈을 정의하고 의존성을 관리하는 API를 제공한다.
예를 들어, util.js라는 모듈이 있다고 가정해보자. 이 모듈은 문자열을 다루는 유틸리티 함수들을 제공한다.
var util = loadModule('util');
util.trim();
이렇게 하면 util.js 모듈을 로딩하고, 해당 모듈에서 제공하는 trim() 함수를 사용할 수 있다. 하지만 이 방식은 모듈 간의 의존성을 명시적으로 관리하지 않는다. AMD는 모듈을 정의할 때 의존성을 명시적으로 관리할 수 있는 방법을 제공한다.
requirejs를 사용하여 util.js 모듈을 정의하고, 해당 모듈에서 제공하는 trim() 함수를 사용할 수 있다.
define(['util'], function (util) {
// util 모듈을 의존성으로 정의
return {
trim: function (str) {
return util.trim(str);
}
};
});
이렇게 하면 util 모듈을 의존성으로 정의하고, 해당 모듈에서 제공하는 trim() 함수를 사용할 수 있다.
모듈화
스크립트 내부에서만 사용하는 변수, 함수들은 전역 공간에 둘 필요가 없고 두어서도 안 된다. 전역변수 남발과 이로 인한 충돌은 유지 보수에 막대한 영향을 끼쳐서 개발자의 심신을 괴롭히기 때문이다. 스크립트의 모듈화는 이런 문제를 방지한다.
기본적인 모듈 패턴은 다음과 같다. return으로 외부에서 접근할 변수와 함수만 골라서 노출할 수 있으며, 외부에 노출할 필요 없는 변수와 함수는 클로저(Closure)를 이용하여, 전역 공간에 위치시키지 않고도 접근할 수 있다.
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();
단점
-
비효율적인 초기 로딩 시간: AMD는 모듈을 비동기적으로 로드하기 때문에 초기 페이지 로딩 시간이 길어질 수 있습니다. 특히 모듈의 의존성이 많은 경우, 모든 모듈이 비동기적으로 로드되기 때문에 초기 로딩 속도가 느려질 수 있습니다.
-
복잡성: AMD를 사용하면 모듈 간의 의존성을 명시적으로 관리해야 합니다. 이는 코드를 작성하고 유지보수하는 데 복잡성을 추가할 수 있습니다. 특히 대규모 프로젝트의 경우 모듈 간의 의존성을 관리하는 것이 어려울 수 있습니다.
-
브라우저 호환성: AMD는 브라우저 환경에서 기본적으로 지원되지 않습니다. 따라서 AMD를 사용하는 경우, RequireJS와 같은 AMD 로더 라이브러리를 별도로 포함해야 합니다. 이는 번들링 과정에서 추가적인 오버헤드를 초래할 수 있습니다.
-
성능: AMD는 모듈을 비동기적으로 로드하기 때문에 성능 면에서 다른 모듈 시스템에 비해 느릴 수 있습니다. 특히 초기 로딩 시간이나 모듈의 의존성이 많은 경우에는 더욱 뚜렷할 수 있습니다.