AMD란?

Posted by : on

Category : requirejs


AMD

Asynchronous module definition

AMD는 동적 로딩, 의존성 관리, 모듈화가 톱니바퀴처럼 아름답게 맞물린 API 디자인을 제시한다. AMD의 자세한 배경과 연관 기술에 관해서는 “JavaScript 표준을 위한 움직임: CommonJS와 AMD” 를 참고한다. 이 글에서는 AMD의 근간이 되는 3가지 개념을 살펴보겠다.

동적 로딩

script 태그는 페이지 렌더링을 방해한다. script 태그의 HTTP 요청과 다운로드, 파싱(Parsing), 실행이 일어나는 동안 브라우저는 다른 동작을 하지 않는다. 브라우저 입장에서는 당연하고 안전한 동작 방식이지만 사용자 입장에서는 빨리 화면이 보이고 버튼이 동작하기를 바랄 뿐이다. 그래서 최적화 기법 중의 하나로 script 태그를 가능한 한 body 태그의 마지막에 배치하는 방법이 있다.

하지만 사용자의 첫 인터랙션이 가능할 때까지 걸리는 시간이 줄어들지는 않는다. 페이지를 더 빨리 렌더링할 수는 있어도 첫 렌더링과 첫 인터랙션에 필요하지 않은, 페이지에 필요한 모든 JavaScript를 로딩하기 때문이다.

화면이 복잡하고 AJAX로 점철된 웹앱 수준의 규모에서는 이 시간이 큰 폭으로 커진다. 웹앱은 AJAX로 전환되는 여러 뷰(View)를 가지고 있는 경우가 흔하다. 더 최적화를 하자면 첫 렌더링과 인터랙션에 필요한 JavaScript만 먼저 로딩하고 후에 사용자의 반응에 따라 나머지를 로딩하는 점진적인 방식이 필요하다.

동적 로딩(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을 매개변수로 받아 범용적인 동적 로딩 함수를 만들 수 있다. 그리고 로딩 완료 이벤트 처리가 가능하므로 안전하게 해당 파일의 변수나 함수를 사용할 수 있다. 즉, 비동기로 동작하며 로딩 완료 이벤트 핸들러는 콜백 함수이다.

이를 응용하면 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는 이를 자연스럽게 해결했다.

의존성 관리

JavaScript는 스크립트 간의 의존성을 파악하기 힘들다. 왜냐하면 언어 차원에서 #include나 package/import같은 명시적이고 강제적인 키워드, 패키징 정책을 지원하지 않기 때문이다. 파일 상단에 JSDoc의 @requires라도 적혀 있으면 다행이지만 결국 주석일 뿐이다.

결국 Java의 package/import같은 기능이 필요하고 이것이 특정 기능을 불러와서 사용할 수 있는 유일한 방법이어야 한다. 이를 위한 기본 조건은 특정 기능의 스크립트가 이름을 붙일 수 있는 하나의 단위로 묶여야 한다. 그래야 다른 스크립트에서 그 이름으로 스크립트를 불러올 수 있는 방법이 생기기 때문이다.

정의한 객체를 사용하기 위해서는 반대로 호출 함수가 있어야 한다. 정의한 객체가 비밀 공간에 있기 때문이다. 이 호출 함수가 다음과 같이 객체를 불러오는 강제적이고 유일한 방법이 된다.

var util = loadModule('util');  
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();

단점

  1. 비효율적인 초기 로딩 시간: AMD는 모듈을 비동기적으로 로드하기 때문에 초기 페이지 로딩 시간이 길어질 수 있습니다. 특히 모듈의 의존성이 많은 경우, 모든 모듈이 비동기적으로 로드되기 때문에 초기 로딩 속도가 느려질 수 있습니다.

  2. 복잡성: AMD를 사용하면 모듈 간의 의존성을 명시적으로 관리해야 합니다. 이는 코드를 작성하고 유지보수하는 데 복잡성을 추가할 수 있습니다. 특히 대규모 프로젝트의 경우 모듈 간의 의존성을 관리하는 것이 어려울 수 있습니다.

  3. 브라우저 호환성: AMD는 브라우저 환경에서 기본적으로 지원되지 않습니다. 따라서 AMD를 사용하는 경우, RequireJS와 같은 AMD 로더 라이브러리를 별도로 포함해야 합니다. 이는 번들링 과정에서 추가적인 오버헤드를 초래할 수 있습니다.

  4. 성능: AMD는 모듈을 비동기적으로 로드하기 때문에 성능 면에서 다른 모듈 시스템에 비해 느릴 수 있습니다. 특히 초기 로딩 시간이나 모듈의 의존성이 많은 경우에는 더욱 뚜렷할 수 있습니다.


About 유재석
유재석

개발자 유재석 입니다. Web Developer.

Email : jaeseok9405@gmail.com

Website : https://github.com/yoo94