본문으로 바로가기

Module Pattern

모듈화와 모듈 패턴에 대해 알아봅니다.


먼저 이 개념에 대해 알아보기 전에 고유변수(private member)와 특권메소드(privileged member)에 대해 간략히 짚고 넘어가자.


자바 등 다른 언어와는 달리 자바스크립트에는 private, protected, public 프로퍼티와 메서드를 나타내는 별도의 문법이 없다. 

객체의 모든 멤버는 public, 즉 공개되어 있다.

다시 말해서,자바스크립트에는 고유 구성원(private member)이란 개념이 없으며 객체의 프로퍼티는 모두 공용(public)이다. 

하지만 '고유변수'란 개념은 존재한다. 

예를 들어 함수 안에서 정의한 변수는 함수 밖에서 접근할 수가 없으므로 모두 고유변수(private member)라고 간주한다. 

이 고유변수에는 함수의 매개변수,지역 변수, 내부 함수 등이 포함된다

아래에서는 먼저 모듈 패턴 #1 에서 알아본 네임스페이스 패턴에 대해 추가적으로 알아보고 위에서 언급한 내용에 대해 계속 알아보도록 하겠습니다.



네임스페이스 패턴 생성시 유용한 범용적인 네임스페이스 함수

프로그램의 복잡도가 증가하고 코드의 각 부분들이 별개의 파일로 분리되어 선택적으로 문서에 포함하게 되면, 어떤 코드가 특정 네임스페이스나 그 내부의 프로퍼티를 처음으로 정의한다고 가정하기가 위험합니다.

네임스페이스에 추가하려는 프로퍼티가 이미 존재할 수도 있고 따라서 내용을 덮어쓰게 될 지도 모릅니다.

그러므로 네임스페이스를 생성하거나 프로퍼티를 추가하기 전에 먼저 이미 존재하는지 여부를 확인하는 것이 최선입니다.


다음의 예제를 살펴보도록 합니다.

javascript
// 위험하다.
var MYAPP = {};

// 개선안
if (typeof MYAPP === 'undefined') {
    MYAPP = {};
}

// 또는 더 짧게 작성할 수 있다.
var MYAPP = MYAPP || {};

이렇게 추가되는 확인 작업 때문에 상당량의 중복 코드가 생겨날 수 있습니다.

예를 들어 MYAPP.modules.module2 를 정의하려면, 각 단계의 객체와 프로퍼티를 정의할 때마다 확인 작업을 거쳐야 하므로 코드가 세 번 중복됩니다.

따라서 네임스페이스 생성의 실제 작업을 맡아 줄 재사용 가능한 함수를 만들어두면 편리합니다.

다음은 네임스페이스 함수를 구현한 예제입니다.

다음과 같은 방식은 해당 네임스페이스가 존재하면 덮어쓰지 않기 때문에 기존 코드를 망가뜨리지 않습니다.

var MYAPP = MYAPP || {};

MYAPP.namespace = function (ns_string) {
    var parts  = ns_string.split('.'),
        parent = MYAPP,
        i;

    // 처음에 중복되는 전역 객체명은 제거한다.
    if (parts[0] === 'MYAPP') {
        parts = parts.slice(1);
    }

    for (i = 0; i < parts.length; i += 1) {
        if (typeof parent[parts[i]] === 'undefined') {
            parent[parts[i]] = {};
        }
        parent = parent[parts[i]];
    }

    return parent;

};


이 코드는 다음 모든 예에서 사용할 수 있습니다.

javascript
var module2 = MYAPP.namespace('MYAPP.modules.module2');
console.log(module2);
console.log(module2 === MYAPP.modules.module2); // true 가 기록

// 첫 부분의 'MYAPP' 을 생략하고도 사용할 수 있다.
MYAPP.namespace('modules.module10');

// 매우 긴 네임스페이스를 만들어 본다.
MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property');

console.log(MYAPP);


다음의 그림은 위 예제를 요소 검사기로 MYAPP 을 실제로 검사했을 때의 모습입니다.



의존 관계 선언

자바스크립트 라이브러리들은 대개 네임스페이스를 지정하여 모듈화되어 있기 때문에, 필요한 모듈만 골라서 사용할 수 있습니다.

예를 들어, YUI2 에는 네임스페이스 역할을 하는 YAHOO 라는 전역 변수가 있고, 이 전역 변수의 프로퍼티로 YAHOO.util.DOM(DOM 모듈)이나 YAHOO.util.Event(이벤트 모듈)와 같은 모듈이 추가되어 있습니다.

이때 함수나 모듈 최상단에, 의존 관계에 있는 모듈을 선언하는 것이 좋습니다.

즉, 지역변수를 만들어 모듈을 가리키도록 선언하는 것입니다.

javascript
var myFunction = function () {
    // 의존 관계에 있는 모듈들
    var event = YAHOO.util.Event,
        dom   = YAHOO.util.Dom;

    // 이제 event 와 dom 이라는 변수를 사용한다...

};

대단히 간단한 패턴이지만 상당히 많은 장점을 가지고 있습니다.

  • 의존 관계가 명시적으로 선언되어 있기 때문에 코드를 사용하는 사람이 페이지 내에서 반드시 포함시켜야 하는 스크립트 파일이 무엇인지 알 수 있습니다.
  • 함수의 첫머리에 의존 관계가 선언되기 때문에 의존 관계를 찾아내고 이해하기 쉽습니다.
  • dom 과 같은 지역 변수는 YAHOO 와 같은 전역 변수보다 언제나 더 빠르며 YAHOO.util.Dom 처럼 전역 변수의 중첩 프로퍼티와 비교하면 더 말할 것도 없습니다. 의존 관계 선언 패턴을 잘 지키면 함수 안에서 전역 객체 판별을 단 한번만 수행하고 이 다음부터는 지역 변수를 사용하기 때문에 훨씬 빠릅니다.



비공개 멤버

javascript
function add(num1, num2) {
    var sum = num1 + num2;
    return sum;
}


위 코드의 함수에는 num1, num2, sum이라는 세 가지의 고유변수가 있다. 이들 변수는 함수 내부에서는 접근이 가능하지만 함수 외부에서는 접근이 불가능하다. 

비공개 멤버에 대한 별도의 문법은 없지만 클로저를 사용해서 구현할 수 있다.

생성자 함수 안에서 클로저를 만들면, 클로저 유효범위 안의 변수는 생성자 함수 외부에 노출되지 않지만 객체의 공개 메서드 안에서는 쓸 수 있다. 클로저를 함수 안에 만들어 사용하면 스코프 체인을 통해 이들 변수에 접근이 가능하게 된다. 

즉, 생성자에서 객체를 반환할 때 객체의 메서드를 정의하면, 이 메서드안에서는 비공개 변수에 접근할 수 있는 것이다.


javascript
function Gadget(){
    // 비공개 멤버(private member)
    var name = 'iPod';

    // 공개된 함수(privieged member)
    this.getName = function(){
        return name;
    };
}

var toy = new Gadget();

// 'name'은 비공개이므로 undefined가 출력된다.
console.log(toy.name);

// 공개 메서드에서는 'name'에 접근할 수 있다.
console.log(toy.getName());


보시다시피 자바스크립에서도 쉽게 비공개 멤버를 구현할 수 있다. 

비공개로 유지할 데이터를 함수로 감싸기만 하면 된다. 

이 데이터들을 함수의 지역 변수로 만들면, 함수 외부에서는 접근할 수 없다.



특권(privileged) 메서드

위와 같은  코드 기법을 활용하여 함수 외부에서 고유변수에 접근이 가능하도록 공용 메소드를 만들 수가 있는데 이를 특권(privileged) 메소드라고 한다.

다시 말해, 특권 메소드는 고유변수/함수에 접근이 가능한 공용메소드인 것이다.

특권 메서드는 단지 비공개 멤버에 접근권한을 가진 공개 메서드를 가리키는 이름일 뿐이다. 

앞선 예제에서는 getName()은 비공개 프로퍼티인 name에 '특별한' 접근 권한을 가지고 있기 때문에 특권 메서드라고 할 수 있다.



비공개 멤버의 허점

특권 메서드에서 비공개 변수의 값을 바로 반환할 경우 이 변수가 객체나 배열이라면 값이 아닌 참조가 반환되기 때문에, 외부 코드에서 비공개 변수 값을 수정할 수 있다.


javascript
function Gadget(){
    // 비공개 멤버
    var specs = {
        screen_width : 320,
        screen_height : 480,
        color : "white"
    };

    // 공개 함수
    this.getSpecs = function(){
        return specs;
    };
}

var toy = new Gadget(),
        specs = toy.getSpecs();

specs.color = "black";
specs.price = "free";

console.log(toy.getSpecs());
//Object {screen_width: 320, screen_height: 480, color: "black", price: "free"}


얼핏 보기엔 별 문제 없어 보이나 여기서 getSpec() 메서드가 specs 객체에 대한 참조를 반환한다는게 문제다. 

specs는 감춰진 비공개 멤버처럼 보이지만 Gadget 사용자에 의해 변경될 소지가 있다.

이와 같은 예기치 않은 문제를 해결하기 위해서는 비공개로 유지해야 하는 객체나 배열에 대한 참조를 전달할 때 주의를 기울여야 하며 주어진 객체의 최상위 프로퍼티만을 복사하는 extend() 함수와 모든 중첩 프로퍼티를 재귀적으로 복사하는 extendDeep() 함수로 해결할 수 있다.

객체에 특권 메소드를 만드는 방법은 여러가지가 있지만 이 포스팅에서는 모듈화,모듈 패턴화를 알아보면서 어떤 방식으로 특권 메소드를 사용하는지 알아보도록 하자.



모듈화(캡슐화)

javascript
// 전역 스코프(gloval scope)
// 변수(멤버 변수)
// var sayHi = "안녕";

// 함수 영역
/*
 var moduleFunc = function() {
 // 함수 스코프(새로운 영역이 생성됨)
 };
 */

// 위의 멤버변수와 함수 영역을 하나의 코드블럭을 생성한다
// 즉, 위의 코드는 전역에 정의되어 있지만 멤버변수와 함수 영역을 전역 스코프와 상관없이 
// 하나의 새로운 범위를 생성,관리하기위해 모듈화시킨다

// 새로운 모듈화(캡슐화)
// 자가 실행 함수
(function(){

    var sayHi = "안녕";

    var moduleFunc = function() {
        return sayHi;
    };

    console.log(moduleFunc()); // 같은 스코프안에서 함수 호출함

}()); // 즉시 실행 함수

// 외부에서 호출
// console.log(sayHi); // Uncaught ReferenceError: sayHi is not defined
// console.log(moduleFunc()) // Uncaught ReferenceError: moduleFunc is not defined



모듈 패턴

더글라스 크록포드가 고안한 모듈 패턴은 싱글톤에서 같은 일을 한다.

싱글톤이란 인스턴스를 단 하나만 갖게 의도한 객체이다. 전통적으로 자바스크립트에서 싱글톤을 만들 때는 다음 코드와 같이 객체 리터럴 표기법을 사용한다.


javascript
var singleton = {
    name: value,
    method : function() {
        // 메소드 코드
    }
}



객체 리터럴과 비공개 멤버

생성자가 아닌 객체 리터럴로 비공개 멤버를 구현할 수 있다. 

객체 리터럴에서는 익명 즉시 실행함수를 추가하여 클로저를 만든다. 

모듈 패턴은 위 코드 형식에 따라 기본 싱글톤을 확장하여 고유멤버(private member)와 특권 메소드(privileged member)를 사용할 수가 있다.


javascript
var myobj;          // 이 변수에 객체를 할당한다.
(function(){
    // 비공개 멤버
    var name = "my, oh my";

    // 공개될 부분을 구현한다.
    // var를 사용하지 않는다.
    myobj = {
        //특권 메서드
        getName : function(){
            return name;
        }
    };
}());

myobj.getName();


또는 다음과 같이 구현할 수 있다.

javascript
var myobj = (function(){
    //비공개 멤버
    var name = "my, oh my";

    //공개될 부분을 구현한다.
    return {
        //특권 메서드
        getName : function(){
            return name;
        }
    };
}());

myobj.getName();



좀 더 코드를 작성하여 복습해 보자.

javascript
var myObj = function(){

    // 고유 멤버(private member)가 될 스코프
    var sayHi = "안녕하세요!!";
    var intCnt = 0;
    var hi = function() {
        intCnt += 1; // hi 함수를 호출할 때마다 1씩 증가하도록 함 
        return sayHi;
    };

    var cnt = function() {
        return intCnt; // closure를 통해서 증가된 intCnt값을 반환하도록 해준다.
    }


    // 특권/공용메소드와 프로퍼티 (공용 인터페이스)=>외부에 공개하기 위한 역할
    // 객체 리터럴을 함수 값으로 반환(반환되는 객체리터럴에는 공용이 될 프로퍼티와 메소드만 소유)
    return {
        getHi : function() {
            return sayHi; // 고유멤버에 접근이 가능(특권 메소드)
        },
        getHi2 : function() {
            return '반갑습니다~~!!';
        },
        getHi3 : hi,
        getCnt : intCnt, // intCnt를 직접 외부에 반환시켜주면 증가된 값이 출력되지 않는다.
        getCnt2 : cnt
    }
}();

// 모듈 패턴 외부에서 함수 호출이 가능한 것은 객체 리터럴을 반환값으로 넘겨줬기 때문이다
// 이렇게 외부에서 사용이 가능하다고 하여 공용 메소드라고 일컫는다.
console.log(myObj.getHi());
console.log(myObj.getHi2());
console.log(myObj.getHi3());
console.log(myObj.getCnt);
console.log(myObj.getCnt2());
console.log(myObj.getHi3());
console.log(myObj.getCnt2());


이렇게 모듈 패턴은 객체를 반환하는 익명 함수를 사용한다.

익명 함수 내부에서는 첫번째로 고유멤버인 함수의 매개변수,지역변수,함수 등을 정의하고 그 다음에는 객체 리터럴을 함수값으로 반환하는 것이다.

반환 된 객체 리터럴에는 공용적으로 사용될 프로퍼티나 메소드만 포함하게 되고 이 객체는 익명함수 내에서 정의되었기 때문에 공용메소드는 고유멤버에 접근하여 사용이 가능하다.

즉, 객체 리터럴이 싱글톤에 대한 공용 인터페이스를 정의하는 것이다.



프로토타입과 비공개 멤버

생성자를 사용하여 비공개 멤버를 만들 경우, 생성자를 호출하여 새로은 객체를 만들 때마다 비공개 멤버가 매번 재생성된다는 단점이 있다.

사실 생성자 내부에서 this 에 멤버를 추가하면 항상 이런 문제가 발생한다.

이러한 중복을 없애고 메모리를 절약하려면 공통 프로퍼티와 메서드를 생성자의 prototype 프로퍼티에 추가해야 합니다.

이렇게 하면 동일한 생성자로 생성한 모든 인스턴스가 공통된 부분을 공유하게 된다.

감춰진 비공개 멤버들도 모든 인스턴스가 함께 쓸 수 있다.

이를 위해서는 두 가지 패턴, 즉 생성자 함수 내부에 비공개 멤버를 만드는 패턴과 객체 리터럴로 비공개 멤버를 만드는 패턴을 함께 써야 한다.

왜냐하면 prototype 프로퍼티도 결국 객체라서 객체 리터럴로 생성할 수 있기 때문이다.

javascript
function Gadget() {
    // 비공개 멤버
    var name = 'iPod';

    // 공개 함수
    this.getName = function () {
        return name;
    }
}

Gadget.prototype = (function () {
    // 비공개 멤버
    var browser = 'Mobile Webkit';

    // 공개된 프로토타입 멤버
    return {
        getBrowser : function () {
            return browser;
        }
    }

})();

var toy = new Gadget();
console.log(toy.getName()); // 객체 인스턴스의 특권 메서드
console.log(toy.getBrowser()); // 프로토타입의 특권 메서드



비공개 함수를 공개 메서드로 노출시키는 방법

노출 패턴(revelation pattern) 은 비공개 메서드를 구현하면서 동시에 공개 메서드로도 노출하는 것을 말합니다.

객체의 모든 기능이 객체가 수행하는 작업에 필수불가결한 것들이라서 최대한 보호가 필요한데, 동시에 이 기능들의 유용성 때문에 공개적인 접근도 허용하고 싶은 경우가 있을 수 있습니다.

노출 패턴은 이런한 경우에 유용하게 쓸 수 있습니다.

메서드가 공개되어 있다는 것은 결국 이 메서드가 위험에 노출되어 있다는 말과도 같습니다.

공개 API 사용자가 어저면 본의 아니게 메서드를 수정할 수 있기 때문입니다.

ECMAScript 5에서는 객체를 고정(freeze)시킬 수 있는 선택자가 있지만, 이전 버전에서는 그렇지 않습니다.

이제 노출 패턴에 대해 알아보도록 하자.

이 용어는 크리스천 헤일먼(Christian Heilmann)이 만들어냈으며 처음에는 '모듈 노출 패턴(revealing module pattern)'이라고 했습니다.

먼저 예제를 살펴보도록 합니다.

이 예제는 객체 리터럴 안에서 비공개 멤버를 만드는 패턴에 기반하고 있습니다.

javascript
var arr;

(function () {

    var astr = '[object Array]';
    var toString = Object.prototype.toString;

    function isArray(a) {
        return toString.call(a) === astr;
    }

    function indexOf(haystack, needle) {
        var i   = 0,
            max = haystack.length;
        for (; i < max; i += 1) {
            if (haystack[i]  === needle ) {
                return i;
            }
        }
        return -1;
    }

    arr = {
        isArray : isArray,
        indexOf : indexOf,
        inArray : indexOf
    }

})();

여기에는 비공개 변수 두 개와 비공개 함수 두 개인 isArray()indexOf() 가 존재하고 있습니다.

즉시 실행 함수의 마지막 부분을 보면, 공개적인 접근을 허용해도 괜찮겠다고 결정한 기능들이 arr 객체에 채울 수 있습니다.

비공개 함수 indexOf() 는 ECMAScript 5 식의 이름인 indexOfPHP 에서 영향을 받은 이름이 inArray 라는 두 개의 이름으로 노출되어 있습니다.

위에서 정의한 코드를 바탕으로 새로운 arr 객체를 태스트해보도록 합니다.

javascript
arr.isArray([1, 2]); // true 가 기록
arr.isArray({0: 1}); // false 가 기록
arr.indexOf(['a', 'b', 's'], 's'); // 2 가 기록
arr.inArray(['a', 'b', 's'], 's'); // 2 가 기록

이제 공개된 메서드인 indexOf() 에 예기치 못한 일이 일어나더라도, 비공개 함수인 indexOf() 는 안전하게 보호되기 때문에 inArray() 는 계속해서 잘 동작할 것입니다.

javascript
arr.indexOf = null;
arr.inArray(['a', 'b', 'z', 'z']); // 2 가 기록



모듈 패턴 정리(Module Pattern)

모듈 패턴은 늘어나는 코드를 구조화하고 정리하는데 도움이 되기 때문에 널리 쓰입니다.

다른 언어와는 달리 자바스크립트에는 패키지를 위한 별도의 문법이 없습니다.

하지만 모듈 패턴을 사용하면 개별적인 코드를 느슨하게 결합시킬 수 있습니다.

따라서 각 기능들을 블랙박스처럼 다루면서도 소프트웨어 개발 중에(끊임없이 변하는) 요구사항에 따라 기능을 추가하거나 교체하거나 삭제하는 것도 자유롭게 할수 있습니다.


모듈 패턴은 지금까지 알아본 다음의 패턴들 여러 개를 조합한 것이다!!

  • 네임스페이스 패턴
  • 즉시 실행함수
  • 비공개 멤버와 특권멤버
  • 의존관계 선언


첫 단계는 네임스페이스를 설정하는 것입니다.

위 상단 글에서 알아본 namespace() 함수를 사용해, 유용한 배열 메서드를 제공하는 유틸리티 모듈 예제를 만들어 봅니다.

javascript
MYAPP.namespace('MYAPP.utilities.array');

그 다음 단계는 모듈을 정의하는 것입니다. 공개 여부를 제한해야 한다면 즉시 실행 함수를 사용해 비공개 유효범위를 만들면 됩니다.

즉시 실행 함수는 모듈이 될 객체를 반환합니다.

이 객체에는 모듈 사용자에게 제공할 공개 인터페이스가 담기게 될 것입니다.

javascript
MYAPP.utilities.array = function () {
    return {
        inArray : function (needle, haystack) {
            // ...
        },
        isArray : function (a) {
            // ...
        }
    }
}();

즉시 실행함수의 비공개 유효범위를 사용하면, 비공개 프로퍼티와 메서드를 마음껏 선언할 수 있습니다.

모듈에 의존 관계가 있다면 즉시 실행 함수 상단에서 정의합니다.

변수를 선언한 다음에는 필요에 따라 모듈을 초기화하는 데 필요한 일회성 초기화 코드를 두어도 좋습니다.

즉시 실행 함수가 반환하는 최종 결과는 모듈의 공개 API 담은 객체입니다.

javascript
MYAPP.namespace('MYAPP.utilities.array');

MYAPP.utilities.array = function () {

    // 의존 관계 선언
    var uobj  = MYAPP.utilities.object,
        ulang = MYAPP.utilities.lang;

    // 비공개 프로퍼티
    var array_string = '[object Array]',
        ops          = Object.prototype.toString;

    // 비공개 메서들
    // ...

    // 필요하다면 일회성 초기화 절차를 실행한다.
    // ...

    // 공개 API
    return {
        inArray : function (needle, haystack) {
            for (var i = 0, max = haystack.length; i < max; i += 1) {
                if (haystack[i] === needle) {
                    return true;
                }
            }
        },
        isArray : function (a) {
            return ops.call(a) === array_string;
        }
        // ... 더 필요한 메서드와 프로퍼티를 여기 추가할 수 있다.
    }
}();

모듈 패턴은 특히 점점 늘어만 가는 코드를 정리할 때 널리 사용되면 매우 추천하는 방법입니다.


다음 포스팅에서는 계속해서 모듈 노출 패턴, 생성자를 생성하는 모듈, 그리고 샌드박스 패턴 등에 대해 알아보도록 하겠습니다.





Jaehee's WebClub