아디봉의.net

[자바스크립트] 코딩기법과핵심패턴: 코드재사용패턴#2 (펌) 본문

JAVASCRIPT

[자바스크립트] 코딩기법과핵심패턴: 코드재사용패턴#2 (펌)

아디봉 2013. 5. 25. 13:34

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

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

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

프로토타입을 활용한 상속 

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

 

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


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


출처 : http://blog.jidolstar.com/796