본문으로 바로가기

모듈 노출 패턴, 생성자를 생성하는 모듈 그리고 샌드박스 패턴

이 포스팅에서는 전 포스팅의 모듈 패턴 #2 에서 알아본 내용 이외의 패턴들에 대해 알아봅니다.




모듈 노출 패턴

전 포스팅에서 비공개 멤버와 관련된 패턴을 살펴보면서 이미 노출 패턴을 다룬 바 있습니다.

모듈 패턴도 비슷한 방식으로 작성할 수 있습니다.

즉, 모든 메서드를 비공개 상태로 유지하고, 최종적으로 공개 API 를 갖출 때 공개할 메서드만 골라서 노출하는 것입니다.

javascript
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;

};

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

MYAPP.utilities.array = (function () {
	
    // 비공개 프로퍼티
    var arr_string = '[object Array]',
        ops        = Object.prototype.toString;
    
    // 비공개 메서드
    var inArray = function (haystack, needle) {
	    for (var i = 0, max = haystack.length; i < max; i += 1) {
		    if (haystack[i] === needle) {
		        return i;
		    }
	    }
	    return -1;
    };
    
    var isArray = function (a) {
        return ops.call(a) === arr_string;
    };
    
    // 공개 API 노출
    return {
        isArray : isArray,
	    inArray : inArray
    }
    
}());

console.log(MYAPP.utilities.array.inArray(['1',3], 3)); // 1 이 기록
console.log(MYAPP.utilities.array.isArray({'a': 1})); // false 가 기록



생성자를 생성하는 모듈

앞선 예제에서 MYAPP.utilities.array 라는 객체를 만들었습니다.

하지만 생성자 함수를 사용해 객체를 만드는 것이 더 편할 때도 있습니다.

모듈 패턴을 사용하면서도 이렇게 할 수 있습니다.

모듈을 감싼 즉시 실행 함수가 마지막에 객체가 아니라 함수를 반환하게 하면 됩니다.

다음 모듈 패턴 예제는 생성자 함수인 MYAPP.utilities.Array 를 반환합니다.


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

MYAPP.utilities.Array = (function(){
	
	// 의존 관계 선언
	var uobj  = MYAPP.utilities.object,
	    ulang = MYAPP.utilities.lang,
	    // 비공개 프로퍼티와 메서드를 선언한 후 ...
	    Constr;
	// var 선언을 마친다.
	
	// 필요하다면 일회성 초기화 절차를 실행한다.
	// ...
	
	// 공개 API - 생성자 함수
	Constr = function (o) {
		this.elements = this.toArray(o);
	};
	
	// 공개 API - 프로토타입
	Constr.prototype = {
		consturctor : MYAPP.utilities.Array,
		version : '2.0',
		toArray : function (obj) {
			for (var i = 0, a = [], len = obj.length; i < len; i += 1) {
				a[i] = obj[i];
			}
			return a;
		}
	};
	
	// 생성자 함수를 반환한다.
	// 이 함수가 새로운 네임스페이스에 할당될 것이다.
	return Constr;
	
}());

// 위 생성자 함수는 다음과 같이 사용할 수 있다.
var arr = new MYAPP.utilities.Array({});

console.log(arr.version);


모듈에 전역변수 가져오기

이 패턴의 흔한 변형 패턴으로는 모듈을 감싼 즉시 실행 함수에 인자를 전달하는 형태가 있습니다.

어떠한 값이라도 가능하지만 보통 전역 변수에 대한 참조 또는 전역 객체 자체를 전달합니다.

이렇게 전역 변수를 전달하면 즉시 실행 함수 내에서 지역 변수로 사용할 수 있게 되기 때문에 탐색 작업이 좀더 빨리집니다.

javascript
MYAPP.utilities.module = (function (app, global) {
	
	// 전역 객체에 대한 참조와
	// 전역 애플리케이션 네임스페이스 객체에 대한 참조가 지역 변수화 된다.
	
}(MYAPP, this));



샌드박스 패턴

샌드박스 패턴은 네임스페이스 패턴의 다음과 같은 단점을 해결합니다.

  • 애플리케이션 전역 객체가 단 하나의 전역 변수에 의존한다. 따라서 네임스페이스 패턴으로는 동일한 애플리케이션이나 라이브러리의 두 가지 버전을 한 페이지에서 실행시키는 것이 불가능하다. 여러 버전들이 모두 이를테면 MYAPP 이 라는 동일한 전역 변수명을 쓰기 때문이다.
  • MYAPP.utilities.array 와 같이 점으로 연결된 긴 이름을 써야 하고 런타임에는 탐색 작업을 거쳐야한다.

이름을 보고 짐작할 수 있듯이 샌드박스 패턴은 어떤 모듈이 다른 모듈과 그 모듈의 샌드박스에 영향을 미치지 않고 동작할 수 있는 환경을 제공하는 것입니다.


전역 생성자

네임스페이스 패턴에서는 전역 객체가 하나입니다.

샌드박스 패턴의 유일한 전역은 생성자입니다.

이것을 Sandbox() 라고 해보자. 이 생성자를 통해 객체들을 생성할 것입니다.

그리고 이 생성자에 콜백 함수를 전달해 해당 코드를 샌드박스 내부 환경으로 격리시킬 것입니다.


샌드박스 사용법은 다음과 같습니다.

javascript
new Sandbox(function (box) {
	// 여기에 코드가 들어간다...
})

box 객체는 네임스페이스 패턴에서의 MYAPP 과 같은 것입니다.

코드가 동작하는데 필요한 모든 라이브러리 기능들이 여기에 들어갑니다.


이 패턴에 두 가지를 추가해 보도록 하자.

  • new 를 강제하는 패턴을 활용하여 객체를 생성할 때 new 를 쓰지 않아도 되게 만든다.
  • Sandbox() 생성자가 선택적인 인자를 하나 이상 받을 수 있게 한다. 이 인자들은 객체를 생성하는 데 필요한 모듈의 이름을 지정한다. 우리는 코드의 모듈화를 지향하고 있으므로 Sandbox() 가 제공하는 기능 대부분이 실제로는 모듈 안에 담겨지게 될 것이다.


이제 객체를 초기화하는 코드가 어떤 모습인지 예제를 보도록 합니다.

다음과 같이 new 를 쓰지 않고도, 가상의 모듈 'ajax' 와 'event' 를 사용하는 객체를 만들 수 있습니다.

javascript
Sandbox(['ajax','event'], function (box) {
	// console.log(box);
})


다음 예제는 앞선 예제와 비슷하지만 모듈 이름을 개별적인 인자로 전달합니다.

javascript
Sandbox('ajax','dom', function (box) {
	// console.log(box);
})


"쓸 수 있는 모듈을 모두 사용한다" 는 의미로 와일드카드 * 인자를 사용하면 어떨까?

편의를 위해 모듈명을 누락시키면 샌드박스가 자동으로 * 를 가정하도록 해보자.

그렇다면 모든 모듈을 사용하는 방법으로 다음 두 가지가 가능하게 될 것입니다.

javascript
Sandbox('*', function (box) {
	// console.log(box);
});

Sandbox(function (box) {
	// console.log(box);
});


마지막으로 샌드박스 객체의 인스턴스를 여러 개 만드는 예제를 살펴보도록 하자.

심지어 한 인스턴스 내부에 다른 인스턴스를 중첩시킬 수도 있습니다.

이 때도 두 인스턴스 간의 간섭 현상은 일어나지 않습니다.

javascript
Sandbox('dom', 'event', function (box) {
	
	// dom 과 event 를 가지고 작업하는 코드
	Sandbox('ajax', function () {
		// 샌드박스된 box 객체를 또 하나 만든다.
		// 이 "box" 객체는 바깥쪽 함수의 "box" 객체와는 다르다.
		
		// ajax 를 사용하는 작업 완료
		
	});
	
	// 더 이상 ajax 모듈의 흔적은 찾아볼 수 없다.
	
});

이 예제들에서 볼 수 있듯이, 샌드박스 패턴을 사용하면 콜백 함수로 코드를 감싸기 때문에 전역 네임스페이스를 보호할 수 있습니다.

필요하다면 함수가 곧 객체라는 사실을 활용하여 Sandbox() 생성자의 "스태틱" 프로퍼티에 데이터를 저장할 수도 있습니다.

또 원하는 유형별로 모듈의 인스턴스를 여러 개 만들 수도 있습니다.

이 인스턴스들은 각각 독립적으로 동작하게 됩니다.

그럼 이제 이 모든 기능을 지원하는 Sandbox() 생성자와 그 모듈을 구현하는 방법을 살펴보도록 하자.


모듈 추가하기

실제 생성자를 구현하기 전에 모듈을 어떻게 추가할 수 있는지부터 살펴보도록 합니다.

Sandbox() 생성자 함수 역시 객체이므로 modules 이라는 프로퍼티를 추가할 수 있습니다.

이 프로퍼티는 키-값의 쌍을 담은 객체로, 모듈의 이름이 되고 각 모듈을 구현한 함수가 값이 되도록 할 것입니다.

javascript
Sandbox.modules = {};

Sandbox.modules.dom = function (box) {
	box.getElement = function () {
	};
	box.getStyle = function () {
	};
	box.foo = 'bar';
};

Sandbox.modules.event = function (box) {
	// 필요에 따라 다음과 같이 Sandbox 프로토타입에 접근할 수 있다.
	// box.constructor.prototype.m = 'mmm';
	
	box.attachEvent = function () {
	};
	box.detachEvent = function () {
	};
	
};

Sandbox.modules.ajax = function (box) {
	box.makeRequest = function () {
	};
	box.getResponse = function () {
	};
};

위 예제에서는 dom, event, ajax 라는 모듈을 추가했습니다.

모든 라이브러리와 복잡한 웹 애플리케이션에서 흔히 사용되는 기능들입니다.

각 모듈을 구현하는 함수들이 현재의 인스턴스 box 를 인자로 받아들인 다음 이 인스턴스에 프로퍼티와 메서드를 추가하게 됩니다.


생성자 구현

이제 Sandbox() 생성자를 구현해 보도록 합니다.(이 생성자의 이름은 여러분의 라이브러리나 애플리케이션에 맞게 바꾸어도 좋다.)

javascript
function Sandbox() {
	// arguments 를 배열로 바꾼다.
	var args = Array.prototype.slice.call(arguments);
	
	// 마지막 인자는 콜백 함수다.
	var callback = args.pop();
	
	// 모듈은 배열로 전달될 수도 있고, 개별 인자로 전달될 수도 있다.
	var modules = (args[0] && typeof args[0] === 'string') ? args : args[0];
	var i;
	
	// 함수가 생성자로 호출되도록 보장한다.(new 를 강제하는 패턴)
	if ((!this instanceof Sandbox)) {
		return new Sandbox(modules, callback);
	}
	
	// this 에 필요한 프로퍼티를 추가한다.
	this.a = 1;
	this.b = 2;
	
	// 코어 'this' 객체에 모듈을 추가한다.
	// 모듈이 없거나 '*' 이면 사용 가능한 모든 모듈을 사용한다는 의미이다.
	if (!modules || modules === '*' || modules[0] === '*') {
		modules = [];
		for (i in Sandbox.modules) {
			if (Sandbox.modules.hasOwnProperty(i)) {
				modules.push(i);
			}
		}
	}
	
	// 필요한 모듈들을 초기화한다.
	for (i = 0; i < modules.length; i += 1) {
		Sandbox.modules[modules[i]](this);
	}
	
	// 콜백 함수를 호출한다.
	callback(this);
	
}

// 필요한 프로토타입 프로퍼티들을 추가한다.
Sandbox.prototype = {
	name: 'My Application',
	version : '1.0',
	getName : function () {
		return this.name;
	}
};

위 구현에서 핵심적인 사항들은 다음과 같습니다.

  • thisSandbox 의 인스턴스인지 확인하고, 그렇지 않으면 (즉, Sandbox() 가 new 없이 호출되었다면) 함수를 생성자로 호출한다.
  • 생성자 내부에서 this 에 프로퍼티를 추가한다. 생성자의 프로토타입에도 프로퍼티를 추가할 수 있다.
  • 필요한 모듈은 배열로도, 개별적인 인자로도 전달할 수 있고, * 와일드카드를 사용하거나, 쓸 수 있는 모듈을 모드 쓰겠다는 의미로 생략할 수도 있다. 이 예제에서는 필요한 기능을 다른 파일로부터 로딩하는 것까지는 구현하지 않았지만, 이러한 선택지도 확실히 고려해보아야 한다.
  • 필요한 모듈을 모두 파악한 다음에는 각 모듈을 초기화한다. 다시 말해 각 모듈을 구현한 함수를 호출한다.
  • 생성자의 마지막 인자는 콜백 함수이다. 이 콜백 함수는 맨 마지막에 호출되며, 새로 생성된 인스턴스가 인자로 전달된다. 이 콜백 함수가 실제 사용자의 샌드박스이며 필요한 기능을 모두 갖춘 상태에서 box 객체를 전달받게 된다.




Jaehee's WebClub