본문으로 바로가기

스크립트 모듈화 - Require.js #1

category 웹코딩/Javascript 2016. 4. 1. 19:23

자바스크립트(JS)는 자체적으로 파일간의 모듈화를 지원하지 않기때문에 이를 유지 보수하는 일에서 전역변수를 오염시키고 서로간의 간섭을 신경써야 합니다. 모듈패턴을 이용하여 최소한의 간섭을 유지하고, 동적으로 스크립트를 로딩하여 필요한 것만 다운받게 만들며 최적화를 꾀할 수 있지만, 파일이 커지고 코드가 길어질 수록 이를 체계적으로 관리할 수 있는 도구가 절실해집니다. 이를 보완해주는 라이브러리중에 하나인 Require.js에 대하여 알아봅니다.

Require.js 소개

Require.jsAMD(Asynchronous Module Definition - 비동기 모듈 정의)를 바탕으로 제작된 JS 모듈 로더(loader) 파일이다. 이는 JS 코드를 모듈화하고 모듈간의 의존성 문제를 해결하여 코드를 파악하기 쉽게 만들고 관리에 도움을 준다. 또한, lazy-loading(동적로딩)을 통해서 스크립트 로딩을 최적화한다.

AMD는 브라우저의 비동기적인 상황에서 JS 모듈화의 표준을 구현하기 위해 조직된 그룹이다.

브라우저 지원

  • IE 6+
  • Firefox 2+
  • Safari 3.2+
  • Chrome 3+
  • Opera 10+

아래의 특징들을 살펴본다면 이해에 도움이 될 것이다.

모듈을 파일로 세분화

기존에는 하나 혹은 몇개의 파일에 JS를 모듈패턴으로 구조화하여 최소한으로 전역에 노출하는 방식을 애용했다. 이를 유지보수하는 입장에서는 전역에 노출된 변수, 함수, 객체등을 유의해야 했고 새로운 무엇인가를 추가할때도 모듈패턴으로 구조화해야 했다.

Require.js는 전역에 어떠한 것도 노출시키지 않는 구조를 기본으로 하며, 이를 지원하지 않는 다른 라이브러리들이나 파일들의 경우는 설정을 통해 전역에 노출할 수 있게 한다. 모듈을 전역에 노출시키지 않기 위해 모듈별로 하나의 파일로 구성하며, return을 이용하여 모듈 외부에서 접근할 수 있도록 노출할 것을 지정할 수 있다.

의존성(종속성) 해결

JS 코드를 유지보수시에 또 하나의 걸림돌은 의존성을 파악하는 것이다. 코드 어느부분에서 다른 모듈이나 라이브러리에서 어떤 것을 가져다 쓰는지를 파악하는 것은 전체 구조를 이해해야 하는 작업이 될 수도 있는 까다로운 일이다. 또한 브라우저는 네트워크를 통해 접근하려는 파일의 다운로드가 필요하기 때문에 Requier.js는 모듈을 정의하거나 호출할때 이를 정해진 문법에 따라 초입에 종속성 코드를 작성하므로 한 눈에 관계를 파악할 수 있게 된다.

Lazy-loading(동적로딩)

필요한 경우에만 스크립트를 로딩하기 위한 기법으로 아래와 같이 동적로딩을 사용해왔다.

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가 로딩 완료한 시점에 실행
});

Require.js는 모듈화로 구성되기 때문에 자동적으로 무엇인가 호출하면 필요한 모듈을 로드하고 모듈에서 필요한 종속성을 검사하고 다시 필요한 것을 로드한다. 이미 한번 로드된 것이라면 캐시를 활용하니 필요한 것만을 자동적으로 로드하는 동적로드가 구성된다.

충돌의 가능성을 제거

여러 라이브러리들을 이용하거나 같은 라이브러리의 다른 버전들이 사용되는 경우가 있다. 어떤 것들은 같은 전역변수를 이용하여(prototype과 jQuery처럼) 서로 충돌이 발생한다.

Require.js를 이용해 각각의 버전들을 따로 모듈화할 수도 있고, 모듈에 주어진 ID로 호출하는 대신 다른 모듈 ID로 대체하여 사용할 수 있다.

Require.js 파일로딩

Require.js는 일반적인 <script> 태그를 사용한 스크립트 로딩 방식과 다른 접근방식을 취하고 있다. 이는 빠르게 실행되고 최적화되는 동시에, 주요 목표는 모듈형 코드가 되도록 하는 것이다. 그 일환으로, <script> 태그의 URL 대신에 모듈 ID를 사용하는 것을 권장하고 있다.

Require.js는 baseUrl로 설정된 위치에 상대경로를 통해 모든 코드를 불러온다. baseUrl은 일반적으로 아래와 같이 data-main 속성이 설정된 최상위 스크립트와 같은 경로를 가지게 되며, data-main 속성은 require.js가 스크립트 로딩을 시작하게 되는 위치를 지정하게 된다.

<!-- "scripts" 디렉토리에 baseUrl을 설정하고, 모듈 ID를 'main'으로 하는 스크립트를 불러온다 -->
<script data-main="scripts/main" src="scripts/require.js"></script>

baseUrl은 RequireJS Config를 통해 직접 설정할 수도 있다. 만일 명시적인 Config 설정이 없고, data-main 속성도 사용되지 않았다면 baseUrl은 Require.js가 실행되는 HTML 페이지를 포함하는 디렉토리이다.

설명이 복잡하지만 문서에 require.js 파일을 삽입하고, require.js가 처음으로 불러오는 스크립트 파일을 설정하며, baseUrl로 기본 디렉토리를 설정하는 것이다.

Requier.js는 기본적으로 모든 의존적인 파일들을 스크립트 파일(js)이라고 가정한다. 그렇기때문에 .js를 모듈 ID에 생략할 수 있으며, 자동적으로 해석될때 .js를 덧붙여준다.

Require.js 모듈 정의와 사용

Requir.js의 모듈은 전역 네임스페이스를 오염시키지 않으며 스코프가 형성되어 있는 객체이다. 이것은 의존성 목록을 쉽게 확인 가능하며, 전역 객체를 참조할 필요없이 의존성을 조작할 수 있도록 해주며, 의존성에 해당하는 것을 함수의 인자로 넘겨준다. Require.js에서의 모듈은 모듈패턴의 확장이며, 다른 모듈을 참조하기 위해 전역 객체를 필요로 하지 않는 이점이 더해졌다.

Require.js 문법을 통해 가능한한 빠르게 모듈을 로딩할 수 있으며, 순서가 정해져있지 않더라도 이를 정확한 의존성 순서로 정렬하며, 전역 변수들이 생성되지 않기 때문에 다양한 버전의 모듈을 한 페이지에 불러오는 것을 가능하게 한다. 파일당 오직 하나의 모듈만 정의되어야 하며, 모듈은 최적화 툴을 통해 묶음으로 그룹핑될 수 있다.

간단한 이름/값으로 이루어진 모듈 정의

모듈이 어떠한 의존성도 가지고 있지 않고, 이름/값으로 이루어져 있다면 객체 리터럴의 방식으로 정의할 수 있다.

define({
    color: "black",
    size: "unisize"
});

함수 정의

모듈이 의존성은 가지지 않지만, 작업을 위해 함수가 필요하다면 define() 내에 함수를 추가한다.

 define(function () {
    //Do setup work here

    return {
        color: "black",
        size: "unisize"
    }
});

의존성이 있는 함수 정의

모듈이 의존성을 포함하고 있다면 첫번째 인자로 의존성 이름을 배열로 나열하고, 두번째 인자는 함수 정의여야 한다. 함수는 모든 의존성이 불러들여졌을때 모듈에 대한 define을 호출하게 된다. 함수는 모듈을 정의한 객체를 반환해야 하며, 의존성은 정의된 함수에 함수 인자의 형태로 의존성 배열의 순서와 같은 순서로 들어가야 한다.

define(["./cart", "./inventory"], function(cart, inventory) {
         return {
            color: "blue",
            size: "large",
            addToCart: function() {
                inventory.decrement(this);
                cart.add(this);
            }
        }
    }
);

위의 예제에서는, 모듈이 /cart 와 /inventory에 의존성이 있는 구조이며, 함수 호출은 두개의 인자를 기술하고 있다. 이 두개의 의존성들이 모두 로드될때 까지 함수는 호출되지 않는다.

모듈을 이름으로 정의

define() 호출시에 첫번째 인자로 모듈의 이름을 명시할 수도 있다.

define("foo/title",
    ["my/cart", "my/inventory"],
    function(cart, inventory) {
        //Define foo/title object in here.
    }
);

이 형태는 일반적으로 optimizer tool에서 생성되는 형태이다. 모듈의 이름을 명시할 수 있지만, 이것은 모듈의 이동성을 제한하게 된다. 개발시에 파일의 위치이동은 빈번하게 발생하므로, 그때마다 이름을 변경해야하는 수고스러움이 뒤따르기 때문에 평소에는 모듈의 이름을 명시하는 것을 피하고, 최적화툴에 의해 자동으로 이름을 작성하도록 하는 것이 최선이다. optimizer tool에서는 브라우저의 로딩 속도를 높이기 위해, 하나 이상의 모듈이 하나의 파일로 합쳐질 수 있도록 이름을 더해주게 된다.