본문 바로가기

JavaScript

자바스크립트 코딩기법과 핵심패턴 제 6장 코드 재사용 패턴 #2

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.


요즘 개인적으로 자바스크립트를 공부하고 있다. 하지만 내 경우 어정쩡하게 알고 있는 자바스크립트라 기초책은 보나마나인데, 이 책은 정말 실무에서도 바로 쓸 수 있는 패턴을 뽑아서 먹여주는 책 같다. 이 책과 더불어 자바스크립트 성능 최적화도 보면 정말 좋겠다. 아무튼 이 책을 요약하면서 정리하고자 한다. 이 책은 정말 강력 추천하며 자바스크립트를 제대로 학습하기 위한 필수 소장서이다. 

책구입 : http://tinyurl.com/7ejd4rs 
출판사 책소개 : http://blog.insightbook.co.kr/245 


자바스크립트 코딩기법과 핵심패턴 제 6장 코드 재사용 패턴 #2  

이 책에서는 자바스크립트에서 코드 재사용 패턴은 상속, 다른 객체와 합성, 믹스-인 객체 사용, 메서드 빌려쓰기등으로 소개하고 있다. 코드 재사용 작업을 접근할 때, GoF의 충고인 '클래스 상속보다 객체 합성을 우선시하라'를 생각하는게 중요하다. 

지난 글(http://welchsy.tistory.com/236) 에서 코드 재사용 패턴중 클래스 방식의 상속에 대해서 다루었다. 나머지는 여기서 다룬다. 

프로토타입을 활용한 상속

프로토타입을 활용한 상속은 클래스를 사용하지 않는 '새로운' 방식의 패턴이다. 
 
다음과 같은 함수가 이것을 실현시킨다.
 

 
1.// 프로토타입 활용한 상속을 가능케하는 함수
2.function object(o) {
3.    function F() {};
4.    F.prototype = o;
5.    return new F();
6.}



위 함수를 아래처럼 사용할 수 있다.

 
01.// 상속할 객체
02.var parent = {
03.    name: "Papa",
04.    getName:function() {
05.        return this.name;
06.    }
07.};
08.// 새로운 객체
09.var child = object(parent);
10.//테스트
11.console.log(child.name); //"Papa"
12.console.log(child.getName()); //"Papa"



위 코드처럼 부모를 객체 리터럴로 생성하는 것 뿐만 아니라 생성자 함수를 통해서도 부모를 생성할 수 있다.

01.//부모 생성자
02.function Person() {
03.    // 부모 생성자 자신의 프로퍼티
04.    this.name = "Adam";
05.}
06.// 프로토타입에 추가된 프로퍼티
07.Person.prototype.getName = function () {
08.    return this.name;
09.};
10.// Person 인스턴스 생성
11.var papa = new Person();
12.// 이 인스턴스를 상속
13.var kid = object(papa);
14.// 부모 자기 자신의 프로퍼티와 프로토타입의 프로퍼티가 모두 상속되었는지 확인
15.console.log(kid.getName()); //"Adam"



하지만 주의할 것은 생성자 함수의 프로토타입 객체만 상속받게 할 수 있다.

1.var kid2 = object(Person.prototype);
2.console.log(typeof kid2.getName); //"function" 이 메서드는 프로토타입 안에 정의된 프로퍼티이다.
3.console.log(typeof kid2.name); //"undefined" 프로토타입만 상속했기 때문에 부모에 정의된 name 프로퍼티는 상속되지 않음



ECMAScript 5에는 Object.create()가 위 object() 함수를 구현하고 있다.

1.var parent = new Person();
2.var child2 = Object.create(parent, {
3.    age: { value: 2 } //ECMA 5 기술자(Descriptor)
4.});
5.console.log(child2.hasOwnProperty("age")); //true



자바스크립트 라이브러리에서 YUI3에서도 Y.Object() 메서드가 그것을 구현하고 있음을 알아두자.

책 내용에는 없지만 위 방식을 그대로 사용하는 것은 부정적이다. 이것은 여전히 전역을 더럽히기 때문에 네임스페이스 패턴이든 샌드박스 패턴이든 사용해 전역을 최소화할 필요가 있겠다. 하지만 기존 클래스 방식의 상속보다 훨씬 간단하면서도 매끄럽게 재사용 패턴을 적용할 수 있다는 점은 크게 매력적이다. 

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

아래는 얕은 복사 방식이다.

01.//프로퍼티 얕은 복사를 통한 상속 패턴 적용 함수
02.function extend(parent, child) {
03.    var i,
04.        child = child || {};
05.    for( i in parent ) {
06.        if( parent.hasOwnProperty(i) ) {
07.            child[i] = parent[i];
08.        }
09.    }
10.    return child;
11.}
12. 
13.//프로퍼티 복사 확인
14.var dad = {name: "Adam"};
15.var kid = extend(dad);
16.console.log(kid.name); //"Adam"
17. 
18.//프로퍼티 얕은 복사 확인
19.var dad2 = {
20.    counts: [1, 2, 3],
21.    reads: {paper: true}
22.};
23.var kid2 = extend(dad2);
24.kid2.counts.push(4);
25.console.log(kid2.counts.toString()); //"1,2,3,4"
26.console.log(dad2.reads === kid2.reads); //true


아래 코드는 프로퍼티 깊은 복사를 통한 상속 패턴 적용 함수이다. 

01.function extendDeep(parent, child) {
02.    var i,
03.        toStr = Object.prototype.toString;
04.        astr = "[object Array]";
05.    child = child || {};
06.    for (i in parent) {
07.        if (parent.hasOwnProperty(i)){
08.            if (typeof parent[i] === "object") {
09.                child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
10.                extendDeep(parent[i], child[i]);
11.            } else {
12.                child[i] = parent[i];
13.            }
14.        }
15.    }
16.    return child;
17.}
18.     
19.var dad = {
20.    counts: [1, 2, 3],
21.    reads: {paper: true}
22.};
23.var kid = extendDeep(dad);
24.kid.counts.push(4);
25.console.log(kid.counts.toString()); //"1,2,3,4"
26.console.log(dad.counts.toString()); //"1,2,3"
27.console.log(dad.reads === kid.reads); // false
28.kid.reads.paper = false;
29.console.log(dad.reads.paper);  //true


위 함수들은 아주 간단하고 널리 사용된다고 한다. 그리고 jQuery의 extend() 메서드는 깊은 복사를 하고 Y.clone() 깊은 복사를 수행하면서 함수도 복사해 자식 객체와 바인딩 해준다고 한다. 이 패턴은 프로토타입을 전혀 사용하지 않은 것도 주목할 만하다. 

하지만 내 생각에.... 깊은 복사를 하는 과정에서 extendDeep()을 재귀적으로 호출하고 있다. 이 점은 자바스크립트 특성상 이렇게 쓰는 경우 스택오버가 걸릴 가능성이 농후하므로 뭔가 비동기적으로 동작하도록 만들 필요가 있다. 

게다가 배열이 for-in 루프로 요소를 탐색하는 것은 2장 기초에서 다루었듯이 for 루프를 사용하는 것이 맞다. 


믹스-인 
프로퍼티 복사 아이디어를 발전시켜 믹스-인 패턴을 생각할 수 있다. 이것은 하나의 객체를 복사하는게 아니라 여러 객체를 복사해 하나의 객체에 섞어 넣을 수 있다.

01.function mix() {
02.    var arg, prop, child = {};
03.    for (arg = 0; arg < arguments.length; arg += 1) {
04.        for (prop in arguments[arg]) {
05.            if (arguments[arg].hasOwnProperty(prop)) { //프로토타입 프로퍼티를 걸러냄
06.                child[prop] = arguments[arg][prop];
07.            }
08.        }
09.    }
10.    return child;
11.}
12.var cake = mix(
13.    {eggs: 2, large: true},
14.    {butter: 1, salted: true},
15.    {flour: "3 cups"},
16.    {sugar: "sure!"}
17.);
18.console.dir(cake);

결과적으로 아래처럼 나온다.

butter: 1
eggs: 2
large: true
salted: true
flour: "3 cups"
sugar: "sure!"
 



믹스-인 개념에는 단순히 루프를 돌고, 프로퍼티를 복사하는 것이기 때문에 부모들과의 연결 고리는 끊어진 상태이다. 

개인적인 이 패턴에 대한 생각을 남기면... 위 mix() 메서드는 여러개의 객체중에 프로퍼티 이름이 중복되면 마지막에 들어간 것이 기존에 있는 것을 덮어쓰게 될 것이다. 

게다가 다음과 같은 경우에는 대응하지 못한다.

01.var a = {array: [1,2,3]};
02.var cake = mix(
03.    {eggs: 2, large: true},
04.    {butter: 1, salted: true},
05.    {flour: "3 cups"},
06.    {sugar: "sure!"},
07.    a
08.);
09.a.array.push(4);
10.console.log(a.array.toString()); //"1,2,3,4"
11.console.log(cake.array.toString()); //"1,2,3,4"


원래 기대하는 바는 cake.array.toString()의 경우 "1,2,3"이어야 할 것이다. 즉, 배열값에 대해서는 얕은 복사를 했으므로 깊은 복사를 할 수 있도록 개선해야한다. 


메서드 빌려쓰기 

메서드 빌려쓰기 재사용 패턴은 정말 자바스크립트의 특징을 대변해주는 패턴일 것이다. 이 패턴은 부모-자식 관계까지 만들지 않고 어떤 객체의 메서드 한두개만 빌려쓰는데 유용하다. 

책 내용중에는 apply와 call을 사용해 bind를 구축하는 방법과 this문제를 잘 다루었다. 마지막에 ECMAScript 5부터 지원하는 Function.prototype.bind() 메서드를 사용하면 된다고 했다. 하지만 이 메서드가 지원되지 않은 경우도 감안해서 아래와 같은 코드를 쓰면 언제든지 bind()를 활용할 수 있게 된다. 

01.if (typeof Function.prototype.bind === "undefined") {
02.    Function.prototype.bind = function (thisArg) {
03.        var fn = this,
04.             slice = Array.prototype.slice,
05.             args = slice.call(argments, 1);
06.        return function () {
07.             return fn.apply(thisArg, args.concat(slice.call(arguments)));
08.        };
09.   }
10.}


메서드 빌려쓰기 패턴은 잘쓰면 꽤 유용할 듯 싶다.