[es5] Object.create기반 클래스

개요top

ES6에서는 풍부한 class의 기능을 사용하면 되지만 es5까지의 장황한 프로토타입문법은 누구나 거슬리기 때문에 거의 모든 라이브러리는 자체 클래스 생성 문법을 지원합니다. 당연히 저도 제 스타일의 클래스 생성기가 있습니다. 오늘은 그걸 공개해보죠.

우선 요즘 세상에도 ES5를 쓰냐고 물어보면 당연히 써야 한다고 말씀드리고 싶네요. 바벨로 번역되는 es5는 호환이 될지는 몰라도 돌아간다고 말하기 어렵습니다. 너무 느리기 때문이죠. IE나 구형 안드로이드, iOS를 똥 취급하는 게 아니라 제대로 사용자 경험을 제공하고 싶다면 바벨을 돌리는 것으로는 무리가 있습니다. 당연히 ES5기반으로 정성스럽게 만들어야 제대로 속도가 납니다 ^^

보통 제가 다른 글에서 ES6만 다루지만 실무에서는, 그리고 회사라이브러리는 es5기반이 많습니다(먹고 살아야죠 ^^)
Object.create과 관련된 글은 이미 여러 번 다뤘습니다.
Object 1. 새로운 방식의 객체 생성
Object 2. 보다 안전한 객체

이번 글에서는 실무에서 사용하고 있는 일명 prop, create함수 라이브러리를 보면서 실제 응용을 살펴보겠습니다.

기본 생성top

우선 es3.1과 es5는 생성 방식이 크게 다른데, Object.create를 지원하기 때문이죠(물론 폴리필 가능합니다만 무의미한 함수를 대량으로 만들어야 합니다)

var instance = Object.create(prototype or null);

간단히 특정 객체로 프로토타입 체인이 되는 객체를 만들 수 있습니다. 함수로 한번 더 감싸면 아래와 같을 것입니다.

var create = function(proto){
  return Object.create(proto || null);
};
var instance = create();

instanceof의 문제top

헌데 js의 instanceof 연산자는 함수를 받아 그 함수의 prototype객체를 대상으로 작동하므로 저 방식으로 생성된 객체는 instanceof를 사용할 수 없다는 문제가 있습니다. 이는 ES3.1에서부터 내려오는 isPrototypeOf로 해결할 수 있습니다.

var create = function(proto){
  var result = Object.create(proto || null);
  return result;
};
var proto = {};
var instance = create(proto);
proto.isPrototypeOf(instance); //true

재정의의 위험성도 있고 매번 길게 쓰기도 귀찮으니 is로 퉁칩니다.

var create = function(proto){
  var result = Object.defineProperty(Object.create(proto || null), 
    'is', {
      value:function(v){
        return Object.prototype.isPrototypeOf.call(proto, this);
      }
  });
  return result;
};
var proto = {};
var instance = create(proto);
instance.is(proto); //true

isPrototypeOf는 부모를 한꺼번에 검사해주는 기능이 없습니다. 오직 체인 중 딱 하나에 대응하죠. 다형성을 지원하려면 보다 복잡한 루프를 돌아야 합니다. 인스턴스마다 함수를 만들기는 부담스러우니 숨겨진 proto를 활용해보죠.

var create = (function(){
  var isProto = Object.prototype.isPrototypeOf;
  var is = {value:function(v){
    var target = this;
    do{
      if(isProto.call(target.__proto__, target)) return true;
    }while(target = target.__proto__);
    return false;
  }};
  return function(proto){
    var result = Object.defineProperty(Object.create(proto || null), 'is', is);
    if(!result.__proto__) Object.defineProperty(result, '__proto__', {value:proto});
    return result;
  };
})();
var proto = {};
var parent = create(proto);
parent.is(proto); //true

var child = create(parent);
child.is(parent); //true
child.is(proto); //true

이제 계층 구조를 전부 is로 확인할 수 있게 되었습니다.

property 처리top

es6에서는 Object.assign이 있어 굉장히 손쉽게 여러 속성을 할당할 수 있죠. 그것을 흉내내 prop라는 함수를 만들어보겠습니다.

var prop = (function(){
  var own = Object.prototype.hasOwnProperty;
  return function(target){
    var a = arguments, i, j, k , v;
    for(i = 1, j = a.length; i < j; i++){
      if(v = a[i]){
        for(k in v) if(own.call(v, k)) target[k] = v[k];
      }
    }
    return target;
  };
})();

var obj = prop({}, {a:3, b:5}, {c:7});
obj.a == 3;
obj.b == 5;
obj.c == 7;

이제 간단히 Object.assign을 대체하는 prop함수를 갖게 되었습니다.

create에 prop를 조합top

create 함수를 약간 개조하여 첫 번째 인자로 prototype을 받고 뒤이어 오는 인자는 prop로 정의하는 방식으로 create함수를 확장할 수 있습니다.

var create = (function(){
  var isProto = Object.prototype.isPrototypeOf;
  var is = {value:function(v){
    var target = this;
    do{
      if(isProto.call(target.__proto__, target)) return true;
    }while(target = target.__proto__);
    return false;
  }};
  return function(proto){
    var result = Object.defineProperty(Object.create(proto || null), 'is', is), a;
    if(!result.__proto__) Object.defineProperty(result, '__proto__', {value:proto});
    if(arguments.length > 1){
      a = Array.prototype.slice.call(arguments, 0);
      a[0] = result; //target은 result다!
      prop.apply(null, a);
    }
    return result;
  };
})();

var proto = {};
var instance = create(proto, {a:3, b:5});
instance.is(proto); //true
instance.a == 3;
instance.b == 5;

자기참조 무결성top

이 create함수는 인스턴스를 만드는 구문과 클래스를 만드는 구문의 차이가 없습니다. 하나의 로직으로 클래스와 인스턴스가 모두 생성되는 방식을 취하고 있어 별도의 구문이 필요치 않게 되는 거죠. 이제 이걸로 클래스도 정의하고 그 클래스의 인스턴스도 정의해보겠습니다. 많은 예제에 나오는 rect를 생각해보죠.

  • x, y, width, height 속성을 갖고 태어난다.
  • right(), bottom() 등의 계산된 속성을 지원한다.

기타 겹친 상태 검출 등의 다양한 메소드도 제공되지만, 나중에 하기로 하고, 보통 es6클래스로 만들면 다음과 같을 것입니다.

const Rect = class{
  constructor(x, y, width, height){Object.assign(this, {x, y, width, height});}
  right(){return this.x + this.width;}
  bottom(){return this.y + this.height;}
};
const rect = new Rect(10,10, 100,100);
rect.right(); //110;

헌데 create에서는 클래스도 인스턴스도 전부 create로 해결됩니다.

var Rect = create(null, {
 right:function(){return this.x + this.width;},
 bottom:function(){return this.y + this.height;}
});
var rect = create(Rect, {x:10, y:10, width:100, height:100});
rect.right(); //110

생성자 처리top

명시적인 생성자가 필요하다면 init등의 메소드를 정의해서 사용합니다.

var Rect = create(null, {
 init:function(x, y, width, height){
   return prop(this, {x:x, y:y, width:width, height:height});
 },
 right:function(){return this.x + this.width;},
 bottom:function(){return this.y + this.height;}
});
var rect = create(Rect).init(10, 10, 100, 100);
rect.right(); //110

하지만 코틀린이나 스위프트 스타일의 호출을 통한 인스턴스 생성이 더 나을 수도 있습니다.

var Rect = (function(){
  var Rect = create(null, {
   right:function(){return this.x + this.width;},
   bottom:function(){return this.y + this.height;}
  });
  return function(x, y, width, height){
    return prop(create(Rect), {x:x, y:y, width:width, height:height});
  };
})();
var rect = Rect(10, 10, 100, 100);
rect.right(); //110

또한 전형적인 정적 팩토리로 제공될 수도 있죠.

var Rect = create(null, {
 $get:function(x, y, width, height){
   return prop(create(Rect), {x:x, y:y, width:width, height:height});
 },
 right:function(){return this.x + this.width;},
 bottom:function(){return this.y + this.height;}
});
var rect = Rect.$get(10, 10, 100, 100);
rect.right(); //110

저는 바로 이 세 번째 방법을 사용합니다. 자신의 정적 함수도 직접 정의하면 됩니다. 단지 이렇게 되면 어디까지가 정적함수인지 메소드인지 잘 구분이 안되므로 이름 앞에 $등을 붙이는 것으로 구분합니다(안해도 그만이지만 ^^)
오히려 자바처럼 정적메소드를 인스턴스도 쓸 수 있는 구조가 되는 식입니다.

var Rect = create(null, {
 $get:function(x, y, width, height){
   return prop(create(Rect), {x:x, y:y, width:width, height:height});
 },
 right:function(){return this.x + this.width;},
 bottom:function(){return this.y + this.height;}
});
var rect1 = Rect.$get(10, 10, 100, 100);
//자바처럼 인스턴스로부터도 정적메소드가 호출가능
var rect2 = rect1.$get(100, 100, 100, 100);

결론top

표준 사양에 입각한 Object.create, isPrototypeOf를 활용해 가볍게 create함수를 구현하고 이를 통해 클래스와 인스턴스 생성 모두를 해결해보았습니다.
특수한 자체 규격이나 키를 사용하지 않아 결과물은 3.1의 순정(?) 프로토타입구조를 썼을 때와 다를 바 없이 가볍고, prop함수의 작동방식 역시 Object.assign을 따르고 있으므로 추가적힌 인터페이스 학습을 최소화했습니다.

회사에서는 많은 프로덕션에 es5를 지원해야 하는 형편이라 자주 사용하고 있는 구조입니다.