UI Laboratory

UI 개발을 위한 레퍼런스

INDEX

  1. 생성자를 통해 생성된 객체를 식별하는 ID를 구현하기 위한 특권 메서드
  2. 상수를 정의하는 범용 constant 객체
  3. 자식 객체가 부모 생성자의 프로퍼티의 복사본을 가져오는 동시에 부모의 프로토타입 멤버에 추가된 기능들에 대한 참조를
    상속받기 위한 패턴은?
  4. 프록시 생성자(임시 생성자)를 이용하여 부모 생성자의 this에 추가한 멤버는 상속하지 않고 부모의 프로토타입만을 상속하는 상속 패턴은?

개요


패턴

소프트웨어 개발에서의 패턴이란 일반적인 문제에 대한 해결책을 가리킨다. 모범적인 관행, 쓰임새에 맞게 추상화된 원리, 어떤 범주의 문제를 해결하는 템플릿을 말한다.

자바스크립트의 개념

| 객체지향

자바스크립트는 객체지향 언어이다. 함수 또한 객체다. 함수도 프로퍼티와 메서드를 가진다. 사실 자바스크립트에서 변수를 선언한다면, 이미 객체를 다루는 것이다. 첫째로 변수는 자동으로, 활성화 객체라 불리는 내부적인 객체의 프로퍼티가 된다. (전역 변수인 경우에는 전역 객체의 프로퍼티가 된다.) 둘째로 이 변수는 자신만의 프로퍼티를 가지기 때문에 실제로 객체와 비슷하다. 변수의 프로퍼티를 어트리뷰트(attribute)라고 한다.

객체란 무엇일까? 객체는 단지 이름이 지정된 프로퍼티의 모음이며, 키-값 쌍으로 이뤄진 목록이다. 객체의 프로퍼티가 함수일 경우 이를 메서드라고 부른다.

기억해야할 두가지 주요 객체 타입이 있다.

네이티브 객체는 내장 객체(예를 들면, Array나 Date) 또는 사용자 정의 객체(var o = {};)로 분류된다.

호스트 객체의 예로는 window 객체나 모든 DOM 객체를 들 수 있다.

| 클래스가 없다

자바스크립트에는 클래스가 없다. 오직 객체만을 다룬다. 자바스크립트에서는 빈 객체를 필요한 시점에 생성하고 그 이후에 필요한 멤버를 추가할 수 있다. 객체에 원시 데이터 타입이나 함수, 다른 객체를 추가하여 객체의 프로퍼티를 구성한다. 빈(blank) 객체는 사실 완전히 비어있는 것이 아니다. 빈객체는 몇몇 내장 프로퍼티를 이미 가지고 있지만 자신이 직접 소유한 프로퍼티가 없을 뿐이다.

| 프로토타입

자바스크립트에서도 상속을 할 수 있다. 상속은 다양한 방법으로 구현할 수 있는데, 주로 프로토타입(prototype)을 사용한다. 프로토타입은 하나의 객체이며, 사용자가 생성한 모든 함수는 새로운 빈 객체를 가리키는 prototype 프로퍼티를 가진다. 사용자는 이 빈 객체에 멤버를 추가할 수 있고, 상속을 통해 다른 객체가 이 객체의 프로퍼티를 자기 것처럼 쓰게 만들 수도 있다.

기초


전역변수 최소화

자바스크립트는 함수를 사용하여 유효범위를 관리한다. 전역변수란 어떤 함수에도 속하지 않고 선언되거나, 아예 선언되지 않은 채로 사용되는 변수를 가리킨다.

모든 자바스크립트 샐행 환경에는 전역 객체(global object)가 존재한다. 어떤 함수에도 속하지 않은 상태에서 this를 사용하면 전역 객체에 접근하게 된다. 전역 변수를 생성하는 것은, 이 전역 객체의 프로퍼티를 만드는 것과 같다. 편의상 브라우저에는 전역 객체에 window라는 부가적인 프로퍼티가 존재하며 전역 객체 자신을 가리킨다. 다음 코드는 브라우저 환경에서 전역변수를 생성하고 이 변수에 접근하는 방법을 보여준다.

			myglobal = "hello";					// 안티패턴
			console.log(myglobal);				// "hello"
			console.log(window.myglobal);		// "hello"
			console.log(window["myglobal"]);	// "hello"
			console.log(this.myglobal);			// "hello"
		

| 전역변수의 문제점

전역변수의 문제점은 자바스크립트 애플리케이션이나 웹페이지 내 모든 코드 사이에서 공유된다는 점이다. 즉, 애플리케이션 내의 다른 영역에서 목적이 다른 전역 변수를 동일한 이름으로 정의할 경우 서로 덮어쓰게 된다. 따라서 다른 스크립트들과 한 페이지 안에서 사이좋게 공존하려면, 전역변수를 최소한으로 사용해야 한다. 그리고 무엇보다도 중요한 패턴은, 변수를 선언할 때 항상 var를 사용하는 것이다.

자바스크립트는 선언하지 않고 사용한 변수는 자동으로 전역 객체의 프로퍼티가 되어, 명시적으로 선언된 전역변수와 별 차이없이 사용할 수 있다.

			function sum(x,y){
				//안티패턴 : 암묵적 전역
				result = x + y;
				return result;
			}
		

이 코드에서 result는 선언되지 않은 상태로 사용되엇다. 언제나 var를 사용하여 변수를 선언해야 한다 위의 sum() 함수를 개선하면 다음과 같다.

			function sum(x,y){
				var result = x + y;
				return result;
			}
		

암묵적 전역을 생성하는 또 다른 안티패턴은 하나의 var 선언에서 연쇄적으로 할당을 사용하는 것이다. 다음 코드에서 a는 지역변수지만 b는 전역변수가 된다.

			//안티패턴, 사용하지 말 것.
			function foo(){
				var a = b =0;
				...
			}
		

변수를 미리 선언해두면 의도치 않은 전역변수가 생성되는 일은 없다.

			function foo(){
				var a, b;
				...
				a = b =0;	// 모두 지역변수
				...
			}
		

| var 선언을 빼먹었을 때의 부작용

암묵적 전역 변수와 명시적으로 선언된 변수 사이에 존재하는 또 하나의 작은 차이점은 delete 연산자를 사용하여 이 변수의 정의를 취소할 수 있는지 여부다.

이는 암묵적 전역변수가 엄밀히 말하면 변수가 아니라 전역 객체의 프로퍼티라는 사실을 보여준다. 프로퍼티는 delete 연산자로 삭제할 수 있지만 변수는 삭제할 수 없다.

| 전역 객체에 대한 접근

브라우저에서는 코드 어느 곳에서든 window 속성을 통해 전역객체에 접근할 수 있다. window라는 식별자를 직접 사용하지 않고 전역객체에 접근하고 싶다면, 함수 유효범위 안에서 다음과 같이 정의하면 된다.

			var global = (function(){
				return this;
			}());
		

이렇게 하면 항상 전역객체를 얻을 수 있다. 함수를 new와 생성자를 사용해 호출하지 않고 그냥 함수로 호출한 경우, 함수 안에서 this는 항상 전역객체를 가리킨다.

| 단일 var 패턴

함수 상단에서 var 선언을 하나만 쓰고 여러개의 변수를 쉼표로 연결하여 선언하는 것은 유용하다. 또한 변수를 선언할 때 초기 값을 주어 초기화하는 것 역시 좋은 습관이다. 또한 DOM 참조를 다루는 것도 좋은 예이다.

			function func(){
				var a = 1,
					b = 2,
					sum = a+b,
					myobject = {},
					i,
					j;

				....
			}

			function updateElement(){
				var el = document.getElementById('result'),
					style = el.style;

				...
			}
		

| 호이스팅(hoisting): 분산된 var 선언의 문제점

자바스크립트에서는 함수 내 여기저기서 여러 개의 var 선언을 사용할 수 있지만, 실제로는 모두 함수 상단에서 변수가 선언된것과 동일하게 동작한다. 이러한 동작 방식을 '호이스팅(hoisting, 끌어올리기)'이라고 한다. 때문에 함수 안에서 변수를 사용한 다음에 선언하면 로직상의 오류를 일으킬 수 있다.

			// 안티패턴
			myname = "global";	//전역변수
			function func(){
				alert(myname);	//"undefined"
				var myname = "local";
				alert(myname);	// "local"
			}
			func();
		

이 예제를 보면 첫번째 alert()의 결과로 'global'이 출력되고 두번째에는 'local'이 출력될 거라고 예상하기 쉽다. 첫번째 alert이 호출되는 시점에는 myname이 아직 선언되지 않았으므로 전역변수인 myname을 바라볼 것이라고 예상해야 맞을 것 같다. 그러나 실제 동작은 그렇지 않다. 첫번째 alert은 'undefined'를 출력한다. myname이 이 함수의 지역 변수로 선언되었다고 간주하기 때문이다. 선언문 자체는 그 다음에 나온다 해도 말이다. 모든 변수 선언문은 함수 상단으로 끌어올려진다. 따라서 이러한 혼란을 피하기 위해서는 사용할 변수를 모두 맨 첫줄에서 선언하는 것이 좋다.

앞의 코드는 다음과 같다.

			myname = "global";	//전역변수
			function func(){
				var myname;		// var myname = undefined;와 동일하다
				alert(myname);	//"undefined"
				myname = "local";
				alert(myname);	// "local"
			}
			func();
		

for 루프

for 루프 안에서는 보통 배열이나 arguments, HTMLCollection 등 배열과 비슷한 객체를 순회한다.

			//최적화되지 않은 루프
			for(var i=0;i<myarray.length;i++){
				//myarray[i]를 다루는 코드
			}
		

이 패턴의 문제점은 루프 순회시마다 배열의 length에 접근한다는 점이다. myarray가 배열이 아니라 HTMLCollection이라면 이 때문에 코드가 느려질 수 있다. 즉, 콜렉션의 length 속성에 접근할 때마다 실제 DOM에 질의를 요청하는 것과 같으며, DOM접근은 일반적으로 비용이 크다.

for 루프를 좀더 최적화하기 위한 방법은 배열(또는 콜렉션)의 length를 캐시하는 것이다.

			//좀 더 최적화한 루프
			for(var i = 0, max = myarray.length; i < max; i++){
				//myarray[i]를 다루는 코드
			}
		

이렇게 하면 length 값을 한번만 구하고, 루프를 도는 동안 이값을 사용하게 된다. HTMLCollection을 순회할 때 length를 캐시하면, 사파리 3에서 2배, IE7에서는 190배에 이르는 속도가 향상된다.

for문에는 두가지 변형 패턴이 있으며 다음과 같은 미세 최적화를 시도한다.

  1. 첫번째 변형 패턴은 다음과 같다.

    					var i, myarray = [];
    
    					for(i = myarray.length; i--;){
    						//myarray[i]를 다루는 코드
    					}
    				
  2. 두번째 변형 패턴은 while 루프를 사용한다.

    					var myarray = [],
    						i = myarray.length;
    
    					while(i--){
    						//myarray[i]를 다루는 코드
    					}
    				

이러한 미세 최적화는 성능이 결정적인 요소가 되는 작업에서만 차이가 두드러진다.

for-in 루프

for-in 루프는 배열이 아닌 객체를 순회할 때만 사용해야 한다.

객체의 프로퍼티를 순회할 때는 프로토타입 체인을 따라 상속되는 프로퍼티들을 걸러내기 위해 hasOwnProperty() 메서드를 사용해야 한다.

			//객체
			var man = {
				hands : 2,
				legs : 2,
				heads : 1
			}

			//모든 객체에 메서드 추가
			if (typeof Object.prototype.clone === "undefined"){
				Object.prototype.clone = function(){};
			}
		

위 예제는 객체 리터럴을 사용하여 man이라는 이름의 간단한 객체를 정의하고 어디선가 Object 프로토타입에 clone()이라는 이름의 메서드를 편의상 추가했다. 프로토타입 체인의 변경 사항은 실시간으로 반영되기 때문에, 자동적으로 모든 객체가 이 새로운 메서드를 사용할 수 있다. man을 열거할 때 clone() 메서드가 나오지 않게 하려면 프로토타입 프로퍼티를 걸러내기 위해 hasOwnProperty()를 호출해야 한다. 이렇게 걸러내지 않으면 clone()이 나오게 되는데, 대부분의 경우에 이러한 동작 방식은 바람직하지 않다.

			//for-in 루프
			for(var i in man){
				if (man.hasOwnProperty(i)){		//프로토타입 프로퍼티를 걸러낸다.
					console.log(i, ":", man[i]);
				}
			}

			/*콘솔에 출력되는 결과
			hands : 2
			legs : 2
			heads : 1
			*/
		
			//안티패턴 - hasOwnProperty()를 확인하지 않은 for-in 루프
			for(var i in man){
				console.log(i, ":", man[i]);
			}

			/*콘솔에 출력되는 결과
			hands : 2
			legs : 2
			heads : 1
			clone : function()
			*/
		

Object.prototype에서 hasOwnProperty()를 호출하는 것도 또 하나의 패턴이다.

			for(var i in man){
				if (Object.prototype.hasOwnProperty.call(man, i)){		//프로토타입 프로퍼티를 걸러낸다.
					console.log(i, ":", man[i]);
				}
			}
		

프로퍼티 탐색이 Object 까지 멀리 거슬러 올라가지 않게 하려면, 지역변수를 사용하여 이 메서드를 '캐시'하면 된다.

			var i,
				hasOwn = Object.prototype.hasOwnProperty;
			for(i in man){
				if (hasOwn.call(man, i)){		//프로토타입 프로퍼티를 걸러낸다.
					console.log(i, ":", man[i]);
				}
			}
		

엄밀히 말하면 hasOwnProperty()를 사용하지 않았다고 해서 에러가 발생하지는 않는다. 그러나 객체와 객체 프로토타입 체인의 내용을 보장할 수 없다면, 그냥 hasOwnProperty() 확인을 추가하는 편이 좀더 안전하다.

내장 생성자 프로토타입 확장하기 / 확장하지 않기

Object(), Array(), Function()과 같은 내장 생성자의 프로토타입을 확장하는 것은 매력적이나 코드의 지속성을 심각하게 저해할 수 있는 만큼 확장하지 않는 것이 최선이다. 하지만 해당 기능이 ECMAScript의 향후 버전에 구현될 예정이고 아직 브라우저에 내장되지 않은 메서드라면 추가할 수 있다.

			if (typeof Object.prototype.myMethod !== "function"){
				Object.prototype.myMethod = function(){
					...
				};
			}
		

switch 패턴

다음은 가독성과 견고성을 향상시킨 switch문이다.

			var inspect_me = 0,
				result = '';

			switch (inspect_me) {
			case 0:
				result = "zero";
				break;
			case 1:
				result = "one";
				break;
			default:
				result = "unknown";
			}
		

암묵적 타입 캐스팅 피하기

자바스크립트는 변수를 비교할 때 암묵적으로 타입캐스팅을 실행한다. 때문에 false == 0이나 "" == 0과 같은 비교가 true를 반환한다. 암묵적 타입캐스팅으로 인한 혼동을 막기 위해서는, 항상 표현식의 값과 타입을 모두 확인하는 ===(완전항등연산자)와 !==(완전비항등연산자) 연산자를 사용해야 한다.

			var zero = 0;
			if(zero === false){
				//zero는 0이고 false가 아니기 때문에 이 블록은 실행되지 않는다.
			}
		
			var zero = 0;

			//안티패턴
			if(zero == false){
				//이 블록은 실행된다.
			}
		

| eval() 피하기

이 함수는 임의의 문자열을 받아 자바스크립트 코드로 실행한다. 동적인 프로퍼티에 접근할 때는 대괄호 표기법이 더 간단하고 좋은 방법이다.

			//안티패턴
			var property = "name";
			alert(eval("obj." + property));
		
			//권장안
			var property = "name";
			alert(obj[property]);
		

eval() 사용은 보안 문제와도 관련된다. Ajax 요청으로 받아온 JSON 응답을 다룰 때 이런 안티패턴을 흔히 볼수 있다. 보안과 유효성을 보장하기 위해서는 브라우저와 내장 메서드를 사용하여 JSON 응답을 파싱하는 것이 좋다. JSON.parse()를 내장 지원하지 않는 브라우저에서는 JSON.org의 라이브러리를 사용할 수 있다.

또 하나는, setInterval()과 setTimeout() 그리고 Function() 생성자에 문자열을 넘기는 것도 eval()을 사용하는 것과 상당히 비슷하기 때문에, 역시 사용을 자제해야 한다. 자바스크립트가 전달받은 문자열을 프로그래밍 코드로 평가하여 실행하는 것은 마찬가지다.

			//안티패턴
			setTimeout("myFunc()", 1000);
			setTimeout("myFunc(1, 2, 3)", 1000);
		
			//권장안
			setTimeout(myFunc, 1000);
			setTimeout(function(){
				myFunc(1, 2, 3);
			}, 1000);
		

parseInt()를 통한 숫자 변환

parseInt()를 사용하면 문자열로 부터 숫자 값을 얻을 수 있다. 이 함수는 두번째 매개변수로 기수를 받는데, 생략하는 경우가 맍지만 그래서는 안된다. 파싱할 문자열이 0으로 시작할 경우 문제가 생길 수 있다. 폼 필드에 입력하는 날짜 부분이 이런 예다. 일관성 없고 예측을 벗어나는 결과를 피하려면 항상 기수 매개변수를 지정해 주어야 한다.

			var month = "06",
				year = "09";
			month = parseInt(month, 10);
			year = parseInt(year, 10);
		

위 예제에서 parseInt(year)와 같이 기수 매개변수를 생략하면 "09"는 8진수로 간주되고 기수가 8일 때 09는 유효하지 않은 수 이기 때문에 반환값은 0이 된다.

문자열을 숫자로 변환하는 또 다른 방법으로는 다음과 같은 것들이 있다.

			+ "08"	// 결과값은 8이다.
			Number("08")	// 8
		

이 방법들은 대체로 parseInt() 보다 빠르다. parseInt()는 단순히 변환만 하는 것이 아니라 이름이 뜻하는 바대로 파싱을 하기 때문이다. 그러나 입력값으로 "08 hello" 같은 값이 들어올 수 있다면 parseInt()를 사용해야 숫자를 얻을 수 있다. 다른 방법을 사용하면 NaN이 반환되면서 실패해버린다.

리터럴과 생성자


객체 리터럴

자바스크립트에서 '객체'라고 하면 단순히 이름-값 쌍의 해시 테이블을 생각하면 된다. 원시 데이터 타입과 객체 모두 값이 될수 있으며 함수도 값이 될수 있다. 이런 함수는 메서드라고 부른다. 빈 객체를 정의해 놓고 기능을 추가해 나갈 수도 있다. 객체 리터럴 표기법은 이처럼 필요에 따라 객체를 생성할 때 이상적이다. 그러나 반드시 빈 객체에서시작해야 하는 것은 아니며 생성 시점에 객체에 기능을 추가할 수 있다.

| 생성자 함수로 객체 생성하기

자바스크립트에도 자바와 같은 클래스 기반 객체 생성과 비슷한 문법을 가지는 생성자 함수가 존재한다. 객체를 생성할 때는 직접 만든 생성자 함수를 사용할 수도 있고 Object(), Date(), String()등 내장 생성자를 사용할 수도 있다.

다음 예제는 동일한 객체를 생성하는 두 가지 방법을 보여준다.

			// 첫번째 방법 - 리터럴 사용
			var car = {goes : "far"};

			// 두번째 방법 - 내장 생성자 사용
			// 이 방법은 안티패턴이다.
			var car = new Object();
			car.goes = "far";
		

보다시피 객체 리터럴 표기법의 명백한 이점은 더 짧다는 것이며 유효범위 판별 작업도 발생하지 않는다.

| 객체 생성자의 함정

Object() 생성자가 인자를 받을 경우 인자로 전달되는 값에 따라 생성자 함수가 다른 내장 생성자에 객체 생성을 위임할 수 있고, 따라서 기대한 것과는 다른 객체가 반환되기도 한다.

			// 모두 안티패턴이다.

			//빈 객체
			var o = new Object();
			console.log(o.constructor === Object);		//true

			//숫자 객체
			var o = new Object(1);
			console.log(o.constructor === Number);		//true

			//불린 객체
			var o = new Object(true);
			console.log(o.constructor === Boolean);		//true
		

Object() 생성자의 이 같은 동작 방식 때문에, 런타임에 결정하는 동적인 값이 생성자에 인자로 전달될 경우 예기치 않은 결과가 반환될 수 있다. 거듭 말하지만 결론적으로, new Object()를 사용하지 마라. 더 간단하고 안정적인 객체 리터럴을 사용하라.

사용자 정의 생성자 함수

객체 리터럴 패턴이나 내장 생성자 함수를 쓰지 않고, 직접 생성자 함수를 만들어 객체를 생성할 수도 있다.

			var adam = new Person("Adam");
			adam.say();		//"I am Adam"
		

이 패턴은 자바에서 Person이라는 클래스를 사용하여 객체를 생성하는 방식과 상당히 유사하다. 그러나 문법은 비슷해도 자바스크립트에는 클래스라는 것이 없으며 Person은 그저 보통의 함수일 뿐이다. 다음은 Person 생성자 함수를 정의한 예시다.

			var Person = function(name){
				this.name = name;
				this.say = function(){
					return "i am" + this.name;
				};
			}
		

new와 함께 생성자 함수를 호출하면 함수 안에서 다음과 같은 일이 일어난다.

즉 다음과 같다.

			var Person = function(name){
				//객체 리터럴로 새로운 객체를 생성한다.
				//var this = {};

				//프로퍼티와 메서드를 추가한다.
				this.name = name;
				this.say = function(){
					return "i am" + this.name;
				};

				//this를 반환한다.
				//return this;
			}
		

이 예제에서는 간단히 say()라는 메서드를 this에 추가했다. 결과적으로 new Person()을 호출할 때마다 메모리에 새로운 함수가 생성된다. say()라는 메서드는 인스턴스별로 달라지는게 아니므로 이런 방식은 명백히 비효율적이며 Person의 프로토타입에 추가하는 것이 더 낫다.

			Person.prototype.say = function(){
				return "I am " + this.name;
			};
		

메서드와 같이 재사용되는 멤버는 프로토타입에 추가해야 한다는 점을 기억해 두자.

| 생성자의 반환 값

생성자 함수를 new와 함께 호출하면 항상 객체가 반환된다. 기본값은 this로 참조되는 객체다. 생성자 함수 내에서 아무런 프로퍼티나 메서드를 추가하지 않았다면 '빈' 객체가 반환될 것이다. 함수 내에 return 문을 쓰지 않았더라도 생성자는 암묵적으로 this를 반환한다. 그러나 반환 값이 될 객체를 따로 정할 수도 있다.

			var Objectmaker = function(){
				this.name = "This is it";

				var that = {};
				that.name = "And that's that";
				return that;
			};

			var o = new Objectmaker;
			console.log(o.name);		// 
		

new를 강제하는 패턴

생성자란 new와 함께 호출될 뿐 여전히 별다를 것 없는 함수에 불과하다. 하지만 생성자를 호출할 때 new를 빼먹으면 문법오류나 런타임 에러가 발생하지는 않지만, 논리적인 오류가 생겨 예기치 못한 결과가 나올 수 있다. new를 빼먹으면 생성자 내부의 this가 전역 객체를 가리키게 된다. (브라우저에서라면 this가 window를 가리키게 된다.)

			//생성자
			function Waffle() {
				this.taste = "yummy";
			}

			//안티패턴
			var good_morning = Waffle();
			console.log(typeof good_morning);		//"undefined"
			console.log(window.taste);			//"yummy"
		

생성자함수가 new 없이 호출되어도 항상 동일하게 동작하도록 보장하는 방법을 써야 한다.

| 명명 규칙

가장 간단한 대안은 생성자 함수명의 첫글자를 대문자로 쓴다.

| that 사용

this에 모든 멤버를 추가하는 대신, that에 모든 멤버를 추가한 후 that을 반환하는 것이다.

			//생성자
			function Waffle() {
				var that = {};
				that.taste = "yummy";
				return that;
			}
		

간단한 객체라면 객체 리터럴을 통해 객체를 반환해도 된다.

			//생성자
			function Waffle() {
				return {
					taste : "yummy"
				};
		

어느 것을 사용해도, 호출 방법과 상관없이 항상 객체가 반환된다.

			var first = new Waffle(),
				second = Waffle();
			
			console.log(first.taste);	//"yummy"
			console.log(second.taste);	//"yummy"
		

이 패턴의 문제는 프로토타입과의 연결고리를 잃어버리게 된다는 점이다. 즉 Waffle() 프로토타입에 추가한 멤버를 객체에서 사용할 수 없다.

| 스스로를 호출하는 생성자

앞서 설명한 패턴의 문제점을 해결하고 인스턴스 객체에서 프로토타입의 프로퍼티들을 사용할 수 있게 하려면, 다음 접근 방법을 고려할 수 있다. 생성자 내부에서 this가 해당 생성자의 인스턴스인지를 확인하고, 그렇지 않은 경우 new와 함께 스스로를 재호출하는 것이다.

			//생성자
			function Waffle() {
				if (!(this instanceof Waffle)){
					return new Waffle();
				}
				this.taste = "yummy";
			}

			Waffle.prototype.wantAnother = true;

			var first = new Waffle(),
				second = Waffle();
			
			console.log(first.taste);	//"yummy"
			console.log(second.taste);	//"yummy"

			console.log(first.wantAnother);		// true
			console.log(second.wantAnother);	// true
		

인스턴스를 판별하는 또다른 범용적인 방법은 생성자 이름 대신 arguments.callee와 비교하는 것이다.

			if (!(this instanceof arguments.callee)){
				return new arguments.callee();
			}
		

이것은 모든 함수가 호출될 때 내부적으로 arguments라는 객체가 생성되며 arguments의 callee라는 프로퍼티는 호출된 함수를 가리킨다. 하지만 arguments.callee는 ES5의 스트릭트 모드에서는 허용되지 않는다는 점에 유의하여 향후의 사용은 제한하는 것이 좋다.

배열 리터럴

배열도 객체이며 내장 생성자인 Array()로 배열을 생성하는 것 보다는 배열 리터럴 표기법이 더 간단하고 장점이 많다.

			// 안티패턴
			var a = new Array("itsy", "bitsy", "spider");

			// 배열 리터럴
			var a = ["itsy", "bitsy", "spider"];

			console.log(typeof a);		// 배열도 객체이기 때문에 "object"가 출력된다.
			console.log(a.constructor === Array);		// true
		

| 배열 생성자의 특이성

new Array()를 멀리해야 하는 또다른 이유는 이 생성자가 품고 있는 함정을 피하기 위해서다. Array() 생성자에 숫자 하나를 전달할 경우, 이 값은 배열의 첫번째 원소 값이 되는 게 아니라 배열의 길이를 지정한다.

			// 한 개의 원소를 가지는 배열
			var a = [3];
			console.log(a.length);		// 1
			console.log(a[0]);		//3

			// 세개의 원소를 가지는 배열
			var a = new Array(3);
			console.log(a.length);		// 3
			console.log(typeof a[0]);		// "undefined"
		

런타임에 동적으로 배열을 생성할 경우 에러 발생을 피하려면 배열의 리터럴 표기법을 쓰는 것이 훨씬 안전하다.

그러나 Array() 생성자를 이용하면 255개의 공백문자로 이루어진 문자열을 반환할 수 있다.

			var white = new Array(256).join('');
		

| 배열인지 판별하는 방법

배열에 typeof 연산자를 사용하면 "object"가 반환된다.

			console.log(typeof [1, 2]);	// "object"
		

값이 실제로 배열인지 알아내야 하는 경우가 자주 있다. instanceof Array를 사용할 수도 있지만, 이 방법은 IE 일부 버전에서는 프레임간 사용시 올바르게 동작하지 않는다.

ES5에서는 Array.isArray() 라는 새로운 메서드가 정의되었다. 이 메서드는 인자가 배열이면 true를 반환한다.

			Array.isArray([]);		//true

			Array.isArray({
				length : 1,
				"0" : 1,
				slice : function(){}
			});			//false가 반환된다.
		

실행환경에서 이 메서드를 사용할 수 없는 경우에는 Object.prototype.toString() 메서드를 호출하여 판별할 수 있다. 배열에 toString을 호출하면 "[object Array]"라는 문자열을 반환하게 되어 있다. 객체일 경우에는 문자열 "[object Object]"가 반환될 것이다. 따라서 다음과 같이 배열 판별 메서드를 작성할 수 있다.

			if (typeof Array.isArray ==== "undefined"){
				Array.isArray = function(arg){
					return Object.prototype.toString.call(arg) === "[object Array]";
				};
			}
		

JSON

JSON은 자바스크립트 객체 표기법의 준말로, 데이터 전송 형식의 일종이다. JSON은 그저 배열과 객체 리터럴 표기법의 조합일 뿐이다.

			{"name" : "value", "some" : [1, 2, 3]}
		

JSON에서는 프로퍼티명을 따옴표로 감싸야 한다는 점이 객체 리터럴과의 유일한 문법적 차이다. JSON 문자열에는 함수나 정규식 리터럴을 사용할 수 없다.

| JSON 다루기

eval은 보안 문제가 있기 때문에 가능하면 JSON.parse()를 사용하는 것이 최선책이다. 이 메서드는 ES5 부터 언어에 포함되었고 최신 브라우저의 자바스크립트 엔진에 내장되어 있다. 구형 자바스크립트에서는 JSON.org의 라이브러리를 쓸 수 있다.

			//JSON 문자열
			var jstr = '{"mykey" : "my value"}';

			//안티패턴
			var data = eval('(' + jstr + ')');

			//권장안
			var data = JSON.parse(jstr);

			console.log(data.mykey);			//"my value"
		

jQuery에는 parseJSON()이라는 메서드가 있다.

			//JSON 문자열
			var jstr = '{"mykey" : "my value"}';

			var data = jQuery.parseJSON(jstr);
			console.log(data.mykey);			//"my value"
		

JSON.parse() 메서드의 반대는 JSON.stringify()다. 이 메서드는 객체 또는 배열을 인자로 받아 JSON 문자열로 직렬화한다.

			var dog = {
				name : "Fido",
				dob : new Date(),
				legs : [1, 2, 3, 4]
			};

			var jsonstr = JSON.stringify(dog);

			console.log(jsonstr);	// {"name":"Fido","dob":"2013-05-26T07:29:37.680Z","legs":[1,2,3,4]}
		

원시 데이터 타입 래퍼

자바스크립트에는 숫자, 문자열, 불린, null, undefined의 다섯가지 원시 데이터 타입이 있다. null, undefined를 제외한 나머지 세 개는 원시 데이터 타입 래퍼라 불리는 객체를 가지고 있다. 이 래퍼 객체는 각각 내장 생성자인 Number(), Stirng(), Boolean()을 사용하여 생성된다.

다음 예제는 원시 데이터 타입 숫자와 숫자 객체이 차이를 보여준다.

			//원시 데이터 타입 숫자
			var n = 100;
			console.log(typeof n);		//"number"

			//숫자객체
			var nobj = new Number(100);
			console.log(typeof nobj);		//"object"
		

래퍼 객체에는 유용한 프로퍼티와 메서드들이 있다. 이런 편리한 메서드를 쓰기 위해 원시 데이터 타입을 쓰지 않고 객체를 만들게 된다. 그러나 메서드를 호출하는 순간 내부적으로는 원시 데이터 타입 값이 객체로 임시 변환되어 객체처럼 동작한다.

			//원시 데이터 타입 문자열을 객체로 사용한다.
			var s = "hello";
			console.log(s.toUpperCase());		//"HELLO"

			//값 자체만으로도 객체처럼 동작할 수 있다.
			"monkey".slice(3, 6);		//"key"
		

원시 데이터 타입 값도 언제든 객체처럼 쓸 수 있기 때문에 장황한 래퍼 생성자를 쓸 필요는 사실 별로 없다.

			// 다음과 같은 방식을 피하라.
			var s = new String("my string");
			var n = new Number(101);
			var b = new Boolean(true);

			// 이렇게 쓰는 것이 더 좋다.
			var s = my string;
			var n = 101;
			var b = true;
		

값을 확장하거나 상태를 지속시키기 위해 래퍼 객체를 쓰는 경우도 있다. 원시 데이터 타입은 객체가 아니기 때문에 프로퍼티를 추가하여 확장할 수가 없다.

			var greet = "Hello therer";
			greet.split(' ')[0];		//"Hello"

			//원시 데이터 타입에 확장을 시도할 경우 에러는 발생하지 않는다.
			greet.smile = true;

			//그러나 실제로 동작하지 않는다. 
			typeof greet.smile;			//"undefined"
		

위 코드에서 만약 greet가 new String()을 사용하여 객체로 정의되었다면 smile 프로퍼티가 생성되었을 것이다.

함수


배경지식

| 용어 정리

			// 이름있는 함수 표현식
			var add = function add(a, b){
				return a + b;
			};

			// 함수 표현식 (또는 익명함수)
			var add = function (a, b){
				return a + b;
			};

			// 함수 선언문
			function foo(){
				// 함수 본문
			}
		

문법적인 면에서, 함수 표현식의 결과를 변수에 할당하지 않을 경우 이름 있는 함수 표현식과 함수 선언문은 비슷해 보인다. 세미 콜론이 붙는지 여부에 따라 그 둘의 문법적인 차이점이 있다. 함수 선언문에는 세미콜론이 필요하지 않지만 함수 표현식에는 필요하다.

| 선언문 vs. 표현식

함수 객체를 매개변수로 전달하거나, 객체 리터럴로 메서드를 정의하는 경우 함수 표현식을 사용할 수 있다.

			// 함수 표현식(익명 함수)을 callMe 함수의 인자로 전달한다.
			callMe(function(){
					//이 함수는 익명함수 표현식이다.
			});

			// 이름 있는 함수 표현식을 callMe 함수의 인자로 전달한다.
			callMe(function me(){
					//이 함수는 "me"라는 이름을 가진 함수 표현식이다.
			});

			// 함수 표현식을 객체의 프로퍼티로 저장한다.
			var myobject = {
				say : function(){
					//이 함수는 함수 표현식이다.
				}
			};
		

함수 선언문은 전역 유효범위나 다른 함수의 본문 내부, 즉 '프로그램 코드'에서만 쓸 수 있다. 변수나 프로퍼티에 할당할 수 없고, 함수 호출시 인자로 함수를 넘길 때도 사용할 수 없다.

			//전역 유효범위
			function foo(){}
			function local(){
				//지역 유효범위
				function bar(){}

				return bar;
			}
		

| 함수의 name 프로퍼티

함수 선언문과 이름 있는 함수 표현식을 사용하면 name 프로퍼티가 정의된다. 반면 익명함수 표현식의 name 프로퍼티 값은 IE에서는 undefined가 되고, 파이어폭스와 웹킷에서는 빈문자열로 정의된다.

			function foo(){}
			var bar = function(){}
			var baz = function baz(){}

			foo.name;		// "foo"
			bar.name;		// ""
			baz.name;		//"baz"
		

name 프로퍼티는 파이어버그나 다른 디버거에서 코드를 디버깅할 때 유용하다. 함수 선언문보다 함수 표현식을 선호하는 이유는, 함수 표현식을 사용하면 함수가 다른 객체들과 마찬가지로 객체의 일종이며 어떤 특별한 언어 구성요소가 아니라는 사실이 좀더 드러나기 때문이다.

콜백 패턴

함수는 객체다. 즉 함수를 다른 함수에 인자로 전달할 수 있다. introduceBugs() 함수를 writeCode() 함수의 인자로 전달하면, 아마도 writeCode()는 어느 시점에 introduceBugs()를 실행할 것이다 이때 introduceBugs()를 콜백 함수 또는 간단하게 콜백이라고 부른다.

			function writeCode(callback){
				//어떤 작업을 수행한다.
				callback();
				//...
			}
			function introduceBugs(){
				...
			}
			writeCode(introduceBugs);
		

introduceBugs()가 writeCode()의 인자로 괄호없이 전달된 사실에 눈여겨 보자. 괄호를 붙이면 함수가 실행되는데 이 경우에는 함수의 참조만 전달하고 실행은 추후 적절한 시점에 wirteCode()가 해줄 것이기 때문에 괄호를 덧붙이지 않는다.

| 콜백 예제

복잡한 작업을 수행한 후 그 결과로 대용량 데이터셋을 반환하는 범용 함수가 있다고 하자. 이 함수는 findNodes()와 같은 형식으로 호출되며, DOM 트리를 탐색해 필요한 엘리먼트의 배열을 반환한다.

			var findNodes = function(){
				var i = 100000,		//긴 루프
					nodes = [],	//결과를 저장할 배열
					found;			//노드 탐색 결과
				while(i){
					i -= 1;
					//이 부분에 복작한 로직이 들어간다.
					nodes.push(found);
				}
				return nodes;
			};
		

이 함수는 범용으로 쓸 수 있도록 실제 엘리먼트에는 어떤 작업도 하지 않고 단지 DOM 노드의 배열을 반환하기만 하도록 유지하는게 좋다. 노드를 수정하는 로직은 다른 함수에 두자. 예를 들어 hide()라는 함수를 만들어 페이지에서 노드를 숨긴다.

			var hide = function(nodes){
				var i = 0, max = nodes.length;
				for(; i < max; i += 1){
					nodes[i].style.display = "none";
				}
			};

			//함수를 실행한다.
			hide(findNodes());
		

이 구현은 findNodes()에서 반환된 노드의 배열에 대해 hide()가 다시 루프를 돌아야하기 때문에 비효율적이다. findNodes()에서 노드를 선택하고 바로 숨긴다면 재차 루프를 돌지 않아 더 효율적일 것이다. 그렇지만 findNodes()안에서 노드를 숨기는 로직을 구현하면 탐색과 수정 로직의 결합으로 인해 범용 함수의 의미가 퇴색될 것이다. 바로 이럴 때 콜백 패턴을 사용할 수 있다. 노드를 숨기는 로직의 실행을 콜백 함수에 위임하고 이 함수를 findNode()에 전달한다.

			//findeNode()가 콜백을 받도록 리팩터링한다.
			var findNodes = function(callback){
				var i = 100000,		//긴 루프
					nodes = [],	//결과를 저장할 배열
					found;			//노드 탐색 결과

				//콜백함수를 호출할 수 있는지 확인한다.
				if(typeof callback !== "function"){
					callback = false;
				}

				while(i){
					i -= 1;
					//이 부분에 복작한 로직이 들어간다.

					//여기서 콜백을 실행한다.
					if(callback){
						callback(found);
					}

					nodes.push(found);
				}
				return nodes;
			};
		

이 구현 방법은 직관적이다. findNodes()에는, 콜백 함수가 추가되었는지 확인하고, 있으면 실행하는 작업 하나만 추가한다. 콜백은 생략할 수 있기 때문에 리팩터링된 findNodes()는 여전히 이전과 동일하게 사용할 수 있고, 기존 API에 의존하는 코드를 망가뜨리지 않는다.

hide()의 구현은 노드들을 순회할 필요가 없어져 더 간단해졌다.

			//콜백 함수
			var hide = function(nodes){
				nodes.style.display = "none";
			};

			//노드를 찾아서 바로 숨긴다.
			findNodes(hide);
		

이 예제에서 보다시피 이미 존재하는 함수를 콜백 함수로 쓸 수도 있지만, findNodes() 함수를 호출할 때 익명 함수를 생성해서 쓸 수도 있다. 예를 들어, 동일한 범용의 findNodes() 함수를 사용해 노드를 보여주는 방법은 아래와 같다.

			//익명함수를 콜백으로 전달한다.
			findNodes(function(node){
				node.style.display = "block";
			});
		

| 콜백과 유효범위

이전 예제에서, 콜백은 다음과 같은 형태로 실행되었다.

			callback(parameters);
		

그러나 콜백이 일회성의 익명함수나 전역함수가 아닌 객체의 메서드인 경우 자신이 속해있는 객체를 참조하기 위해 this를 사용하면 예상치 않게 동작할 수도 있다.

myapp이라는 객체의 메서드인 paint() 함수를 콜백으로 사용한다고 가정해보자.

			var myapp = {};
			myapp.color = "green";
			myapp.paint = function(node){
				node.style.color = this.color;
			};

			var findNodes = function(callback){
				...
				if(typeof callback === "function"){
					callback(found);
				}
				...
			};
		

findNodes(myapp.paint)를 호출하면 this.color가 정의되지 않아 예상대로 동작하지 않는다. findNodes()가 전역함수이기 때문에 객체 this는 전역 객체를 참조한다. findNodes()가 dom.findNodes()처럼 dom이라는 객체의 메서드라면, 콜백 내부의 this는 예상과 달리 myapp이 아닌 dom을 참조하게 된다.

이 문제를 해결하기 위해서는 콜백 함수와 함께 콜백이 속해 있는 객체를 전달하면 된다.

			findNodes(myapp.paint, myapp);
		

전달받은 객체를 바인딩하도록 findNodes() 또한 수정한다.

			var findNodes = function(callback, callback_obj){
				...
				if(typeof callback === "function"){
					callback.call(callback_obj, found);
				}
				...
			};
		

콜백으로 사용될 메서드와 객체를 전달할 때, 메서드를 문자열로 전달할 수도 있다. 이렇게 하면 객체를 두번 반복하지 않아도 된다.

			findNodes(myapp.paint, myapp);
		

위를 다음과 같이 바꿀 수 있다.

			findNodes("paint", myapp);
		

이 두가지 방법에 모두 대응하는 findNodes()를 다음과 같이 정의할 수 있다.

			var findNodes = function(callback, callback_obj){
				if(typeof callback === "string"){
					callback = callback_obj[callback];
				}
				...
				if(typeof callback === "function"){
					callback.call(callback_obj, found);
				}
				...
			};
		

| 비동기 이벤트 리스너

콜백패턴은 일상적으로 다양하게 사용된다. 예를 들어 페이지의 엘리먼트에 이벤트 리스너를 붙이는 것도, 실제로는 이벤트가 발생했을 때 호출될 콜백 함수의 포인터를 전달한는 것이다. 다음은 document의 click 이벤트 리스너로 console.log() 콜백 함수를 전달하는 예이다.

			document.addEventListener("click", console.log, false);
		

대부분의 클라이언트 측 브라우저 프로그래밍은 이벤트 구동방식인데 자바스크립트가 이벤트 구동형 프로그래밍에 특히 적합한 이유가 비동기적으로, 달리 말하면 무작위로 동작할 수 있게 하는 콜백 패턴 덕분이다.

| 타임아웃

또 다른 콜백 패턴은 브라우저의 window 객체에 의해 제공되는 타임아웃 메서드들인 setTimeout()과 setInterval()이다. 이 메서드들도 콜백함수를 받아서 실행시킨다.

			var thePlotThickens = function(){
				console.log('500ms later...');
			};

			setTimeout(thePlotThickens, 500);
		

thePlotThickens가 괄호 없이 변수로 전달된 점에 주의하라. 여기서는 이 함수를 곧바로 실행하지 않고 setTimeout()이 나중에 호출할 수 있도록 함수를 가리키는 포인터만을 전달하고 있다. 함수 포인터 대신 문자열 "thePlotThickens()"를 전달하는 것은 eval()과 비슷한 흔한 안티패턴이다.

| 라이브러리에서의 콜백

콜백은 라이브러리를 설계할 때 유용한 간단하고 강력한 패턴이다. 소프트웨어 라이브러리에 들어갈 코드는 가능한 범용적이고 재사용할 수 있어야 한다. 콜백은 이런 일반화에 도움이 될 수 있다. 생각할 수 있는 모든 기능을 예측하고 구현할 필요는 없다. 이는 라이브러리를 쓸데없이 부풀릴 뿐이고 대부분의 사용자는 그런 커다란 기능들을 절대 필요로 하지 않기 때문이다. 대신에 핵심 기능에 집중하고 콜백의 형태로 '연결고리'를 제공해야 한다. 콜백 함수를 활용하면 조금 더 쉽게 라이브러리 메서드를 만들고 확장하고 가다듬을 수 있다.

함수 반환하기

함수는 객체이기 때문에 반환값으로 사용될 수 있다. 아래 예제는 일회적인 초기화 작업을 수행한 후 반환 값을 만든다. 반환 값은 실행 가능한 함수다.

			var setup = function(){
				alert(1);
				return function(){
					alert(2);
				};
			};

			//setup 함수를 사용
			var my = setup();			// alert으로 1이 출력된다.
			my();			// alert으로 2가 출력된다.
		

setup()은 반환된 함수를 감싸고 있기 때문에 클로저를 생성한다. 클로저는 반환되는 함수에서는 접근할 수 있지만 코드 외부에서는 접근할 수 없기 때문에, 비공개 데이터 저장을위해 사용할 수 있다. 아래는 매번 호출할 때마다 값을 증가시키는 카운터이다.

			var setup = function(){
				var count = 0;
				return function(){
					return (count += 1);
				};
			};

			var next = setup();
			next();		//1을 반환한다.
			next();		//2를 반환한다.
			next();		//3을 반환한다.
		

즉시 실행 함수

즉시 실행 함수 패턴은 함수가 선언되자마자 실행되도록 하는 문법이다.

			(function(){
				alert('watch out!');
			}());
		

즉시 실행 함수 패턴은 다음의 부분들로 구성된다.

다음의 대체 문법 또한 일반적으로 사용되지만 JSLint는 처음의 패턴을 선호한다.

			(function(){
				alert('watch out!');
			})();
		

이 패턴은 페이지 로드가 완료된 후, 이벤트 핸들러를 등록하거나 객체를 생성하는 등의 초기 설정 작업을 할 경우 유용하다. 단 한번만 실행되기 때문에 재사용하기 위해 이름이 지정된 함수를 생성할 필요가 없고 단지 초기화 단계가 완료될 때까지만 사용할 임시 변수들이 필요핟. 이 모든 변수를 전역으로 생성하는 것은 좋지 않은 생각이다. 즉시 실행 함수는 모든 코드를 지역 유효범위로 감싸고 어떤 변수도 전역 유효범위로 새어나가지 않게 한다.

			(function(){
				var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
					today = new Date(),
					msg = 'Today is ' + days[today.getDay()] + ', ' + today.getDate();

				alert(msg);
			})();		// "Today is Fri, 13"
		

만약 이 코드가 즉시 실행 함수로 감싸여 있지 않았다면 days, today, msg 변수는 전역 변수가 되어 초기화 코드 이후에도 남아 있게 된다.

| 즉시 실행 함수의 매개변수

즉시 실행 함수에 인자를 전달할 수도 있다. 일반적으로 전역객체가 즉시 실행 함수의 인자로 전달된다. 따라서 즉시 실행 함수 내에서 window를 사용하지 않고도 전역 객체에 접근할 수 있다. 이러한 방법을 통해 브라우저 외의 실행 환경에서도 코드를 공통으로 사용할 수 있다.

			(function(global){

				//전역 객체를 'global'로 참조
			
			}(this));
		

| 즉시 실행 함수의 반환값

다른 함수와 비슷하게, 즉시 실행 함수도 값을 반환할 수 있고 반환된 값은 변수에 할당될 수 있다.

			var result = function(){
				return 2 + 2;
			}();
		

즉시 실행 함수의 반환 값을 변수에 할당할 때는 괄호가 필요 없다. 결과 값은 즉시 실행 함수의 반환 값, 즉 이 경우에는 숫자 4를 참조한다. 동일한 결과를 갖는 또 다른 문법은 다음과 같다.

			var result = (function(){
				return 2 + 2;
			})();
		

이 예제는 즉시 실행 함수의 실행 결과로 원시 데이터 타입인 정수 값을 반환한다. 원시 데이터 값 외에도 모든 타입의 값이 가능하고, 새로운 함수를 반환할 수도 있다. 이 경우 즉시 실행 함수의 유효범위를 사용해 특정 데이터를 비공개 상태로 저장하고, 반환되는 내부 함수에서만 접근하도록 할 수도 있다.

다음 예제를 보면, 즉시 실행 함수가 함수를 반환하고 이 반환 값이 getResult라는 변수에 할당된다. 이 함수는 즉시 실행 함수에서 미리 계산하여 클로저에 저장해둔 res라는 값을 반환한다.

			var getResult = (function(){
				var res = 2 + 2;
				return function(){
					return res;
				};
			}());
		

즉시 실행 함수는 객체 프로퍼티를 정의할 때에도 사용할 수 있다. 어떤 객체이 프로퍼티가 객체의 생명주기 동안에는 값이 변하지 않고, 처음에 값을 정의할 때는 적절한 계산을 위한 작업이 필요하다고 가정해 보자. 그렇다면 이 작업을 즉시 실행 함수로 감싼 후, 즉시 실행 함수의 반환 값을 프로퍼티 값으로 할당하면 된다.

			var o = {
				message : (function(){
					var who = "me", what = "call";
					return what + "" + who;
				}()),
				getMsg : function(){
					return this.message
				}
			};

			o.getMsg();		// "call me"
			o.message;		// "call me"
		

이 예제에서 o.message는 함수가 아닌 문자열 프로퍼티이지만 값을 정의하려면 함수가 필요하다. 이 함수는 스크립트가 로딩될 때 실행되어 프로퍼티를 정의한다.

| 장점과 사용방법

즉시 실행 함수 패턴은 폭넓게 사용된다. 선언된 모든 변수는 스스로를 호출하는 함수의 지역 변수가 되기 때문에 임시 변수가 전역 공간을 어지럽힐까봐 걱정하지 않아도 된다.

즉시 실행 함수 패턴을 사용해 개별 기능을 독자적인 모듈로 감쌀 수도 있다. 다음 템플릿을 활용하면 기능을 단위별로 정의할 수 있다. 이것을 module1이라고 부르자.

			// module1.js에서 정의한 module
			(function(){
				
				// 모든 module1 코드....

			}());
		

이 템플릿을 따라 또 다른 모듈도 코딩할 수 있다. 그리고 실제 사이트에 코드를 올릴 때, 어떤 기능이 사용될 준비가 되었는지 결정하고 빌드 스크립트를 사용해 해당하는 파일들을 병합하면 된다.

초기화 시점의 분기

초기화 시점의 분기는 최적화 패턴이다. 어떤 조건이 프로그램의 생명주기 동안 변경되지 않는게 확실할 경우, 조건을 단 한번만 확인하는 것이 바람직하다. 브라우저 탐지 (또는 기능 탐지)가 전형적인 예다.

이벤트 리스너를 등록하고 해제하는 메서드를 가지는 유틸리티는 다음과 같다.

			// 변경 이전
			var utils = {
				addListener : function(el, type, fn){
					if(typeof window.addEventListener === 'function'){
						el.addEventListener(type, fn, false);
					}else if(typeof document.attachEvent === 'function'){	// IE
						el.attachEvent('on' + type, fn);
					}else{	//구형의 브라우저
						el['on' + type] = fn;

					}
				},
				removeListener : function(el. type, fn){
					if(typeof window.removeEventListener === 'function'){
						el.removeEventListener(type, fn, false);
					}else if(typeof document.detachEvent === 'function'){	// IE
						el.detachEvent('on' + type, fn);
					}else{	//구형의 브라우저
						el['on' + type] = null;
					}
				}
			};
		

이 코드는 약간 비효율적이다. utils.addListener()나 utils.removeListener()를 호출할 때마다 똑같은 확인 작업이 반복해서 실행된다. 초기화 시점 분기를 이용하면, 처음 스크립트를 로딩하는 동안에 브라우저 기능을 한 번만 확인한다. 다음은 초기하 시점 분기에 대한 접근법을 보여주는 예제다.

			// 변경 이후

			//인터페이스
			var utils = {
				addListener : null,
				removeListener : null
			};
				
			//구현
			if(typeof window.addEventListener === 'function'){
				utils.addListener = function(el, type, fn){
					el.addEventListener(type, fn, false);
				};
				utils.removeListener = function(el, type, fn){
					el.removeEventListener(type, fn, false);
				};
			}else if(typeof document.attachEvent === 'function'){	// IE
				utils.addListener = function(el, type, fn){
					el.attachEvent('on' + type, fn);
				};
				utils.removeListener = function(el, type, fn){
					el.detachEvent('on' + type, fn);
				};
			}else{	// 구형 브라우저
				utils.addListener = function(el, type, fn){
					el['on' + type] = fn;
				};
				utils.removeListener = function(el, type, fn){
					el['on' + type] = null;
				};
			}
		

함수 프로퍼티 - 메모이제이션 패턴

언제든지 함수에 사용자 정의 프로퍼티를 추가할 수 있다. 함수에 프로퍼티를 추가하여 결과(반환값)를 캐시하면 다음 호출 시점에 복잡한 연산을 반복하지 않을 수 있다. 이런 활용 방법을 메모이제이션 패턴이라고 한다.

			var myFunc = function(param){
				if(!myFun.cache[param]){
					var result = {};
					...
					myFunc.cache[param] = result;
				}
				return myFunc.cache[param];
			};

			//캐시저장 공간
			myFunc.cache = {};
		

위 예제는 myFunc 함수에 cache 프로퍼티를 생성한다. 이 프로퍼티는 일반적인 프로퍼티처럼 myFunc.cache와 같은 형태로 접근할 수 있다. cache 프로퍼티는 함수로 전달된 param 매개변수를 키로 사용하고 계산의 결과를 값으로 가지는 객체다. param 이라는 매개변수는 문자열과 같은 원시 데이터 타입이라고 가정한다. 만약 더 많은 매개변수와 더 복잡한 타입을 갖는다면 일반적으로 직렬화하여 해결할 수 있다.

예를 들어, 객체 인자를 JSON 문자열로 직렬화하고 이 문자열을 cache 객체에 키로 사용할 수 있다.

			var myFunc = function(){
				var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)), result;

				if(!myFun.cache[cachekey]){
					var result = {};
					...
					myFunc.cache[cachekey] = result;
				}
				return myFunc.cache[cachekey];
			};

			//캐시저장 공간
			myFunc.cache = {};
		

설정 객체 패턴

설정 객체 패턴은 좀더 깨끗한 API를 제공하는 방법이며 라이브러리나 다른 프로그램에서 사용할 코드를 만들때 특히 유용하다.

많은 수의 매개변수를 전달하기는 불편하다. 모든 매개변수를 하나의 객체로 만들어 대신 전달하는 방법이 더 낫다. 이 객체를 설정을 뜻하는 conf라고 지정하자.

			addPerson(conf);

			var conf = {username : "batman", first : "Bruce", last : "Wayne"};
		

설정 객체의 장점은 다음과 같다.

커리(Curry)

| 함수 적용

자바스크립트에서도 Function.prototype.apply()를 사용하면 함수를 적용할 수 있다. 다음은 함수 적용의 예다.

			//함수를 정의한다.
			var sayHi = function(who){
				return "Hello" + (who ? ", " + who : "") + "!";
			};

			//함수를 호출한다.
			sayHi();		// "Hello"
			sayHi('world');		// "Hello, world!"

			//함수를 적용한다.
			sayHi.apply(null, ["hello"]);		// "Hello, hello!"
		

apply()는 두개의 매개변수를 받는다. 첫번째는 이 함수 내에 this와 바인딩할 객체이고, 두번째는 배열 또는 인자로 함수 내부에서 배열과 비슷한 형태의 arguments 객체로 사용하게 된다. 첫번째 매개변수가 null이면, this는 전역 객체를 가리킨다.

함수가 객체의 메서드일 때는, null을 전달하지 않는다. 다음은 apply()의 첫번째 인자로 객체를 전달하는 예이다.

			var alien = {
				sayHi : function(who){
					return "Hello" + (who ? ", " + who : "") + "!";
				}
			};

			alien.sayHi('world');	//"Hello, world!"
			sayHi.apply(alien, ["humans"]);	//"Hello, humans!"
		

이 코드에서, sayHi() 내부의 this는 alien을 가리킨다.

객체 생성 패턴


네임스페이스 패턴

네임스페이스는 프로그램에서 필요로 하는 전역 변수의 개수를 줄이는 동시에 과도한 접두어를 사용하지 않고도 이름이 겹치지 않게 해준다. 수많은 함수, 객체, 변수들로 전역 유효범위를 어지럽히지 않는 대신, 애플리케이션이나 라이브러리를 위한 전역 객체를 하나 만들고 (단 하나만 만드는 것이 이상적이다.) 모든 기능을 이 객체에 추가하면 된다.

			//수정 전
			//안티 패턴 : 전역 변수 5개

			//생성자 함수 2개
			function Parent(){}
			function Child(){}

			//변수 1개
			var some_var = 1;

			//객체 2개
			var module1 = {};
			module1.data = {a: 1, b: 2};
			var module2 = {};
		

위와 같은 코드를 리팩터링하기 위해서는 먼저 애플리케이션 전용 전역 객체, 이를 테면 MYAPP을 생성한다. 그런 다음 모든 함수와 변수들을 이 전역 객체의 프로퍼티로 변경한다.

			//수정 후 : 전역 변수 1개

			//전역 객체
			var MYAPP = {};

			//생성자
			MYAPP.Parent = function(){};
			MYAPP.Child = function(){};

			//변수
			MYAPP.some_var = 1;

			//객체 컨테이너
			MYAPP.module = {};

			//객체들을 컨테이너 안에 추가한다.
			MYAPP.modules.module1 = {};
			MYAPP.modules.module1.data = {a: 1, b: 2};
			MYAPP.modules.module2 = {};
		

전역 네임스페이스 객체의 이름은 애플리케이션 이름이나 라이브러리의 이름, 도메인명, 회사 이름중에서 선택할 수 있다.

| 범용 네임스페이스 함수

프로그램의 복잡도가 증가하고 코드의 각 부분들이 별개의 파일로 분리되어 선택적으로 문서에 포함되게 되면, 어떤 코드가 특정 네임스페이스나 그 내부의 프로퍼티를 처음으로 정의한다고 가정하기가 위험하다. 네임스페이스에 추가하려는 프로퍼티가 이미 존재할 수도 있고 따라서내용을 덮어쓰게 될지도 모른다. 그러므로 네임스페이스를 생성하거나 프로퍼티를 추가하기 전에 먼저 이미 존재하는지 여부를 확인하는 것이 최선이다.

			//위험하다
			var MYAPP = {};
			
			//개선안
			if(typeof MYAPP === "undefined"){
				var MYAPP = {};
			}

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

이렇게 추가되는 확인 작업 때문에 상당량의 중복 코드가 생겨날 수 있다. 따라서 네임스페이스 생성의 실제 작업을 맡아 줄 재사용 가능한 함수를 만들어 두면 편리하다.

			//네임스페이스 함수를 사용한다.
			MYAPP.namespace('MYAPP.modules.module2');

			//위 코드는 다음과 같은 결과를 반환한다.
			var 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;
			};
		

이 코드는 다음과 같이 사용할 수 있다.

			//반환 값을 지역변수에 할당한다.
			var module2 = MYAPP.namespace('MYAPP.modules.module2');
			module2 ==== MYAPP.modules.module2;		// true

			//첫부분의 'MYAPP'을 생략하고도 쓸 수 있다.
			MYAPP.namespace('modules.module51');
		

의존 관계 선언

자바스크립트 라이브러리들은 대개 네임스페이스를 지정하여 모듈화되어 있기 때문에, 필요한 모듈만 골라서 쓸 수 있다. 예를 들어 YUI2에는 네임스페이스 역할을 하는 YAHOO라는 전역 변수가 있고, 이 전역 변수의 프로퍼티로 YAHOO.util.DOM(DOM 모듈)이나 YAHOO.util.Event(이벤트 모듈)와 같은 모듈이 추가되어 있다.

이때 함수나 모듈 내 최상단에, 의존 관계에 있는 모듈을 선언하는 것이 좋다. 즉 지역 변수를 만들어 원하는 모듈을 가리키도록 선언하는 것이다.

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

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

간단한 패턴이지만 많은 장점을 가지고 있다.

비공개 프로퍼티와 메서드

자바 등 다른 언어와는 달리 자바스크립트에는 private, protected, public 프로퍼티와 메서드를 나타내는 별도의 문법이 없다. 객체의 모든 멤버는 public, 즉 공개되어 있다.

| 비공개(private) 멤버

비공개 멤버에 대한 별도의 문법은 없지만 클로저를 사용해서 구현할 수 있다. 생성자 함수 안에서 클로저를 만들면, 클로저 유효범위 안의 변수는 생성자 함수 외부에 노출되지 않지만 객체의 공개 메서드 안에서는 쓸 수 있다. 즉 생성자에서 객체를 반환할 때 객체의 메서드를 정의하면, 이 메서드안에서는 비공개 변수에 접근할 수 있는 것이다.

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

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

			var toy = new Gadget();

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

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

보다시피 자바스크립에서도 쉽게 비공개 멤버를 구현할 수 있다. 비공개로 유지할 데이터를 함수로 감싸기만 하면 된다. 이 데이터들을 함수의 지역 변수로 만들면, 함수 외부에서는 접근할 수 없다.

| 특권(privileged) 메서드

특권 메서드는 단지 비공개 멤버에 접근권한을 가진 공개 메서드를 가리키는 이름일 뿐이다. 앞선 예제에서는 getName()은 비공개 프로퍼티인 name에 '특별한' 접근 권한을 가지고 있기 때문에 특권 메서드라고 할 수 있다.

| 비공개 멤버의 허점

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

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

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

얼핏 보기엔 별 문제 없어 보이나 여기서 getSpec() 메서드가 specs 객체에 대한 참조를 반환한다는게 문제다. specs는 감춰진 비공개 멤버처럼 보이지만 Gadget 사용자에 의해 변경될 소지가 있다.

			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"}
		

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

| 객체 리터럴과 비공개 멤버

생성자가 아닌 객체 리터럴로 비공개 멤버를 구현할 수 있다. 객체 리터럴에서는 익명 즉시 실행함수를 추가하여 클로저를 만든다.

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

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

			myobj.getName();
		

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

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

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

			myobj.getName();
		

이 예제는 '모듈 패턴'의 기초가 되는 부분이기도 하다.

| 프로토타입과 비공개 멤버

생성자를 사용하여 비공개 멤버를 만들 경우, 생성자를 호출하여 새로운 객체를 만들 때마다 비공개 멤버가 매번 생성된다는 단점이 있다. 이러한 중복을 없애고 메모리를 절약하려면 공통 프로퍼티와 메서드를 생성자의 prototype 프로퍼티에 추가해야 한다. 이렇게 하면 동일한 생성자로 생성한 모든 인스턴스가 공통된 부분을 공유하게 된다. 감춰진 비공개 멤버들도 모든 인스턴스가 함께 쓸 수 있다. 이를 위해서는 두가지 패턴, 즉 생성자 함수 내부에 비공개 멤버를 만드는 패턴과 객체 리터럴로 비공개 멤버를 만드는 패턴을 함께 써야 한다. 왜냐하면 prototype 프로퍼티도 결국 객체라서, 객체 리터럴로 생성할 수 있기 때문이다.

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

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

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

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

			var toy = new Gadget();

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

모듈 패턴

모듈 패턴은 늘어나는 코드를 구조화하고 정리하는 데 도움이 되기 때문에 널리 쓰인다. 모듈 패턴을 사용하면 개별적인 코드를 느슨하게 결합시키며 소프트웨어 개발 중에 요구사항에 따라 기능을 추가하거나 교체하거나 삭제하는 것도 자유롭게 할 수 있다.

모듈 패턴은 다음 패턴들 여러 개를 조합한 것이다.

첫단계는 네임스페이스를 설정하는 것이다. namespace() 함수를 사용해, 유용한 배열 메서드를 제공하는 유틸리티 모듈 예제를 만들어 보자.

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

그 다음 단계는 모듈을 정의하는 것이다. 공개 여부를 제한해야 한다면 즉시 실행 함수를 사용해 비공개 유효범위를 만들면 된다. 즉시 실행 함수는 모듈이 될 객체를 반환한다. 이 객체에는 모듈 사용자에게 제공할 공개 인터페이스가 담기게 될 것이다.

			MYAPP.utilites.array = (function(){
				return {
					// 여기에 객체 내용을 구현한다...
				};
			}());
		

이제 공개 인터페이스에 메서드를 추가한다.

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

즉시 실행 함수의 비공개 유효범위를 사용하면, 비공개 프로퍼티와 메서드를 마음껏 선언할 수 있다. 모듈에 의존 관계가 있다면 즉시 실행 함수 상단에 정의한다. 변수를 선언한 다음에는 필요에 따라 모듈을 초기화하는 데 필요한 일회성 초기화 코드를 두어도 좋다. 즉시 실행 함수가 반환하는 최종 결과는 모듈의 공개 API를 담은 객체다.

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

			MYAPP.utilites.array = (function(){

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

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

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

				//var 선언을 마친다.

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

				//공개 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;
					}
					//더 필요한 메서드와 프로퍼티를 여기에 추가한다.
				};
			}());
		

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

| 모듈 노출 패턴

모든 메서드를 비공개 상태로 유지하고, 최종적으로 공개 API를 갖출 때 공개할 메서드만 골라서 노출하는 것이다. 앞의 예제를 다음과 같이 수정할 수 있다.

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

			MYAPP.utilites.array = (function(){

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

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

					//비공개 메서드
					inArray = function(haystack, needle){
						for(var i=0, max=haystack.length; i<max; i+=1){
							if(haystack[i] === needle){
								return i;
							}
						}
						return -1;
					},
					isArray = function(a){
						return ops.call(a) ==== array_string;
					};

				//var 선언을 마친다.

				//공개 API 노출
				return {
					isArray : isArray,
					indexOf : inArray
				};

			}());
		

| 생성자를 생성하는 모듈

앞선 예제는 MYAPP.utilities.array 라는 객체를 만들어 냈다. 하지만 생성자 함수를 사용해 객체를 만드는 게 더 편할 때도 있다. 모듈을 감싼 즉시 실행함수가 마지막에 객체가 아니라 함수를 반환하게 하면 된다. 다음 모듈 패턴 예제는 생성자 함수인 MYAPP.utilities.Array를 반환한다.

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

			MYAPP.utilites.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 = {
					constructor : 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(obj);
		

| 모듈에 전역 변수 가져오기

이 패턴의 흔한 변형 패턴으로는, 모듈을 감싼 즉시 실행 함수에 인자를 전달하는 형태가 있다. 어떠한 값이라도 가능하지만, 보통 전역 변수에 대한 참조 또는 전역 객체 자체를 전달한다. 이렇게 전역 변수를 전달하면 즉시 실행 함수 내에서 지역 변수로 사용할 수 있게 되기 때문에 탐색 작업이 좀 더 빨라진다.

			MYAPP.utilites.module = (function(app, global){

				//전역 객체에 대한 참조와
				//전역 애플리케이션 네임스페이스 객체에 대한 참조가 지역 변수화 된다.

			}(MYAPP, this));
		

샌드박스 패턴

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

| 전역 생성자

네임스페이스 패턴에서는 전역 객체가 하나다. 샌드박스 패턴의 유일한 전역은 생성자다. 이것을 Sandbox()라고 하자. 이 생성자를 통해 객체들을 생성할 것이다. 그리고 이 생성자에 콜백 함수를 전달해 해당 코드를 샌드박스 내부 환경으로 격리시킬 것이다. 샌드박스 사용법은 다음과 같다.

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

box 객체는 네임스페이스 패턴에서의 MYAPP와 같은 것이다. 코드가 동작하는데 필요한 모든 라이브러리 기능들이 여기에 들어간다. 이 패턴에 두 가지를 추가해보자.

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

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

다음은 모듈 이름을 개별적인 인자로 전달한다.

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

편의를 위해 모듈명을 누락시키면 샌드박스가 자동으로 *를 가정하도록 한다. *인자는 '쓸 수 있는 모듈을 모두 사용한다'는 의미이다.

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

마지막으로 샌드박스 객체의 인스턴스를 여러 개 만드는 예제이다. 심지어 한 인스턴스 내부에 다른 인스턴스를 중첩시킬 수도 있다. 이 때도 두 인스턴스간의 간섭 현상은 일어나지 않는다.

			Sandbox('dom', 'event', function(box){
				
				//dom과 event를 가지고 작업하는 코드
				Sandbox('ajax', function(box){
					//샌드박스된 box객체를 또 하나 만든다.

				});

				//더 이상 ajax 모듈의 흔적을 찾아볼 수 없다.
			});
		

샌드박스 패턴을 사용하면 콜백 함수로 코드를 감싸기 때문에 전역 네임스페이스를 보호할 수 있다. 또 원하는 유형별로 모듈의 인스턴스를 여러 개 만들 수도 있다. 이 인스턴스들은 각각 독립적으로 동작하게 된다.

| 모듈 추가하기

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

Sandbox() 생성자 함수 역시 객체이므로, modules라는 프로퍼티를 추가할 수 있다. 이 프로퍼터는 키-캆의 쌍을 담은 객체로, 모듈의 이름이 키가 되고 각 모듈을 구현한 함수가 값이 될 것이다.

			Sandbox.modules = {};

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

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

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

위 예제에서는 dom, event, ajax라는 모듈을 추가했다. 각 모듈을 구현하는 함수들이 현재의 인스턴스 box를 인자로 받아들인 다음 이 인스턴스에 프로퍼티와 메서드를 추가하게 된다.

| 생성자 구현

이제 Sandbox() 생성자를 구현해 보자.

			function Sandbox(){
				//arguments를 배열로 바꾼다.
				var args = Array.prototype.slice.call(arguments),
					//마지막 인자는 콜백함수다.
					callback = args.pop(),
					//모듈은 배열로 전달될 수도 있고 개별 인자로 전달될 수도 있다.
					modules = (args[0] && typeof args[0] ==== 'string') ? args : args[0],
					i;
				
				//함수가 생성자로 호출되도록 보장한다.
				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;
				}
			};
		

스태틱 멤버

스태틱 프로퍼티와 메서드란 인스턴스에 따라 달라지지 않는 프로퍼티와 메서드를 말한다. 비공개 스태틱 멤버는 클래스 사용자에게는 보이지 않지만 클래스의 인스턴스들은 모두 함께 사용할 수 있다.

| 공개 스태틱 멤버

자바스크립트에는 스태틱 멤버를 표기하는 별도의 문법이 존재하지 않는다. 그러나 생성자에 프로퍼티를 추가함으로써 클래스 기반 언어와 동일한 문법을 사용할 수 있다.

다음 예제는 Gadget이라는 생성자에 스태틱 메서드인 isShiny()와 일반적인 인스턴스 메서드인 setPrice()를 정의한 것이다. isShiny()는 특정 Gadget 객체를 필요로 하지 않기 때문에 스태틱 메서드라 할 수 있다. 반면 개별 Gadget들의 가격은 다를 수 있기 때문에 setPrice() 메서드를 쓰려면 객체가 필요하다.

			//생성자
			var Gadget = function(){};

			//스태틱 메서드
			Gadget.isShiny = function(){
				return "you bet";
			};

			//프로토타입에 일반적인 함수를 추가
			Gadget.prototype.setPrice = function(price){
				this.price = price;
			};
		

이제 이 메서드를 호출해보자. 스태틱 메서드인 isShiny()는 생성자를 통해 직접 호출되지만, 일반적인 메서드는 인스턴스를 통해 호출된다.

			//스태틱 메서드를 호출하는 방법
			Gadget.isShiny();		// "you bet"

			//인스턴스를 생성 한 후 메서드를 호출한다.
			var iphone = new Gadget();
			iphone.setPrice(500);
		

인스턴스 메서드를 스태틱 메서드와 같은 방법으로 호출하거나 스태틱 메서드를 인스턴스 객체를 사용해 호출하면 동작하지 않는다.

			typeof Gadget.setPrice;		//undefined
			typeof iphone.isShiny;			//undefined
		

그러나 프로토타입에 새로운 메서드를 추가하여 스태틱 메서드를 가리키게 만들면 인스턴스를 통해서도 호출할 수 있다.

			Gadget.prototype.isShiny = Gadget.isShiny;
			iphone.isShiny();		//"you bet"
		

마지막으로 어떤 메서드를 호출 방식에 따라 살짝 다르게 동작하게 하는 예제이다.

			//생성자
			var Gadget = function(price){
				this.price = price;
			};

			//스태틱 메서드
			Gadget.isShiny = function(){
				//다음은 항상 동작
				var msg = "you bet";

				if(this instanceof Gadget){
					//다음은 스태틱하지 않은 방식(인스턴스 객체를 통해 호출)으로 호출되었을 때만 동작한다.
					msg += ", it costs $" + this.price + '!';
				}

				return msg;
			};
		
			//프로토타입에 일반적인 메서드를 추가한다.
			Gadget.prototype.isShiny = function(){
				return Gadget.isShiny.call(this);
			};
		

스태틱 메서드 호출을 테스트해보면 다음과 같은 결과가 나온다.

			Gadget.isShiny();		//"you bet"
		

인스턴스 객체를 통해 스태틱하지 않은 방법으로 호출해보면 다음과 같은 결과가 나온다.

			var a = new Gadget('499.99');
			a.isShiny();		//"you bet, it costs $499.99!"
		

| 비공개 스태틱 멤버

비공개 스태틱 멤버란 다음과 같은 의미를 가진다.

다음은 Gadget 생성자 안에 counter라는 비공개 스태틱 프로퍼티를 구현하는 예제이다. 먼저 클로저 함수를 만들고, 비공개 멤버를 이 함수로 감싼 후, 이 함수를 즉시 실행한 결과로 새로운 함수를 반환하게 한다. 반환되는 함수는 Gadget변수에 할당되어 새로운 생성자가 될 것이다.

			var Gadget = (function(){
				
				//비공개 스태틱 변수/프로퍼티
				var counter = 0;

				//생성자의 새로운 구현 버전을 반환한다.
				return function(){
					console.log(counter += 1);
				};
			}());	//즉시 실행한다.
		

새로운 Gadget 생성자는 단순히 비공개 counter 값을 증가시켜 출력한다. 몇 개의 인스턴스를 만들어 테스트를 해보면 실제로 모든 인스턴스가 동일한 counter 값을 공유하고 있음을 확인할 수 있다.

			var g1 = new Gadget();		// 1이 출력된다.
			var g2 = new Gadget();		// 2가 출력된다.
			var g3 = new Gadget();		// 3이 출력된다.
		

객체당 1씩 counter를 증가시키고 있기 때문에 이 스택 프로퍼티는 Gadget 생성자를 통해 생성된 개별 객체의 유일성을 식별하는 ID가 될 수 있다. 유일한 식별자는 쓸모가 많으니 비공개 스태틱 프로퍼티에 접근할 수 있는 getLastId()라는 특권 메서드를 추가해보자.

			var Gadget = (function(){
				
				//비공개 스태틱 변수/프로퍼티
				var counter = 0,
					NewGadget;

				NewGadget = function(){
					counter += 1;
				};

				//특권메서드
				NewGadget.prototype.getLastId = function(){
					return counter;
				};

				//생성자를 덮어쓴다.
				return NewGadget;

			}());	//즉시 실행한다.

			var iphone = new Gadget();
			iphone.getLastId();		//1

			var ipod = new Gadget();
			ipod.getLastId();			//2

			var ipad = new Gadget();
			ipad.getLastId();			//3
		

공개/비공개 스태틱 프로퍼티는 상당히 편리하다. 특정 인스턴스에 한정되지 않는 메서드와 데이터를 담을 수 있고 인스턴스별로 매번 재생성되지도 않는다.

객체 상수

자바스크립트에는 상수가 없지만 흔히 사용되는 명명 규칙을 사용하여 값이 변경되지 말아야 하는 변수명을 모두 대문자로 쓰기도 한다. 사용자 정의 상수에도 동일한 명명 규칙을 적용하여, 생성자 함수에 스태틱 프로퍼티로 추가하면 된다.

			//생성자
			var Widget = function(){
				...
			};

			//상수
			Widget.MAX_HEIGHT = 320;
			Widget.MAX_WIDTH = 480;
		

객체 리터럴로 생성한 객체에도 동일한 명명규칙을 적용하여 대문자로 쓴 일반적인 프로퍼티를 상수로 간주한다.

다음은 메서드를 제공하는 범용 constant 객체를 구현한 것이다.

set(name, value)
새로운 상수를 정의한다.
isDefine(name)
특정 이름의 상수가 있는지 확인한다.
get(name)
상수의 값을 가져온다.

이 예제에서는 상수 값으로 원시 데이터 타입만 허용된다. 또한 선언하려는 상수의 이름이 toString이나 hasOwnProperty 등 내장 프로퍼티의 이름과 겹치지 않도록 보장하기 위해 hasOwnProperty()를 사용해 별도의 확인 작업을 거친다. 마지막으로 모든 상수의 이름 앞에 임의로 생성된 접두어를 붙인다.

			var constant = (function(){
				var constants = {},
					ownProp = Object.prototype.hasOwnProperty,
					allowed = {
						string : 1,
						number : 1,
						boolean : 1
					},
					prefix = (Math.random() + "_").slice(2);

				return {
					set : function(name, value){
						if(this.isDefined(name)){
							return false;
						}
						if(!ownProp.call(allowed, typeof value)){
							return false;
						}
						constants[prefix + name] = value;
						return true;
					},
					isDefined : function(name){
						return ownProp.call(constants, prefix + name);
					},
					get : function(name){
						if(this.isDefined(name)){
							return constants[prefix + name];
						}
						return null;
					}
				};
			}());

			//이미 정의되었는지 확인한다.
			constant.isDefined("maxwidth");		//false

			//정의한다.
			constant.set("maxwidth", 480);			//true

			//정의되었는지 다시 확인해본다.
			constant.isDefined("maxwidth");		//true

			//다시 정의를 시도해본다.
			constant.set("maxwidth", 320);			//false

			//값은 그대로 인가?
			constant.get("maxwidth");				//480
		

체이닝 패턴

체이닝 패턴이란 객체에 연쇄적으로 메서드를 호출할 수 있도록 하는 패턴이다. 만약 메서드에 의미있는 반환 값이 존재하지 않는다면, 현재 작업중인 객체 인스턴스인 this를 반환하게 한다. 이렇게 하면 객체의 사용자는 앞선 메서드에 이어 다음 메서드를 바로 호출할 수 있다.

			var obj = {
				value : 1,
				increment : function(){
					this.value += 1;
					return this;
				},
				add : function(v){
					this.value += v;
					return this;
				},
				shout : functiono(){
					alert(this.value);
				}
			};

			//메서드 체이닝 호출
			obj.increment().add(3).shout();			// 5
		

코드 재사용 패턴


클래스 방식 vs. 새로운 방식의 상속 패턴

자바스크립트의 생성자 함수와 new 연산자 문법은 클래스를 사용하는 문법과 매우 닮아있다.

			//자바
			Person adam = new Person();

			//자바스크립트
			var adam = new Person();
		

자바스크립트의 생성자 호출을 보면 Person이 클래스인 것 같지만, Person이 여전히 보통의 함수라는 사실을 잊지 말아야 한다. 이러한 구현방법을 '클래스 방식'이라고 부를 수 있다. 클래스에 대해 생각할 필요가 없는 나머지 모든 패턴은 '새로운 방식'이라고 전제한다.

클래스 방식의 상속을 사용할 경우 예상되는 산출물

클래스 방식의 상속을 구현할 때의 목표는 Child()라는 생성자 함수로 생성된 객체들이 다른 생성자 함수인 Parent()의 프로퍼티를 가지도록 하는 것이다.

Parent() 생성자와 Child() 생성자를 정의한 예제는 다음과 같다.

			//부모 생성자
			function Parent(name){
				this.name = name || 'Adam';
			}

			//생성자의 프로토타입에 기능을 추가한다.
			Parent.prototype.say = function(){
				return this.name;
			};

			//아무 내용이 없는 자식 생성자
			function Child(name){}

			//상속
			inherit(Child, Parent);
		

클래스 방식의 상속 패턴 #1 - 기본 패턴

가장 널리 쓰이는 기본적인 방법은 Parent() 생성자를 사용해 객체를 생성한 다음, 이 객체를 Child()의 프로토 타입에 할당하는 것이다. 재사용 가능한 inherit() 함수의 첫번째 구현 예제는 다음과 같다.

			function inherit(C, P){
				C.prototype = new P();
			}
		

여기서는 prototype 프로퍼티가 함수가 아니라 객체를 가리키게 하는 것이 중요하다. 즉 프로토타입이 부모 생성자 함수 자체가 아니라 부모 생성자 함수로 생성한 객체 인스턴스를 가리켜야 한다. 이 패턴이 제대로 동작하려면 new 연산자가 반드시 필요하다.

이렇게 구현한 후에 애플리케이션에서 new Child()를 사용해 객체를 생성하면, 프로토타입을 통해 Parent() 인스턴스의 기능을 물려받게 된다.

			var kid = new Child();
			kid.say();		//"Adam"
		

이 패턴을 사용하면 부모 객체의 프로토타입에 추가된 프로퍼티와 메서드들과 함께, 부모 객체 자신의 프로퍼티도 모두 물려받게 된다.

| 패턴 #1의 단점

이 패턴의 단점 중 하나는 부모 객체의 this에 추가된 객체 자신의 프로퍼티와 프로토타입 프로퍼티를 모두 물려받게 된다는 점이다. 대부분의 경우 객체 자신의 프로퍼티는 특정 인스턴스에 한정되어 재사용할 수 없기 때문에 필요가 없다. 범용 inherit() 함수는 인자를 처리하지 못하는 문제도 가지고 있다. 즉 자식 생성자에 인자를 넘겨도 부모 생성자에게 전달하지 못한다.

			var kid = new Child('Seth');
			kid.say();		//"Adam"
		

클래스 방식의 상속 패턴 #2 - 생성자 빌려쓰기

이번 패턴은 자식에서 부모로 인자를 전달하지 못했던 패턴 #1의 문제를 해결한다. 이 패턴은 부모 생성자 함수의 this에 자식 객체를 바인딩한 다음, 자식 생성자가 받은 인자들을 모두 넘겨준다.

			function Child(a,c,b,d){
				Parent.apply(this, arguments);
			}
		

이렇게 하면 부모 생성자 함수 내부의 this에 추가된 프로퍼티만 물려받게 된다. 프로토타입에 추가된 멤버는 상속되지 않는다.

생성자 빌려쓰기 패턴을 사용하면, 자식 객체는 상속된 멤버의 복사본을 받게 된다. 클래스 방식의 패턴 #1에서 자식 객체가 상속된 멤버의 참조를 물려받은 것과는 다르다.

			//부모 생성자
			function Article(){
				this.tags = ['js', 'css'];
			}
			var article = new Article();

			//클래스 방식의 패턴 #1을 사용해 article 객체를 상속하는 blog 객체를 생성한다.
			function BlogPost(){}
			BlogPost.prototype = article;
			var blog = new BlogPost();

			//생성자 빌려쓰기 패턴을 사용해 article을 상속하는 page 객체를 생성한다.
			function StaticPage(){
				Article.call(this);
			}
			var page = new StaticPage();

			alert(article.hasOwnProperty('tags'));		//true
			alert(blog.hasOwnProperty('tags'));			//false
			alert(page.hasOwnProperty('tags'));			//true
		

위 예제에서 부모인 Article()에 대한 상속은 두가지 방식(즉, 참조와 복사)으로 이루어진다. 기본 패턴을 적용한 blog 객체는 tags를 자기 자신의 프로퍼티로 가진 것이 아니라 프로토타입을 통해 접근하기 때문에 hasOwnProperty()로 확인하면 false가 반환된다. 그러나 생성자만 빌려쓰는 방식으로 상속받은 page 객체는 부모의 tags 멤버에 대한 참조를 얻는 것이 아니라 복사본을 얻게 되므로 자기 자신의 tags 프로퍼티를 가진다.

상속된 tags 프로퍼티를 수정할 때의 차이점은 다음과 같다.

			blog.tags.push('html');
			page.tags.push('php');
			alert(article.tags.join(', '));		//"js, css, html"
		

blog 객체가 tags 프로퍼티를 수정하면 동시에 부모의 멤버도 수정된다. 본질적으로 blog.tags와 article.tags는 동일한 배열을 가리키고 있기 때문이다. 그러나 page.tags에 적용된 변경 사항은 부모인 article에 영향을 미치지 않는다. page.tags는 상속 과정에서 별개로 생성된 복사본이기 때문이다.

| 프로토타입 체인

			//부모 생성자
			function Parent(name){
				this.name = name || 'Adam';
			}

			//생성자의 프로토타입에 기능을 추가한다.
			Parent.prototype.say = function(){
				return this.name;
			};

			//자식생성자
			function Child(name){
				Parent.apply(this, arguments);
			}

			var kid = new Child('Patrick');
			kid.name;			//'Patrick'
			typeof kid.say;	//'undefined'
		

생성자 빌려쓰기 패턴을 이용하면 새로 생성된 Child 객체와 Parent 사이에 링크가 존재하지 않는다. Child.prototype은 전혀 사용되지 않았기 때문에 그냥 빈 객체를 가리키고 있다. 이 패턴을 적용하면 kid는 자기 자신의 name프로퍼티를 가지지만 say() 메서드는 상속받을 수 없다. 따라서 say()를 호출하려고 하면 에러가 발생한다. 여기서의 상속은 부모가 가진 자신만의 프로퍼티를 자식으로 프로퍼티로 복사해주는 일회성 동작이며, _proto_라는 링크는 유지되지 않는다.

| 생성자 빌려쓰기 패턴의 장단점

프로토타입이 전혀 상속되지 않는다는 점은 분명히 이 패턴의 한계라 할 수 있다. 앞서 말했듯이 재사용되는 메서드와 프로퍼티는 인스턴스별로 재생성되지 않도록 프로토타입에 추가해야 하기 때문이다. 반면 부모 생성자 자신의 멤버에 대한 복사본을 가져올 수 있다는 것은 장점이다. 이 덕분에 자식이 실수로 부모의 프로퍼티를 덮어쓰는 위험을 방지할 수 있다.

클래스 방식의 상속 패턴 #3 - 생성자 빌려쓰고 프로토타입 지정하기

앞선 두 패턴을 결합하여 먼저 부모 생성자를 빌려온 후, 자식의 프로토타입이 부모 생성자를 통해 생성된 인스턴스를 가리키도록 지정한다.

			function Child(a, c, b, d){
				Parent.apply(this, arguments);
			}

			Child.prototype = new Parent();
		

이렇게 하면 자식 객체는 부모가 가진 자신만의 프로퍼티의 복사본을 가지게 되는 동시에, 부모의 프로토타입 멤버로 구현된 재사용가능한 기능들에 대한 참조 또한 물려받게 된다. 자식이 부모 생성자에 인자를 넘길 수도 있다. 즉, 부모가 가진 모든 것을 상속하는 동시에, 부모의 프로퍼티를 덮어쓸 위험없이 자신만의 프로퍼티를 마음놓고 변경할 수 있다. 반면 부모생성자를 비효율적으로 두번 호출한다는 점과 부모가 가진 자신만의 프로퍼티는 두번 상속된다.

			//부모 생성자
			function Parent(name){
				this.name = name || 'Adam';
			}

			//생성자의 프로토타입에 기능을 추가한다.
			Parent.prototype.say = function(){
				return this.name;
			};

			//자식생성자
			function Child(name){
				Parent.apply(this, arguments);
			}

			Child.prototype = new Parent();

			var kid = new Child('Patrick');
			kid.name;			//'Patrick'
			kid.say();			//'Patrick'
			delete kid.name;
			kid.say();			//'Adam'
		

say() 메서드는 제대로 상속되며 name이 두번 상속된것도 확인할 수 있다. 즉, 자식이 복삭본을 가지고 있는 name을 삭제한 후에도 프로토타입 체친을 통해 name에 접근할 수 있다.

클래스 방식의 상속 패턴 #4 - 프로토타입 공유

이번 패턴은 부모 생성자를 한번도 호출하지 않는다. 원칙적으로 재사용할 멤버는 this가 아니라 프로토타입에 추가되어야 한다. 따라서 상속되어야 하는 모든 것들도 프로토타입 안에 존재해야 한다. 그렇다면 부모의 프로토타입을 똑같이 자식의 프로토타입으로 지정하기만 하면 된다.

			function inherit(C, P){
				C.prototype = P.prototype;
			}
		

하지만 상속 체인의 하단 어딘가에 있는 자식이나 손자가 프로토타입을 수정할 경우, 모든 부모와 손자뻘의 객체에 영향을 미치는 단점이 있다.

클래스 방식의 상속 패턴 #5 - 임시 생성자 (프록시 생성자 활용 패턴)

다음 패턴은 프로토타입 체인의 이점은 유지하면서, 동일한 프로토타입을 공유할 때의 문제를 해결하기 위해 부모와 자식의 프로토타입 사이에 직접적인 링크를 끊는다.

패턴의 구현은 빈 함수 F()가 부모와 자식 사이에서 프록시(proxy) 기능을 맡는다. F()의 prototype 프로퍼티는 부모의 프로토타입을 가리킨다. 이 빈 함수의 인스턴스가 자식의 프로토타입이 된다.

			function inherit(C, P){
				var F = function(){};
				F.prototype = P.prototype;
				C.prototype = new F();
			}
		

이 패턴은 자식이 프로토타입의 프로퍼티만을 상속받는다. 부모 생성자에서 this에 추가한 멤버는 상속되지 않는다.

			var kid = new Child();
		

kid.name에 접근하면 undefinded라는 값을 얻게 된다. name은 부모 자신의 프로퍼티이기 때문이다. kid.say()에 접근하면 프로토타입 체인을 따라 Parent() 생성자의 프로토타입에서 이 메서드를 사용하게 된다. Parent()를 상속하는 모든 생성자와 이를 통해 생성되는 모든 객체들은 똑같이 이 지점에서 이메서드를 사용하기 때문이다. 즉 메모리상의 위치는 동일하다.

| 상위 클래스 저장

이 패턴을 기반으로 하여, 부모 원본에 대한 참조를 추가할 수도 있다. 다른 언어에서 상위 클래스에 대한 접근 경로를 가지는 것과 같은 기능이다. 이 프로퍼티를 uber라고 부르자.

			function inherit(C, P){
				var F = function(){};
				F.prototype = P.prototype;
				C.prototype = new F();
				C.uber = P.prototype;
			}
		

| 생성자 포인터 재설정

나중을 위해 생성자 함수를 가리키는 포인터를 재설정하는 것이다. 생성자 포인터를 재설정하지 않으면 모든 자식 객체들의 생성자는 Parent()로 지정돼 있을 것이고, 이런 상황은 유용성이 떨어진다.

			//부모와 자식의 상속관계
			function Parent(){}
			function Child(){}
			inherit(Child, Parent);

			//생성자를 확인해본다.
			var kid = new Child();
			kid.constructor.name;				// "Parent"
			kid.constructor === Parent;		// true
		

constructor 프로퍼티는 런타임 객체 판별에 유용하다. 거의 정보성으로만 사용되는 프로퍼티이기 때문에, 원하는 생성자 함수를 가리키도록 재설정해도 기능에는 영향을 미치지 않는다.

클래스 방식의 상속 패턴을 완결하는 최종 버전은 다음과 같다.

			function inherit(C, P){
				var F = function(){};
				F.prototype = P.prototype;
				C.prototype = new F();
				C.uber = P.prototype;
				C.prototype.constructor = C;
			}
		

이 패턴은 프록시 함수 또는 프록시 생성자 활용 패턴으로 불리기도 한다. 임시 생성자가 결국은 부모의 프로토타입을 가져오는 프록시로 사용되기 때문이다.

이 최종 버전에 대한 일반적인 최적화 방안은 상속이 필요할 때마다 임시(프록시) 생성자가 생성되지 않게 하는 것이다. 임시 생성자는 한 번만 만들어두고 임시 생성자의 프로토타입만 변경해도 충분하다. 즉시 실행 함수를 활용하면 프록시 함수를 클로저 안에 저장할 수 있다.

			var inherit = (function(){
				var F = function(){};
				return function(C, P){
					F.prototype = P.prototype;
					C.prototype = new F();
					C.uber = P.prototype;
					C.prototype.constructor = C;
				}
			}());
		

프로토타입을 활용한 상속

이 패턴에서는 클래스를 찾아볼 수 없다. 객체가 객체를 상속받는다. 재사용하려는 객체가 하나 있고, 또다른 객체를 만들어 이 첫번째 객체의 기능을 가져온다고 생각하면 된다.

			//상속해줄 객체
			var parent = {
				name : "Papa"
			};

			//새로운 객체
			var child = object(parent);

			function object(o){
				function F(){}
				F.prototype = o;
				return new F();
			}

			alert(child.name);	// "Papa"
		

위 코드는 객체 리터럴로 생성한 parent라는 객체와 동일한 프로퍼티와 메서드를 가지는 또다른 객체 child를 생성하기 위해 object()라는 함수를 정의한다. child 객체는 자기 자신의 프로퍼티를 가지지 않는 빈 객체이지만, __proto__ 링크 덕에 parent의 모든 기능을 가지고 있다.

| 논의

생성자 함수를 통해 부모를 생성한 경우 부모 객체 자신의 프로퍼티와 생성자 함수의 프로토타입에 포함된 프로퍼티가 모두 상속된다는 점에 유의해야 한다.

			//부모 생성자
			function Person(){
				this.name = "Adam";
			}

			Person.prototype.getName = function(){
				return this.name;
			};

			//Person 인스턴스를 생성한다.
			var papa = new Person();

			//이 인스턴스를 상속한다.
			var kid = object(papa);

			function object(o){
				function F(){}
				F.prototype = o;
				return new F();
			}

			alert(kid.getName());	// "Adam"
		

생성자 함수의 프로토타입 객체만 상속받을 수 있도록 변형해보자. 부모 객체가 어떻게 생성되었는 지와는 상관없이 객체가 객체를 상속한다는 점에 유념하라.

			//부모 생성자
			function Person(){
				this.name = "Adam";
			}

			Person.prototype.getName = function(){
				return this.name;
			};

			//이 인스턴스를 상속한다.
			var kid = object(Person.prototype);

			function object(o){
				function F(){}
				F.prototype = o;
				return new F();
			}

			typeof kid.getName;			//"function"
			typeof kid.name;			//"undefined"
		

| ECMAScript5의 추가사항

ECMAScript 5에서는 프로토타입을 활용한 상속 패턴이 언어의 공식 요소가 되었다. Object.create()가 이 패턴을구현하고 있다. 즉, object()와 같은 함수를 따로 만들지 않아도 이 기능이 언어에 내장된다.

			var child = Object.create(parent);
		

Object.create()은 두번째 선택적 매개변수로 객체를 받는다. 전달된 객체의 프로퍼티는 반환되는 child 객체 자신의 프로퍼티로 추가된다. 한번의 메서드 호출로 child 객체의 상속과 정의가 가능하믈 편리하다.

			var child = Object.create(parent, {
				age : {value : 2}
			});
			child.hasOwnProperty('age');		//true
		

프로퍼티 복사를 통한 상속 패턴

프로퍼티 복사를 통한 상속 패턴은 객체가 다른 객체의 기능을 단순히 복사를 통해 가져온다.

			function extend(parent, child){
				var i;
				child = child || {};
				for(i in parent){
					if(parent.hasOwnProperty(i)){
						child[i] = parent[i];
					}
				}
				return child;
			}

			var dad = {name: "Adam"};
			var kid = extend(dad);
			kid.name;	//"Adam"
		

부모의 멤버들에 대해 루프를 돌면서 자식에 복사한다. 두번째 매개변수인 child는 생략 가능하다. 인자가 생략되면 상속을 통해 기존 객체의 기능이 확장되는 대신, 새로운 객체가 생성, 반환된다. 이러한 구현을 '얕은 복사'라고도 한다.

반대로 '깊은 복사'복사하려는 프로퍼티가 객체나 배열인지 확인해보고, 객체 또는 배열이면 중첩된 프로퍼티까지 재귀적으로 순회하여 복사하는 것을 말한다. 자바스크립트에서 객체는 참조만 전달되기 때문에 얕은 복사를 통해 상속을 실행한 경우, 자식 쪽에서 객체 타입인 프로퍼티 값을 수정하면 부모의 프로퍼티도 수정되어 버린다. 함수 역시 객체이고 참조만 전달하기 때문에, 메서드는 이런 방식으로 복사되는 게 더 좋을 수 있다. 그러나 객체와 배열을 다룰 때는 예기치 못한 결과가 나올 수 있다.

			var dad = {
				counts: [1,2,3],
				reads : {paper : true}
			};
			var kid = extend(dad);
			kid.counts.push(4);
			dad.counts.toString();			//"1,2,3,4"
			dad.reads === kid.reads;		//true
		

extend() 함수가 깊은 복사를 후행할 수 있도록 할려면 프로퍼티의 타입이 객체인지 확인한 후, 객체가 맞으면 이 프로퍼티를 재귀적으로 복사하는 기능만 추가하면 된다. 객체가 정말 객체인지 아니면 배열인지에 대한 확인도 필요하다.

			function extendDeep(parent, child){
				var i,
					toStr = Object.prototype.toString,
					astr = "[object Array]";

				child = child || {};

				for(i in parent){
					if(parent.hasOwnProperty(i)){
						if(typeof parent[i] === "object"){
							child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
							extendDeep(parent[i], child[i]);
						}else{
							child[i] = parent[i];
						}
					}
				}
				return child;
			}

			var dad = {
				counts: [1,2,3],
				reads : {paper : true}
			};
			var kid = extendDeep(dad);
			kid.counts.push(4);
			kid.counts.toString();			//'1,2,3,4'
			dad.counts.toString();			//'1,2,3'

			dad.reads === kid.reads;		//false
			kid.reads.paper = false;
			kid.reads.web = true;
			dad.reads.paper;				//true
		

이 프로퍼티 복사 패턴은 매우 간단하고 널리 사용된다. jQuery의 extend() 메서드는 깊은 복사를 수행한다. 이 패턴은 프로토타입과 전혀 관련이 없다.

믹스-인

하나의 객체를 복사하는게 아니라 여러 객체에서 복사해온 것을 한 객체 안에 섞어 넣을 수도 있다. 함수에 인자로 전달된 객체를 받아 루프를 돌면서 모든 프로퍼티를 복사하면 된다.

			function mix(){
				var arg, prop, child = {};
				for(arg=0; arg<arguments.length; arg+=1){
					for(prop in arguments[arg]){
						if(arguments[arg].hasOwnProperty(prop)){
							child[prop] = arguments[arg][prop];
						}
					}
				}
				return child;
			}

			var cake = mix(
				{eggs: 2, large: true},
				{butter: 1, salted: true},
				{flour: "3 cups"},
				{sugar: "sure!"}
			);
		

범용 믹스-인 함수에 여러개의 객체를 넘기면, 이 객체들의 모든 프로퍼티를 가진 새로운 객체가 반환될 것이다. 믹스-인은 단순히 루프를 돌고, 프로퍼티를 복사한 것 뿐이기 때문에 부모들과의 연결 고리는 끊어진 상태다.

메서드 빌려쓰기

어떤 객체에서 메서드 한두 개만 마음에 드는 경우가 있다. 쓸 일이 없는 모든 메서드를 상속받지 않고 원하는 메서드만 골라서 사용하고 싶다면 메서드 빌려쓰기 패턴을 사용하면 된다. 이 패턴은 함수의 메서드인 call()과 apply()를 활용한다. call()은 호출할 함수에 전달할 매개변수를 별개의 인자들로 받고 apply()는 배열로 받는다는 점이 다르다.

			// call() 예제
			notmyobj.doStuff.call(myobj, param1, p2, p3);
			
			// apply() 예제
			notmyobj.doStuff.apply(myobj, [param1, p2, p3]);
		

myobj라는 객체가 있고 notmyobj라는 객체는 doStuff라는 유용한 메서드를 가지고 있다고 하자. 상속을 거쳐 myobj가 필요하지 않은 모든 메서드를 물려받기보다는, 간단히 doStuff() 메서드만 일시적으로 빌려쓸수 있다.

| 예제: 배열 메서드 빌려쓰기

이 패턴은 배열 메서드를 빌려오는데 많이 사용된다. 배열은 유용한 메서드를 많이 갖고 있다. 따라서 arguments와 같이 배열과 비슷한 객체들이 배열의 slice() 같은 메서드를 빌려쓸 수 있다.

			function f(){
				var args = [].slice.call(arguments, 1, 3);
				return args;
			}
			
			f(1,2,3,4,5,6);		// [2,3]이 반환된다.
		

이 예제에서는 배열의 메서드를 사용하기 위해 빈 배열을 생성했다. 좀더 길게 쓰자면, Array.prototype.slice.call(...)을 사용하여 Array의 프로토타입에서 직접 메서드를 빌려올 수 있다.

| 빌려쓰기와 바인딩

call()이나 apply()를 사용하거나 단순한 할당을 통해 메서드를 빌려오게 되면, 빌려온 메서드 안에서 this가 가리키는 객체는 호출식에 따라 정해지게 된다. 즉, 이 함수 포인터가 전역 객체를 가리키거나 이 함수를 콜백 함수로 저달하는 경우 메서드 안에서 this는 전역 객체를 가리키기 때문에 코드가 제대로 동작하지 않는다.

			var one = {
				name : "object",
				say : function(greet){
					return greet + ", " + this.name; 	
				}
			};
			
			one.say('hi');		// "hi, object"
		

또 다른 객체 two는 say() 메서드를 갖고 있지 않지만 one에서 빌려올 수 있다.

			var two = {
				name : "another object"
			};
			
			one.say.apply(two, ['hello']);
		
			//함수를 변수에 할당하면 함수 안의 this는 전역 객체를 가리키게 된다.
			var say = one.say;
			say('hoho');		// "hoho, undefined"
			
			//콜백 함수로 전달할 경우
			var yetanother = {
				name : "Yet another object",
				method : function(callback){
					return callback('Holla');
				}
			};
			yetanother.method(one.say);		//"Holla, undefined"
		

메서드와 객체를 묶어놓기(바인딩) 위해서는 다음과 같은 간단한 함수를 사용할 수 있다.

			function bind(o, m){
				return function(){
					return m.apply(o, [].slice.call(arguments));	
				};
			}
			
			var twosay = bind(two, one.say);
			twosay('yo');		// "yo, another object"
		

이 bind() 함수는 o라는 객체와 m이라는 메서드를 인자로 받은 다음, 이 둘을 바인딩한 새로운 함수를 반환한다. 반환되는 새로운 함수는 클로저를 통해 o 와 m에 접근할 수 있다. 따라서 bind()에서 함수를 반환한 다음에도, 내부 함수는 원본 객체를 가리키는 o와 원본 메서드를 가리키는 m에 접근할 수 있다.

보다시피 twosay()는 전역 함수로 생성되었지만, this가 전역 객체를 가리키지 않고 bind()에 전달된 two 객체를 가리킨다. twosay() 함수를 어떻게 호출하든, this는 항상 two에 바인딩된다.

| Function.prototype.bind()

ECMAScript 5에서는 Function.prototype에 bind() 메서드가 추가되어 쉽게 사용할 수 있다. 따라서 다음과 같은 표현식이 가능하다.

			var newFunc = obj.someFunc.bind(myobj, 1, 2, 3);
		

이 코드는 myobj와 someFunc()를 바인딩하며, someFunc()에 세개의 인자를 넘겨준다. ES5가 구현되지 않은 환경에서 프로그램을 실행할 때는 다음과 같이 한다.

			if(typeof Function.prototype.bind === "undefined"){
				Function.prototype.bind = function(thisArg){
					var fn = this,
						slice = Array.prototype.slice,
						args = slice.call(arguments, 1);
						
					return function(){
						return fn.apply(thisArg, args.concat(slice.call(arguments)));	
					};
				};
			}
			
			var twosay2 = one.say.bind(two);
			twosay2("Bonjour");		// "Bonjour, another object"
		

디자인 패턴


싱글톤(Singleton)

싱글톤 패턴은 특정 클래스의 인스턴스를 오직 하나만 유지한다. 자바스크립트에는 새로운 객체를 만들면 실제로 이 객체는 다른 어떤 객체와도 같지 않기 때문에 이미 싱글톤이다. 객체 리터럴로 만든 단순한 객체 또한 싱글톤의 예다.

			var obj = {
				myprop : "my value"
			};

			var obj2 = {
				myprop : "my value"
			};
			
			obj === obj2;	//false
		

| new 사용하기

자바스크립트에는 생성자 함수를 사용해 객체를 만드는 new 구문이 있고, 이 구문을 사용해서 싱글톤을 구현할 수 있다. 동일한 생성자로 new를 사용하여 여러개의 객체를 만들 경우, 실제로는 동일한 객체에 대한 새로운 포인터만 반환하도록 구현하는 것이다.

			var uni = new Universe();
			var uni2 = new Universe();
			uni === uni2;		// true
		

그러나 객체의 인스턴스인 this가 생성되면 Universe 생성자가 이를 캐쉬하는데 인스턴스를 저장하기 위해 전역변수를 사용하는 것은 추천하지 않는다. 그래서 생성자의 스태틱 프로퍼티에 인스턴스를 저장하는 방법과 인스턴스를 클로저로 감싸 인스턴스를 비공개로 만들어 생성자 외부에서 수정할 수 없게 하는 방법을 살펴보자.

| 스태틱 프로퍼티에 인스턴스 저장하기

다음은 Universe 생성자의 스태틱 프로퍼티 내부에 단일 인스턴스를 저장하는 예제이다.

			function Universe(){
				//이미 instance가 존재하는가?
				if(typeof Universe.instance === "object"){
					return Universe.instance;
				}

				//정상적으로 진행한다.
				this.start_time = 0;
				this.bang = "Big";

				//인스턴스를 캐시한다.
				Universe.instance = this;

				//함축적인 반환
				//return this;
			}

			//테스트
			var uni = new Universe();
			var uni2 = new Universe();
			uni === uni2;		//true
		

스태틱 프로퍼티에 인스턴스를 저장하는 경우 유일한 단점은 instance가 공개되어 외부에서 값을 변경할 수 있다는 점이다.

| 클로저에 인스턴스 저장하기

다음은 클로저를 사용해 단일 인스턴스를 보호하는 방법이다. 비공개 스태틱 멤버 패턴을 사용해서 구현할 수 있다.

			function Universe(){
				//캐싱된 인스턴스
				var instance = this;

				//정상적으로 진행한다.
				this.start_time = 0;
				this.bang = "Big";

				//생성자를 재작성한다.
				Universe =  function(){
					return instance;
				};
			}

			//테스트
			var uni = new Universe();
			var uni2 = new Universe();
			uni === uni2;		//true
		

단점은 재작성된 함수는 재정의 시점 이전에 원본 생성자에 추가된 프로퍼티를 잃어버린다는 점이다.

			//프로토타입에 추가한다.
			Universe.prototype.nothing = true;
			var uni = new Universe();

			//첫번째 객체가 만들어진 이후
			//다시 프로토타입에 추가한다.
			Universe.prototype.everything = true;
			var uni2 = new Universe();

			//원래의 프로토타입만 객체에 연결된다.
			uni.nothing;		//true
			uni2.nothing;		//true
			uni.everything;	//undefined
			uni2.everything;	//undefined

			uni.constructor.name;		//"Universe"
			uni.constructor === Universe;		//false
		

uni.constructor가 더이상 Universe() 생성자와 같지 않은 이유는 uni.constructor가 재정의된 생성자가 아닌 원본 생성자를 가리키고 있기 때문이다.

프로토타입과 생성자 포인터가 제대로 동작해야 하는 것이 요구사항이라면, 몇가지 간단한 수정으로 이를 만족시킬 수 있다.

			function Universe(){
				//캐싱된 인스턴스
				var instance = this;

				//생성자를 재작성한다.
				Universe =  function Universe(){
					return instance;
				};

				//prototype 프로퍼티를 변경한다.
				Universe.prototype = this;

				//instance
				instance = new Universe();

				//생성자 포인터를 재지정한다.
				instance.constructor = Universe;

				//정상적으로 진행한다.
				this.start_time = 0;
				this.bang = "Big";

				return instance;
			}

			//테스트
			Universe.prototype.nothing = true;
			var uni = new Universe();

			Universe.prototype.everything = true;
			var uni2 = new Universe();

			uni.nothing;		//true
			uni2.nothing;		//true
			uni.everything;	//true
			uni2.everything;	//true

			uni.constructor === Universe;		//true
		

또 다른 대안으로 생성자와 인스턴스를 즉시 실행 함수로 감싸는 방법이 있다. 생성자가 최초로 호출되면, 생성자는 객체를 생성하고 비공개 instance를 가리킨다. 두번째 호출부터는 단순히 비공개 변수를 반환한다.

			
			var Universe;

			(function (){

				var instance;

				Universe =  function Universe(){

					if(instance){
						return instance;
					}

					instance = this;

					//정상적으로 진행한다.
					this.start_time = 0;
					this.bang = "Big";
				};
			}());
		

팩토리(Factory)

팩토리 패턴의 목적은 객체들을 생성하는 것이다. 팩토리 패턴은 흔히 클래스 내부에 서 또는 클래스의 스태틱 메서드로 구현되며, 다음과 같은 목적으로 사용된다.

팩토리 메서드로 만들어진 객체들은 의도적으로 동일한 부모 객체를 상속한다. 즉 이들은 특화된 기능을 구현하는 구체적인 서브 클래스들이다.

			var corolla = CarMaker.factory('Compact');
			var solstice = CarMaker.factory('Convertible');
			var cherokee = CarMaker.factory('SUV');
			corolla.drive();			// "Vroom, I have 4 doors"
			solstice.drive();			// "Vroom, I have 2 doors"
			cherokee.drive();			// "Vroom, I have 17 doors"

			var corolla = CarMaker.factory('Compact');
		

이 메서드는 런타임시 문자열로 타입을 받아 해당 타입의 객체를 생성하고 반환한다. new와 함께 생성자를 사용하지 않고, 객체 리터럴도 보이지 않는다. 문자열로 식별되는 타입에 기반하여 객체들을 생성하는 함수가 있을 뿐이다.

다음은 이 코드가 동작하게 만드는 팩토리 패턴 구현 예제다.

			// 부모 생성자
			function CarMaker(){}

			// 부모의 메서드
			CarMaker.prototype.drive = function(){
				return "Vroom, I have " + this.doors + " doors";
			};

			// 스태틱 factory 메서드
			CarMaker.factory = function(type){
				var constr = type,
					newcar;

				// 생성자가 존재하지 않으면 에러를 발생한다.
				if (typeof CarMaker[constr] !== "function"){
					throw {
						name : "Error",
						message : constr + " doesn't exit"
					};
				}

				// 생성자의 존재를 확인했으므로 부모를 상속한다.
				// 상속은 단 한번만 실행하도록 한다.
				if (typeof CarMaker[constr].prototype.drive !== "function"){
					CarMaker[constr].prototype = new CarMaker();
				}

				// 새로운 인스턴스를 생성한다.
				newcar = new CarMaker[constr]();

				// 다른 메서드 호출이 필요하면 여기서 실행한 후, 인스턴스를 반환한다.
				return newcar;
			};

			//구체적인 자동차 메이커들을 선언한다.
			CarMaker.Compact = function(){
				this.doors = 4;
			};
			CarMaker.Convertible = function(){
				this.doors = 2;
			};
			CarMaker.SUV = function(){
				this.doors = 24;
			};
		

여기서는 공통적으로 반복되는 코드를 모든 생성자에서 반복하는 대신 팩토리 메서드 안에 모아놓기 위해 상속을 사용했다.

반복자(Iterator)

반복자 패턴에서, 객체는 일종의 집합적인 데이터를 가진다. 데이터가 저장된 내부 구조는 복잡하더라도 개별 요소에 쉽게 접근할 방법이 필요할 것이다.

반복자 패턴에서, 객체는 next() 메서드를 제공한다. next()를 연이어 호출하면 반드시 다음 순서의 요소를 반환해야 한다.

agg라는 객체가 있다고 하자. 다음과 같이 단순히 루프 내에서 next()를 호출하여 개별 데이터 요소에 접근할 수 있다.

			var element;
			while (element = age.next()){
				// element로 어떤 작업을 수행한다.
				console.log(element);
			}
		

반복자 패턴에서 객체는 보통 hasNext()라는 편리한 메서드도 제공한다. 객체의 사용자는 이 메서드로 데이터의 마지막에 다다랐는지 확인할 수 있다. hasNext()를 사용하여 모든 요소에 순차적으로 접근하는 방법은 다음과 같다.

			while (agg.hasNext()){
				// 어떤 작업을 수행한다.
				console.log(agg.next());
			}
		

반복자 패턴을 구현할 때, 데이터는 물론 다음에 사용할 요소를 가리키는 포인터(인덱스)도 비공개로 저장해두는 것이 좋다.

다음 예제에서 데이터는 단순한 배열로, 다음번 순서의 요소를 가져오는 next()는 배열 요소를 하나 걸러 반환한다고 가정하자.

			var agg = (function(){
				
				var index = 0,
					data = [1,2,3,4,5],
					length = data.length;

				return {
					next : function(){					// 다음 요소를 반환한다.
						var element;
						if(!this.hasNext()){
							return null;
						}
						element = data[index];
						index = index + 2;
						return element;
					},
					hasNext : function(){			// 데이터의 마지막에 다다랐는지 확인한다.
						return index < length;
					},
					rewind : function(){				// 포인터를 다시 처음으로 되돌린다.
						index = 0;
					},
					current : function(){				// 현재의 요소를 반환한다.
						return data[index];
					}
				};
			}());
		
			//이 루프는 1, 3, 5를 찍을 것이다.
			while (agg.hasNext()){
				// 어떤 작업을 수행한다.
				console.log(agg.next());
			}

			//처음으로 되돌린다.
			agg.rewind();
			console.log(agg.current());		//1
		

장식자(Decorator)

장식자 패턴을 이용하면 런타임시 부가적인 기능을 객체에 동적으로 추가할 수 있다.

| 사용방법

어떤 물건을 파는 웹 애플리케이션을 만들고 있다. 각각의 새로운 판매건은 새로운 sale 객체가 된다. sale.getPrice() 메서드를 호출하면 가격을 반환한다. 상황에 따라 추가 기능으로 다음과 같이 이 객체를 장식할 수 있다.

			var sale = new Sale(100);	// 가격은 100달러이다.
			sale = sale.decorate('fedtax');		//연방세를 추가한다.
			sale = sale.decorate('quebec');	//지방세를 추가한다.
			sale = sale.decorate('money');		//통화형식을 지정한다.
			sale.getPrice();			//$112,88
		

예제에서 처럼, 장식자 패턴은 런타임시에 기능을 추가하고 객체를 변경하는 유연한 방법이다.

| 구현

장식자 패턴을 구현하기 위한 한가지 방법은 모든 장식자 객체에 특정 메서드를 포함시킨 후, 이 메서드를 덮어쓰게 만드는 것이다. 각 장식자는 사실 이전의 장식자로 기능이 추가된 객체를 상속한다. 장식 기능을 담당하는 메서드들은 uber(상속된 객체)에 있는 동일한 메서드를 호출하여 값을 가져온 다음 추가 작업을 덧붙이는 방식으로 진행한다.

먼저 생성자와 프로토타입 메서드부터 시작한다.

			function Sale(price){
				this.price = price || 100;
			}

			Sale.prototype.getPrice = function(){
				return this.price;
			};
		

장식자 객체들은 생성자 프로퍼티 Sale.decorators의 프로퍼티로 구현된다.

			Sale.decorators = {};
		

예제 장식자를 하나 살펴보자. 이 장식자 객체는 getPrice() 메서드를 특화하여 구현했다. 이 메서드가 처음에는 부모의 메서드로부터 값을 가져온 다음 그 값을 변경한다는 점에 유의하라.

			Sale.decorators.fedtax = {
				getPrice : function(){
					var price = this.uber.getPrice();
					price += price * 5 / 100;
					return price;
				}
			};

			Sale.decorators.quebec = {
				getPrice : function(){
					var price = this.uber.getPrice();
					price += price * 7.5 / 100;
					return price;
				}
			};

			Sale.decorators.money = {
				getPrice : function(){
					return "$" + this.uber.getPrice().toFixed(2);
				}
			};

			Sale.decorators.cdn = {
				getPrice : function(){
					return "CDN$ " + this.uber.getPrice().toFixed(2);
				}
			};
		

이제 마지막으로, decorate() 메서드를 살펴보자.

			sale = sale.decorate('fedtax');
		

'fedtax' 문자열은 Sale.decorators.fedtax에 구현된 장식자 객체에 대응한다. 새롭게 꾸며진 newObj 객체는 현재 주어져 있는 this 객체를 상속할 것이다. 상속 부분을 수행하기 위해서, 임시생성자 패턴을 사용하자. 자식 객체가 부모 객체에 접근할 수 있도록 newobj에 uber 프로퍼티도 지정해준다. 그러고 나서 모든 장식자들의 추가 프로퍼티들을 새로 꾸며진 newobj 객체로 복사한다. 마지막으로 newobj가 반환된다. 이 예제에서는 newobj가 새롭게 갱신된 sale 객체다.

			Sale.prototype.decorate = function(decorator){
				var F = function(){},
					overrides = this.constructor.decorators[decorator],
					i, newobj;
				
				F.prototype = this;
				newobj = new F();
				newobj.uber = F.prototype;
				for(i in overrides){
					if(overrides.hasOwnProperty(i)){
						newobj[i] = overrides[i];
					}
				}
				return newobj;
			};
		

| 목록을 사용한 구현

이번엔 약간 다른 구현방법이다. 이 방법은 자바스크립트의 동적 특성을 최대한 활용하며 상속은 전혀 사용하지 않는다. 각각의 꾸며진 메서드가 체인 안에 있는 이전의 메서드를 호출하는 대신에, 간단하게 이전 메서드의 결과를 다음 메서드에 매개변수로 전달한다. 이 구현방법을 사용하면 장식을 취소하거나 제거하기 쉽다. 장식자 목록에서 요소를 삭제하기만 하면 된다. decorate()에서 반환된 값을 다시 객체에 할당하지 않기 때문에 조금 더 간단하다.

			var sale = new Sale(100);	// 가격은 100달러이다.
			sale = sale.decorate('fedtax');		//연방세를 추가한다.
			sale = sale.decorate('quebec');	//지방세를 추가한다.
			sale = sale.decorate('money');		//통화형식을 지정한다.
			sale.getPrice();			//$112,88

			function Sale(price){
				this.price = (price>0) || 100;
				this.decorators_list = [];
			}

			Sale.decorators = {};

			Sale.decorators.fedtax = {
				getPrice : function(price){
					return price + price * 5 / 100;
				}
			};

			Sale.decorators.quebec = {
				getPrice : function(price){
					return price + price * 7.5 / 100;
				}
			};

			Sale.decorators.money = {
				getPrice : function(price){
					return "$" + price.toFixed(2);
				}
			};
		

decorate()는 단지 장식자를 목록에 추가할 뿐이고 getPrice()가 모든 일을 한다. 즉 현재 추가된 장식자들의 목록을 조사하고, 각각의 getPrice() 메서드를 호출하면서 이전 반환 값을 전달한다.

			Sale.prototype.decorate = function(decorator){
				this.decorators_list.push(decorator);
			};

			Sale.prototype.getPrice = function(){
				var price = this.price,
					i,
					max = this.decorators_list.length,
					name;

				for(i=0; i<max; i+= 1){
					name = this.decorators_list[i];
					price = Sale.decorators[name].getPrice(price);
				}

				return price;
			};
		

퍼사드(Facade)

퍼사드 패턴은 객체의 대체 인터페이스를 제공한다. 메서드가 너무 많은 작업을 처리하지 않게 설계하는 것은 좋은 방법이나 메서드 숫자가 엄청나게 많아질 수 있다. 두개 이상의 메서드가 함께 호출되는 경우가 많다면, 이런 메서드 호출을 하나로 묶어주는 새로운 메서드를 만드는게 좋다.

예를 들어 브라우저 이벤트를 처리할 때 사용하는 다음과 같은 메서드를 생각해 보자

			stopPropagation();		//이벤트가 상위 노드로 전파되지 않게 중단시킨다.
			preventDefault();		//브라우저의 기본 동작을 막는다.
		

위 두메서드는 서로 다른 목적을 가지고 있으나 한번에 호출되는 경우도 있다. 이 둘을 함께 호출하는 퍼사드 메서드를 생성하는게 좋다.

			var myevent = {
				//...
				stop : function(e){
					e.preventDefault();
					e.stopPropagation();
				}
				//...
			}
		

퍼사드 패턴은 브라우저 스크립팅에도 적합하다. IE에서의 이벤트 API의 차이를 처리하는 코드를 추가해보자.

			var myevent = {
				//...
				stop : function(e){
					// IE 이외의 모든 브라우저
					if(typeof e.preventDefault === 'function'){
						e.preventDefault();
					}
					if(typeof e.stopPropagation === 'function'){
						e.stopPropagation();
					}
					
					// IE
					if(typeof e.returnValue === 'boolean'){
						e.returnValue = false;
					}
					if(typeof e.cancelBubble === 'boolean'){
						e.cancelBubble = true;
					}
				}
				//...
			}
		

프록시(Proxy)

프록시 디자인 패턴에서는 하나의 객체가 다른 객체에 대한 인터페이스로 동작한다. 프록시는 클라이언트 객체와 실제 대상 객체 사이에 존재하면서 접근을 통제한다.

프록시 패턴의 한예로, 게으른 초기화를 들 수 있다. 객체를 초기화하는 데 많은 비용이 들지만, 실제로 초기화한 후에는 한번도 사용하지 않는다고 해보자. 이런 경우에 실제 대상 객체에 대한 인터페이스로 프록시를 사용하면 도움이 된다. 프록시는 초기화 요청을 대신 받지만, 실제 대상 객체가 정말로 사용되기 전까지는 이 요청을 전달하지 않는다.

프록시 패턴은 실제 대상 객체가 비용이 많이 드는 작업을 할 때 유용하다. 네트웍크 요청과 같은 비용이 많이 드는 작업은 가능한 많은 HTTP 요청들을 하나로 결합하는게 효과적이다.

다음은 프록시 패턴을 이용한 예제로 동영상 제목을 클릭하면 제목 아래에 영역이 펼쳐지면서 동영상에 대한 상세정보와 함께 동영상을 재생할 수 있는 버튼이 나온다. proxy 객체를 추가해 http 객체와 videos 객체간의 통신을 전담하게 만들 수 있다. videos 객체는 HTTP 서비스를 직접 호출하는 대신 proxy를 호출한다. proxy는 요청을 전달하기 전에 잠시 대기하다가 다른 요청이 들어오면 하나로 병합하는 방식으로 개별 요청들을 병합시킴으로써 사용자 경험의 속도를 개선하는데 도움이 된다. 웹서버 역시 처리할 요청의 수가 줄어들기 때문에 부하를 상당히 덜 수 있다. 또한 프록시는 새로운 cache 프로퍼티에 이전 요청의 결과를 캐시해두면, 동일한 요청이 들어오면 캐시된 결과를 반환해서 네크워크 라운드트립을 줄인다.

| 프록시 패턴 예제

예제) http://www.jspatterns.com/book/7/proxy.html

			<h1>Dave Matthews vids</h1>
			<p><span id="toggle-all">Toggle Checked</span></p>
			<ol id="vids">
				<li><input type="checkbox" checked><a 
					href="http://new.music.yahoo.com/videos/--2158073">Gravedigger</a></li>
				<li><input type="checkbox" checked><a 
					href="http://new.music.yahoo.com/videos/--4472739">Save Me</a></li>    
				<li><input type="checkbox" checked><a 
					href="http://new.music.yahoo.com/videos/--45286339">Crush</a></li>
				<li><input type="checkbox" checked><a 
					href="http://new.music.yahoo.com/videos/--2144530">Don't Drink The Water</a></li>
				<li><input type="checkbox" checked><a 
					href="http://new.music.yahoo.com/videos/--217241800">Funny the Way It Is</a></li>    
				<li><input type="checkbox" checked><a 
					href="http://new.music.yahoo.com/videos/--2144532">What Would You Say</a></li>    
			</ol>
		
			var $ = function (id) {
				return document.getElementById(id);
			};

			var http = {
				makeRequest: function (ids, callback) {
					var url = 'http://query.yahooapis.com/v1/public/yql?q=',
						sql = 'select * from music.video.id where ids IN ("%ID%")',
						format = "format=json",
						handler = "callback=" + callback,
						script = document.createElement('script');
					
					sql = sql.replace('%ID%', ids.join('","'));
					sql = encodeURIComponent(sql);
					
					url += sql + '&' + format + '&' + handler;
					script.src = url;
					
					document.body.appendChild(script);   
				}
			};

			var proxy = {
				ids: [],
				delay: 50,
				timeout: null,
				callback: null,
				context: null,
				makeRequest: function (id, callback, context) {
					
					// add to the queue
					this.ids.push(id);
					
					this.callback = callback;
					this.context  = context;
					
					// set up timeout
					if (!this.timeout) {
						this.timeout = setTimeout(function () {
							proxy.flush();
						}, this.delay);
					}
				},
				flush: function () {
					
					http.makeRequest(this.ids, "proxy.handler");
							
					// clear timeout and queue
					this.timeout = null;
					this.ids = [];
					
				},
				handler: function (data) {        
					var i, max;
					
					// single video
					if (parseInt(data.query.count, 10) === 1) {
						proxy.callback.call(proxy.context, data.query.results.Video);
						return;
					}
					
					// multiple videos
					for (i = 0, max = data.query.results.Video.length; i < max; i += 1) {
						proxy.callback.call(proxy.context, data.query.results.Video[i]);
					} 
				}
			};


			var videos = {
				getPlayer: function (id) {
					return '' +
					'<object width="400" height="255" id="uvp_fop" allowFullScreen="true">' +
						'<param name="movie" value="http://d.yimg.com/m/up/fop/embedflv/swf/fop.swf"\/>' +
						'<param name="flashVars" value="id=v' + id + '&eID=1301797&lang=us&enableFullScreen=0&shareEnable=1"\/>' +
						'<param name="wmode" value="transparent"\/>' +
						'<embed ' +
							'height="255" ' +
							'width="400" ' +
							'id="uvp_fop" ' +
							'allowFullScreen="true" ' +
							'src="http://d.yimg.com/m/up/fop/embedflv/swf/fop.swf" ' +
							'type="application/x-shockwave-flash" ' +
							'flashvars="id=v' + id  + '&eID=1301797&lang=us&ympsc=4195329&enableFullScreen=1&shareEnable=1"' +
						 '\/>' +
					'<\/object>';
				},
				
				updateList: function (data) {
					var id, 
						html = '',
						info;
						
					if (data.query) {
						data = data.query.results.Video;
					}
					id = data.id;
					html += '<img src="' + data.Image[0].url + '" width="50" \/>';
					html += '<h2>' + data.title + '<\/h2>';
					html += '<p>' + data.copyrightYear + ', ' + data.label + '<\/p>';
					if (data.Album) {
						html += '<p>Album: ' + data.Album.Release.title + ', ' + data.Album.Release.releaseYear + '<br \/>';
					}
					html += '<p><a class="play" href="http://new.music.yahoo.com/videos/--' + id + '">» play<\/a><\/p>';
					info = document.createElement('div');
					info.id = "info" + id;
					info.innerHTML = html;
					$('v' + id).appendChild(info);
					
				},

				getInfo: function (id) {
					
					var info = $('info' + id);
					
					if (!info) {
						proxy.makeRequest(id, videos.updateList, videos);
						return;
					}
					
					if (info.style.display === "none") {
						info.style.display = '';
					} else {
						info.style.display = 'none';
					}   
				}
			};


			$('vids').onclick = function (e) {
				var src, id;
				
				e = e || window.event;
				src = e.target || e.srcElement;
				
				if (src.nodeName.toUpperCase() !== "A") {
					return;
				}
				
				if (typeof e.preventDefault === "function") {
					e.preventDefault();
				}
				e.returnValue = false;
				
				id = src.href.split('--')[1];
				
				if (src.className === "play") {
					src.parentNode.innerHTML = videos.getPlayer(id);
					return;
				} 
				
				src.parentNode.id = "v" + id;
				videos.getInfo(id);    
			};

			$('toggle-all').onclick = function (e) {

				var hrefs,
					i, 
					max,
					id;
				
				hrefs = $('vids').getElementsByTagName('a');
				for (i = 0, max = hrefs.length; i < max; i += 1) {
					// skip play links
					if (hrefs[i].className === "play") {
						continue;
					}
					// skip unchecked
					if (!hrefs[i].parentNode.firstChild.checked) {
						continue;
					}
					
					id = hrefs[i].href.split('--')[1];
					hrefs[i].parentNode.id = "v" + id;
					videos.getInfo(id);
				}
			};		
		

중재자(Mediator)

크기에 상관없이 애플리케이션은 독립된 객체들로 만들어진다. 객체간의 통신은 유지보수가 쉽고 다른 객체를 건드리지 않으면서, 안전하게 수정할 수 있는 방식으로 이루어져야 한다. 그러나 애플리케이션이 점차 커지고, 더욱 더 많은 객체들이 추가되고 직접 통신하게 되면 서로간에 결합도가 높아져 바람직하지 않다. 객체들이 강하게 결합되면, 다른 객체들에 영향을 주지 않고 하나의 객체를 수정하기가 어렵다. 매우 간단한 변경도 어려워지고, 수정에 필요한 시간을 예측하는 것이 사실상 불가능해진다.

중재자 패턴은 결합도를 낮추고 유지보수를 쉽게 개선하여 이런 문제를 완화시킨다. 이 패턴에서 독립된 동료 객체들은 직접 통신하지 않고, 중재자 객체를 거친다. 동료 객체들은 자신의 상태가 변경되면 중재자에게 알리고, 중재자는 이런 변경 사항을 알아야 하는 다른 동료 객체들에게 알린다.

| 중재자 패턴 예제

다음은 두명의 플레이어 중 주어진 30초 동안 버튼을 더 많이 누르는 사람이 이기는 게임 애플리케이션이다. 플레이어 1은 1키를 누르고 플레이어 2는 0키를 누른다. 점수판에는 현재의 점수를 표시한다. 이 게임을 구성하는 객체들은 다음과 같다.

중재자는 다른 모든 객체에 대해 알고 있다. 중재자는 입력장치(키보드)와 통신하며, keypress 이벤트를 처리하고, 어떤 플레이어의 차례인지 결정해서 알려준다. 플레이어는 게임을 하면서 1점을 딸 때마다 득점 사실을 중재자에게 알려준다. 중재자는 점수판 객체에 플레이어의 점수를 전달한다. 전달된 점수는 차례로 화면에 표시된다. 중재자 이외의 객체들은, 다른 객체들에 대해 전혀 알지 못한다. 덕분에 게임에 플레이어를 추가하거나 게임의 남은 시간을 표시하는 등의 새로운 기능을 쉽게 추가할 수 있다.

  1. 1. 플레이어 객체들은 Player() 생성자로 만들어지고 points와 name 프로퍼티를 가진다. 프로토타입에 추가된 play() 메서드는 점수를 1점씩 올리고 점수 변화를 중재자에게 알린다.
    					// player constructor
    					function Player(name) {
    						this.points = 0;
    						this.name = name;
    					}
    					// play method
    					Player.prototype.play = function () {
    						this.points += 1;
    						mediator.played();
    					};				
    				
  2. 2. 점수판 객체는 update() 메서드를 가진다. 플레이어의 차례가 바뀔 때마다 중재자 객체가 이 메서드를 호출한다.
    					// the scoreboard object
    					var scoreboard = {
    						
    						// 점수를 표시할 HTML element
    						element: document.getElementById('results'),
    						
    						// 점수 표시를 갱신한다.
    						update: function (score) {
    							
    							var i, msg = '';
    							for (i in score) {
    								if (score.hasOwnProperty(i)) {
    									msg += '<p><strong>' + i + '<\/strong>: ';
    									msg += score[i];
    									msg += '<\/p>';
    								}
    							}
    							this.element.innerHTML = msg;
    						}
    					};
    				
  3. 3. 중재자 객체는 게임을 초기화하고, setup() 메서드 안에서 player 객체를 만들고, players 프로퍼티에 플레이어 객체들의 참조를 저장해둔다. played() 메서드는 차례가 바뀔 때마다 각 플레이어 객체에 의해 호출된다. 이 메서드는 score 해시를 업데이트한 다음 scoreboard 객체에 전달해 화면에 점수를 표시한다. 마지막 메서드인 keypress()는 키보드 이벤트를 처리하고 어떤 플레이어의 차례인지 판단해 알려준다.
    					var mediator = {
    						
    						// 모든 player 객체들
    						players: {},
    						
    						// 초기화
    						setup: function () {
    							var players = this.players;
    							players.home = new Player('Home');
    							players.guest = new Player('Guest');
    							
    						},
    						
    						// 누군가 play하고 점수를 업데이트한다.
    						played: function () {
    							var players = this.players,
    								score = {
    									Home:  players.home.points,
    									Guest: players.guest.points
    								};
    								
    							scoreboard.update(score);
    						},
    						
    						// 사용자 인터렉션을 핸들링한다.
    						keypress: function (e) {
    							e = e || window.event; // IE
    							if (e.which === 49) { // key "1"
    								mediator.players.home.play();
    								return;
    							}
    							if (e.which === 48) { // key "0"
    								mediator.players.guest.play();
    								return;
    							}
    						}
    					};
    				
  4. 4. 마지막으로 게임을 시작하고 종료시킨다.
    					// 시작
    					mediator.setup();
    					window.onkeypress = mediator.keypress;
    
    					// 30초 후에 게임을 종료시킨다.
    					setTimeout(function () {
    						window.onkeypress = null;
    						alert('Game over!');
    					}, 30000);
    				

감시자(Observer)

감시자 패턴은 클라이언트 측 자바스크립트 프로그래밍에서 널리 사용되는 패턴이다. mouseover, keypress와 같은 모든 브라우저 이벤트가 감시자 패턴의 예다. 감시자 패턴은 커스텀 이벤트라고 부르기도 하는데, 이는 브라우저가 발생시키는 이벤트가 아닌 프로그램에 의해 만들어진 이벤트를 뜻한다. 또 다른 이름으로 구독자/발행자 패턴이라고도 한다.

이 패턴이 주요 목적은 결합도를 낮추는 것이다. 어떤 객체가 다른 객체의 메서드를 호출하는 대신, 객체의 특별한 행동을 구독해 알림을 받는다. 구독자(subscriber)는 감시자라고도 부르며, 관찰되는 객체는 발행자(publisher) 또는 감시대상이라고 부른다. 발행자는 중요한 이벤트가 발생했을 때 모든 구독자에게 알려주며 주로 이벤트 객체의 형태로 메시지를 전달한다.

| 예제 #1: 잡지 구독

일간신문과 월간 잡지를 출판하는 paper라는 발행자가 있다. 구독자 joe는 출판될 때마다 알림을 받게 된다.

paper 객체에는 모든 구독자를 저장하는 배열인 subscribers 프로퍼티가 존재한다. 구독은 단지 이 배열에 구독자를 추가하는 것으로 이뤄진다. 이벤트가 발생하면 paper는 subscribers의 목록을 순회하면서 각 구독자에게 알린다. '알림'이란 구독자 객체의 메서드를 호출한다는 뜻이다. 따라서, 구독자는 구독할 때 자신의 메서드 중 하나를 paper의 subscribe() 메서드에 전달해야 한다.

paper는 unsubscribe() 메서드도 제공할 수 있다. unsubscribe는 subscribers 배열에서 구독자를 제거한다는 뜻이다. 마지막으로 publish() 메서드는 subscribers의 메서드들을 호출한다. 요약하면 발행자 객체는 다음의 멤버들을 가져야 한다.

세 메서드 모두 type 매개변수를 필요로 한다. 발행자는 신문 또는 잡지 출판 등 여러가지 이벤트를 발생시킬 수 있고, 구독자는 어떤 이벤트를 구독할 지 선택할 수 있기 때문이다.

			var publisher = {
				subscribers: {
					any: [] // '이벤트타입: 구독자의 배열'의 형식
				},
				subscribe: function (fn, type) {
					type = type || 'any';
					if (typeof this.subscribers[type] === "undefined") {
						this.subscribers[type] = [];
					}
					this.subscribers[type].push(fn);
				},
				unsubscribe: function (fn, type) {
					this.visitSubscribers('unsubscribe', fn, type);
				},
				publish: function (publication, type) {
					this.visitSubscribers('publish', publication, type);
				},
				visitSubscribers: function (action, arg, type) {
					var pubtype = type || 'any',
						subscribers = this.subscribers[pubtype],
						i,
						max = subscribers.length;
						
					for (i = 0; i < max; i += 1) {
						if (action === 'publish') {
							subscribers[i](arg);
						} else {
							if (subscribers[i] === arg) {
								subscribers.splice(i, 1);
							}
						}
					}
				}
			};
		

다음의 함수는 객체를 받아 발행자 객체로 바꿔준다. 단순히 해당 객체에 범용 발행자 메서드들을 복사해 넣는다.

			function makePublisher(o) {
				var i;
				for (i in publisher) {
					if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
						o[i] = publisher[i];
					}
				}
				o.subscribers = {any: []};
			}
		

paper 객체를 구현해보자. paper 객체는 일간 또는 월간으로 출판하는 일만 처리한다.

			var paper = {
				daily: function () {
					this.publish("big news today");
				},
				monthly: function () {
					this.publish("interesting analysis", "monthly");
				}
			};		
		

paper를 발행자로 만든다.

			makePublisher(paper);	
		

이제 구독자 객체 joe를 살펴보자.

			var joe = {
				drinkCoffee: function (paper) {
					console.log(paper + '를 읽었습니다.');
				},
				sundayPreNap: function (monthly) {
					console.log('잠들기 전에 ' + monthly + '를 읽고 있습니다');
				}
			};
		

그리고 paper의 구독자 목록에 joe를 추가한다(다르게 말해서 joe가 paper를 구독한다)

			paper.subscribe(joe.drinkCoffee);
			paper.subscribe(joe.sundayPreNap, 'monthly');
		

joe는 기본 이벤트 타입인 'any' 이벤트 발생시 호출될 메서드와 'monthly' 타입의 이벤트 발생시 호출될 메서드를 전달한다.

			paper.daily();			//big news today를 읽었습니다.
			paper.daily();			//big news today를 읽었습니다.
			paper.daily();			//big news today를 읽었습니다.
			paper.monthly();		//잠들기 전에 interesting analysis를 읽고 있습니다
		

| 예제 #2: 키 누르기 게임 (http://www.jspatterns.com/book/7/observer-game.html)

중재자 패턴에서 만들었던 게임을 감시자 패턴으로 구현해 보자. Player() 생성자로 player 객체를 생성하며, scoreboard 객체도 그대로 있다. 단지 mediator 객체가 game 객체로 바뀌게 된다. 감시자 패턴에서는 다른 객체들이 관심있는 이벤트를 구독한다. 예를 들어, scoreboard는 game의 'scorechange' 이벤트를 구독할 것이다.

새로운 발행자 객체는 다음과 같다.

			var publisher = {
				subscribers: {
					any: []
				},
				on: function (type, fn, context) {
					type = type || 'any';
					fn = typeof fn === "function" ? fn : context[fn];
					
					if (typeof this.subscribers[type] === "undefined") {
						this.subscribers[type] = [];
					}
					this.subscribers[type].push({fn: fn, context: context || this});
				},
				remove: function (type, fn, context) {
					this.visitSubscribers('unsubscribe', type, fn, context);
				},
				fire: function (type, publication) {
					this.visitSubscribers('publish', type, publication);
				},
				visitSubscribers: function (action, type, arg, context) {
					var pubtype = type || 'any',
						subscribers = this.subscribers[pubtype],
						i,
						max = subscribers ? subscribers.length : 0;
						
					for (i = 0; i < max; i += 1) {
						if (action === 'publish') {
							subscribers[i].fn.call(subscribers[i].context, arg);
						} else {
							if (subscribers[i].fn === arg && subscribers[i].context === context) {
								subscribers.splice(i, 1);
							}
						}
					}
				}
			};		
		

새로운 Player() 생성자는 다음과 같다.

			function Player(name, key) {
				this.points = 0;
				this.name = name;
				this.key  = key;
				this.fire('newplayer', this);
			}

			Player.prototype.play = function () {
				this.points += 1;
				this.fire('play', this);
			};
		

key 매개변수는 해당 플레이어가 득점시 사용할 키보드 키를 지정한다. 또한, 매번 새 player 객체가 생성될 때마다, 'newplayer' 이벤트를 발생한다. 그리고 매번 player가 득점할 때마다 'play' 이벤트를 발생한다.

			var scoreboard = {
				
				element: document.getElementById('results'),
				
				update: function (score) {
					
					var i, msg = '';
					for (i in score) {
						if (score.hasOwnProperty(i)) {
							msg += '<p><strong>' + i + '<\/strong>: ';
							msg += score[i];
							msg += '<\/p>';
						}
					}
					this.element.innerHTML = msg;
				}
			};
		

scoreobard 객체는 동일하다. 현재 점수를 화면에 갱신하는 작업만 수행한다.

새로운 game 객체는 모든 player들을 기록한다. 따라서 플레이어가 득점하면 점수를 기록하고 'scorechange' 이벤트를 발생한다. 또한 브라우저의 모든 'keypress' 이벤트를 구독하여 각각의 키가 어느 플레이어에 대응하는지 알아낸다.

			var game = {
				
				keys: {},

				addPlayer: function (player) {
					var key = player.key.toString().charCodeAt(0);
					this.keys[key] = player;
				},

				handleKeypress: function (e) {
					e = e || window.event; // IE
					if (game.keys[e.which]) {
						game.keys[e.which].play();
					}
				},
				
				handlePlay: function (player) {
					var i, 
						players = this.keys,
						score = {};
					
					for (i in players) {
						if (players.hasOwnProperty(i)) {
							score[players[i].name] = players[i].points;
						}
					}
					this.fire('scorechange', score);
				}
			};
		

makePublisher() 함수는 신문 발행 예제와 같이 객체를 발행자로 바꿔주는 함수다. game 객체가 발행자가 되고 (따라서 game 객체가 'scoreboard' 이벤트를 발생할 수 있다) Player.prototype 역시 발행자가 된다. 이제 player 객체는 구독을 원하는 어떤 객체에게든 'play'와 'newplayer' 이벤트를 발생할 수 있다.

			function makePublisher(o) {
				var i;
				for (i in publisher) {
					if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
						o[i] = publisher[i];
					}
				}
				o.subscribers = {any: []};
			}

			makePublisher(Player.prototype);
			makePublisher(game);
		

game 객체는 'play'와 'newplayer' 이벤트, 브라우저의 'keypress' 이벤트를 구독한다. scoreboard 객체는 'scorechange' 이벤트를 구독한다.

			Player.prototype.on("newplayer", "addPlayer", game);
			Player.prototype.on("play",      "handlePlay", game);

			game.on("scorechange", scoreboard.update, scoreboard);

			window.onkeypress = game.handleKeypress;
		

예제와 같이 구독자는 on() 메서드에 콜백 함수를 지정할 때 함수의 참조(scoreboard.update)나 문자열("addPlayer")을 사용할 수 있다. 문자열은 컨텍스트가 함께 제공되었을 때만 제대로 동작한다.

마지막으로 사용자가 원하는 수만큼 플레이어 객체들은 동적으로 생성한다. 각각에 대응하는 키도 함께 받는다.

			var playername, key;
			while (1) {
				playername = prompt("Add player (name)");
				if (!playername) {
					break;
				}
				while (1) {
					key = prompt("Key for " + playername + "?");
					if (key) {
						break;
					}
				}
				new Player(playername,  key);    
			}		
		

DOM과 브라우저 패턴


기능탐지는 브라우저간의 차이점을 우아하게 다루는 일반적인 기술 중 하나다. 사용자 에이전트를 감지해 코드를 분기하는 대신에, 사용하려는 메서드나 프로퍼티가 현재의 실행 환경에 존재하는지 확인하는 기술을 말한다. 사용자 에이전트 감지는 대체로 안티패턴이라 할 수 있다. 기능탐지로 확실한 결과를 얻을 수 없는 경우에만 최후의 선택사항으로 고려해야 한다.

			// 안티패턴
			if(navigator.userAgent.indexOf('MSIE') != -1){
				document.attachEvent('onclick', console.log);
			}

			// 더 좋은 방법
			if(document.attachEvent){
				document.attachEvent('onclick', console.log);
			}

			// 조금 더 정확한 방법
			if(typeof document.attachEvent !== "undefined"){
				document.attachEvent('onclick', console.log);
			}
		

DOM 스크립팅

| DOM 접근

DOM 접근은 비용이 많이 드는 작업이며 자바스크립트 성능에서 가장 흔한 병목 지점이다. 그래서 DOM 접근을 최소화해야 한다.

다음 예제에서 두번째 루프는 코드가 길어지긴 했지만 브라우저에 따라 수십에서 수백배 빠르다.

			// 안티패턴
			for (var i=0; i < 100; i+= 1){
				document.getElementById("result").innerHTML += i + ", ";
			}

			// 지역변수를 활용한 개선안
			var i, content = "";
			for (var i=0; i < 100; i+= 1){
				content += i + ", ";
			}
			document.getElementById("result").innerHTML = content;
		

다음 예제도 지역변수를 활용하는 첫 예제보다 한 줄이 더 길고, 변수가 하나 더 필요하지만 더 좋은 코드다.

			// 안티패턴
			var padding= document.getElementById("result").style.padding,
				margin= document.getElementById("result").style.margin;


			// 개선안
			var style= document.getElementById("result").style,
				padding = style.padding,
				margin = style.margin;
		

다음 코드는 셀렉터 API를 사용하는 예제다.

			document.querySelector("ul .selected");
			document.querySelectorAll("#widget .class");
		

셀렉터 메서드들은 IE8 이상을 포함한 대부분의 최신 브라우저에서 사용 가능하며, 다른 DOM 메서드를 사용한 선택방식보다 항상 빠르다. 자주 접근하는 엘리먼트에 id 속성을 추가하는 것도 노드를 찾는 가장 쉽고 빠른 방법이다.

| DOM 조작

DOM 업데이트시 브라우저는 화면을 다시 그리고(repaint), 엘리먼트를 재구조화(reflow)하는데, 이 또한 비용이 많이 드는 작업이다. 다시 말하지만, 원칙적으로 DOM 업데이트를 최소화하는 게 좋다. 이를 위해서 변경 사항들을 일괄 처리하거나, 실제 문서트리 외부에서 변경 작업을 수행해야 한다.

비교적 큰 서브 트리를 만들어야 한다면, 서브 트리를 완전히 생성한 다음에 문서에 추가해야 한다. 이를 위해 문서 조각에 모든 하위 노드를 추가하는 방법을 사용할 수 있다. 먼저 문서에 노드를 붙일 때 피해야 할 안티패턴을 살펴보자.

			// 안티패턴
			// 노드를 만들고 곧바로 문서에 붙인다.
			var p, t;

			p = document.createElement('p');
			t = document.createTextNode('first paragraph');
			p.appendChild(t);
			document.body.appendChild(p);

			p = document.createElement('p');
			t = document.createTextNode('second paragraph');
			p.appendChild(t);
			document.body.appendChild(p);
		

개선안은 문서 조각을 생성해 외부에서 수정한 후, 처리가 완전히 끝난 다음에 실제 DOM에 추가하는 것이다. DOM 트리에 문서 조각을 추가하면, 조각 자체는 추가되지 않고 그 내용만 추가된다. 즉 문서조각은 별도의 부모 노드없이도 여러 개의 노드를 감쌀 수 있는 훌륭한 방법이다.

			var p, t, frag;

			frag = document.createDocumentFragment();

			p = document.createElement('p');
			t = document.createTextNode('first paragraph');
			p.appendChild(t);
			frag.appendChild(p);

			p = document.createElement('p');
			t = document.createTextNode('second paragraph');
			p.appendChild(t);
			frag.appendChild(p);

			document.body.appendChild(frag);
		

문서조각을 이용한 방법은 <p> 엘리먼트를 생성할 때마다 문서를 변경하지 않고 마지막에 단 한번만 변경한다. 화면에 다시 그리고 재계산하는 과정도 한번만 이루어진다. 문서 조각은 새로운 노드를 트리에 추가할 때 유용하다.

하지만 문서에 이미 존재하는 트리를 변경할 때는 변경하려는 서브 트리의 루트를 복제해서 변경한 뒤 원래의 노드와 복제한 노드를 맞바꾸면 된다.

			var oldnode = document.getElementById('result'),
				clone = oldnode.cloneNode(true);

			// 변경이 끝나고 나면 원래의 노드와 교체한다.
			oldnode.parentNode.replaceChild(clone, oldnode);
		

이벤트

| 이벤트 처리

클릭할 때마다 카운터의 숫자를 증가시키는 버튼이 있다고 가정해보자. 인라인 onclick 속성을 추가할 수 있지만 분리와 점진적인 개선에 위비된다. 따라서 마크업을 수정하지 않고 항상 자바스크립트에서 이벤트 리스너를 처리해야 한다.

			<button id="clickme">Click me : 0 </button>
		

간단하게 노드의 onclick 프로퍼티에 함수를 할당할 수도 있지만, 이 방법은 한번밖에 쓸 수 없고 여러개의 함수가 실행되게 하면서 동시에 낮은 결합도를 유지하기 어렵다.

addEventListener() 메서드를 사용하는게 훨씬 깔금한 해결책이며 IE 8 버전까지는 존재하지 않기 때문에 IE 8 버전 이하에서는 attachEvent() 메서드를 사용해야 한다.

			var b = document.getElementById('clickme');
			if (document.addEventListener) { // W3C
				b.addEventListener('click', myHandler, false);
			} else if (document.attachEvent) { // IE
				b.attachEvent('click', myHandler);
			} else { // 최후의 수단
				b.onclick = myHandler;
			}

			function myHandler(e) {
				
				var src, parts;

				// 이벤트 객체와 소스 엘리먼트를 가져온다.
				e = e || window.event;
				src = e.target || e.srcElement;
				
				// 버튼의 라벨을 변경한다.
				parts = src.innerHTML.split(": ");
				parts[1] = parseInt(parts[1], 10) + 1;
				src.innerHTML = parts[0] + ": " + parts[1];
				
				// 이벤트가 상위 노드로 전파되지 않게 한다.
				if (typeof e.stopPropagation === "function") {
					e.stopPropagation();
				}
				if(typeof e.cancelBubble !== "undefined"){
					e.cancelBubble = true;
				}

				
				// 기본동작이 수행되지 않게 한다.
				if (typeof e.preventDefault === "function") {
					e.preventDefault();
				}
				if(typeof e.returnValue !== "undefined"){
					e.returnValue = false;
				}
			}
		

| 이벤트 위임

이벤트 위임 패턴은 이벤트 버블링을 이용해서 개별 노드에 붙는 이벤트 리스너의 개수를 줄여준다. div 엘리먼트 내에 열 개의 버튼이 있다면, 각 버튼 엘리먼트에 리스너를 붙이는 대신 div 엘리먼트에 하나의 이벤트 리스너만 붙인다.

			<div id="click-wrap">
			  <button>Click me: 0</button>
			  <button>Click me too: 0</button>
			  <button>Click me three: 0</button>
			</div>
		

각 버튼에 이벤트 리스너를 붙이는 대신 'click-wrap' div에 하나의 리스너만을 붙인다. 그리고 불필요한 클릭을 걸러내기 위해서 버튼에 대한 클릭을 제외한 div 내의 다른 부분에서 발생한 클릭은 무시한다. 다음과 같이 이벤트가 발생한 노드의 nodeName이 "button"인지 확인하는 구문을 추가한다.

			var el = document.getElementById('click-wrap');
			if (document.addEventListener) { // W3C
				el.addEventListener('click', myHandler, false);
			} else if (document.attachEvent) { // IE
				el.attachEvent('click', myHandler);
			} else { // 최후의 수단
				el.onclick = myHandler;
			}

			function myHandler(e) {
				
				var src, parts;

				// 이벤트 객체와 소스 엘리먼트를 가져온다.
				e = e || window.event;
				src = e.target || e.srcElement;

				if(src.nodeName.toLowerCase() !== "button"){
					return;
				}
				
				// 버튼의 라벨을 변경한다.
				parts = src.innerHTML.split(": ");
				parts[1] = parseInt(parts[1], 10) + 1;
				src.innerHTML = parts[0] + ": " + parts[1];
				
				// 이벤트가 상위 노드로 전파되지 않게 한다.
				if (typeof e.stopPropagation === "function") {
					e.stopPropagation();
				}
				if(typeof e.cancelBubble !== "undefined"){
					e.cancelBubble = true;
				}

				
				// 기본동작이 수행되지 않게 한다.
				if (typeof e.preventDefault === "function") {
					e.preventDefault();
				}
				if(typeof e.returnValue !== "undefined"){
					e.returnValue = false;
				}
			}
		

로딩전략

| 다운로드를 차단하지 않는 동적인 <script> 엘리먼트

자바스크립트는 뒤이어 오는 파일들의 다운로드를 차단한다. 다음은 다른 파일의 다운로드를 차단하지 않고 자바스크립트 파일을 비동기적으로 로드하는 예다.

			var script = document.createElement("script");
			script.src = "all_20100426.js";
			document.documentElement.firstChild.appendChild(script);
		

그러나 이 패턴을 적용하여 .js 파일을 로드하는 동안에는, 이 파일에 의존하여 동작하는 다른 스크립트 엘리먼트를 쓸 수 없다는 단점이 있다. 이 문제를 해결하려면 모든 인라인 스크립트를 바로 실행하는 대신 배열 안의 함수로 모아두어야 한다. 그러고 나서 비동기로 js 파일을 받고난 뒤 버퍼 배열 안에 모아진 모든 함수를 실행한다. 이를 위해서는 세단계를 거쳐야 한다.

첫번째로, 모든 인라인 코드를 저장해 둘 배열을 가능한 페이지의 최상단에 만든다.

			var mynamespace = {
				inline_scripts : []
			};
		

그러고 나서 각 인라인 스크립트를 함수로 감싸 inline_scripts 배열에 넣는다.

			// 수정전
			<script>console.log("I am inline");</script>

			// 수정후
			<script>
				mynamespace.inline_scripts.push(function(){
					console.log("I am inline");
				});
			;</script>
		

마지막 단계에는 비동기로 로드된 js 스크립트가 인라인 스크립트의 버퍼 배열을 순회하면서 배열 안의 모든 함수를 실행한다.

			var i, scripts = mynamespace.inline_scripts, max = scripts.length;
			for (i=0; i < max; i += 1){
				scripts[i]();
			}
		
<script> 엘리먼트 붙이기

일반적으로 스크립트는 문서의 <head>에 추가된다. 하지만 스크립트는 <body>를 포함한 어떤 엘리먼트에도 붙일 수 있다.

이전 예제에서는 <head>에 스크립트를 붙일 때 다음과 같이 documentElement를 사용했다. documentElement는 <html>을 가리키고 그 첫번째 자식은 <head>이기 때문이다.

			document.documentElement.firstChild.appendChild(script);
		

다음과 같은 방법도 일반적이다.

			document.getElementsByTagName("head")[0].appendChild(script);
		

| 게으른 로딩

게으른 로딩은 외부 파일을 페이지의 load 이벤트 이후에 로드하는 기법을 말한다. 대체로 두 부분으로 나누는 것이 유리하다.

게으른 로딩의 목적은 페이지를 점진적으로 로드하고 가능한 빨리 무언가를 동작시켜 사용할 수 있게 하는 것이다. 두번째 부분을 로딩하기 위해 동적 스크립트 엘리먼트를 head나 body에 붙이는 방법을 다시 사용한다.

			<script src="all_20130910.js"></script>
			<script>
			window.onload = function(){
				var script = document.createElement("script");
				script.src = "all_lazy_20130910.js";
				document.documentElement.firstChild.appendChild(script);
			};
			</script>
			</body>
			</html>
		

| 주문형 로딩

정말로 필요한 부분만 로드하도록 만들 수도 있을까? 페이지에 여러개의 탭을 가진 사이드바가 있다고 가정하자. 탭을 클릭하면 내용을 가져오기 위해 XHR 요청을 보내고, 응답을 받아 탭 내용을 갱신하며, 색상을 흐르게 만드는 애니메이션을 보여준다. 만약 이 부분에서만 XHR과 애니메이션 라이브러리를 사용하는데, 사용자가 탭을 한번도 클릭하지 않는다면 어떻게 될까?

주문형 로딩 패턴을 적용하면 이런 경우에 효율적으로 대응할 수 있다. 로드할 스크립트의 파일명과 이 스크립트가 로드된 후에 실행될 콜백함수를 받는 require() 함수 또는 메서드를 만들어 보자.

require() 함수는 다음과 같은 형태로 호출된다.

	   require('extra.js', function () {
			functionDefinedInExtraJs();
		});

		function require(file, callback) {

			var script = document.getElementsByTagName('script')[0],
				newjs = document.createElement('script');

			// IE
			newjs.onreadystatechange = function () {
				if (newjs.readyState === 'loaded' || newjs.readyState === 'complete') {
					callback();
				}
			};

			// others
			newjs.onload = function () {
				callback();
			};
			
			newjs.src = file;
			script.parentNode.insertBefore(newjs, script);
		}
		

| 자바스크립트 사전 로딩

현재 페이지에서는 필요하지 않지만 다음으로 이동하게 될 페이지에서 필요한 스크립트를 미리 로드할 수도 있다. 이 방법을 이용하면, 사용자가 두번째 페이지에 도착했을 때, 이미 스크립트가 로드되어 있기 때문에 전체적으로 더 빠른 속도를 경험하게 된다. 사전 로딩은 특정 DOM 노드를 찾으려 한다면 에러가 발생할 수 있다.

스크립트가 파싱되거나 실행되지 않게 로드할 수도 있다. 이 방법은 CSS나 이미지 파일에도 적용할 수 있다.

IE에서는 이미지 비컨 패턴으로 요청을 만들면 된다.

			new Image().src = "preloadme.js";
		

IE이외의 브라우저에서는 <object>엘리먼트를 사용하고 data 속성 값에 로드할 스크립트의 URL을 가리키도록 지정하면 된다.

			var obj = document.createElement('object');
			obj.data = 'preloadme.js';
			document.body.appendChild(obj);
		

범용의 preload() 함수나 메서드를 만들고 초기화 시점 분기 패턴으로 브라우저간의 차이를 처리할 수도 있다.

			var preload; 

			if (/*@cc_on!@*/false) { // 조건 주석문으로 IE를 탐지한다.
				preload = function (file) {
					new Image().src = file;
				};
			} else {
				preload = function (file) {
					var obj = document.createElement('object'),
					body = document.body;
					obj.width = 0;
					obj.height = 0;
					obj.data = file;
					body.appendChild(obj);
				};
			}		

			preload('my_web_worker.js');
		

사전 로딩 패턴은 스크립트 뿐만 아니라 모든 종류의 요소들에 적용할 수 있다. 예를 들어 로그인 페이지에서 유용하게 사용할 수 있다. 사용자가 자신의 아이디를 입력하는 시간을 이용해 전혀 알아차리지 못하게 사전 로딩을 시작할 수 있다.