[es6] Proxy #1

개요

Proxy는 디자인패턴에 나오는 프록시패턴을 시스템적으로 구현해놓은 구상체입니다. es6의 trap은 게이트웨이적인 역할을 통해 대규모로 객체의 사용에 관여할 수 있게 합니다. Proxy를 사용하려면 다음 세가지 주인공을 인식해야 합니다.

  • target : Proxy를 적용할 대상객체입니다.
  • trap : target의 여러 동작을 가로채 게이트웨이 역할을 수행할 함수입니다.
  • handler : trap을 여러개 담고 있는 객체로 실제 target에 결합하게 되는 것은 trap이 아니라 handler가 됩니다.

머 이상에서 살펴본 개념에 따르면 젤 중요한 건 trap입니다. 어떤 trap이 있는지 간단히 살펴보죠.

trap

trap의 종류가 워낙 다양해서 나름대로의 종류로 분류해봤습니다.

  • 속성제어 : get, set
  • 속성정의 : defineProperty, deleteProperty
  • for..in : has, enumerate, ownKeys
  • 속성기술 : isExtensible, preventExtensions, getOwnPropertyDescriptor
  • 객체생성 : construct
  • prototype : getPrototypeOf, setPrototypeOf
  • 함수호출 : apply

handle에 모두를 구현할 필요는 없고 필요에 따라 필요한 만큼만 구현하면 됩니다.
하지만 객체모델을 사용할 경우 이미 내부에 열거나 루프를 내장한 로직을 소유하는 경우가 많기 때문에 for..in관련된 것이나 속성기술자는 잘 안쓰게 되고, es6의 클래스를 기반으로 개발하다보면 동적 프로토타입제어에 대한 필요성이 약해서 객체생성이나 프로토타입에 대해서도 별로 trap을 걸지 않게 됩니다.

가장 흔하게는 get, set, apply 등이 후보입니다만, 경우에 따라 매우 세밀한 Proxy를 제작할 수 있습니다.

set 게이트웨이로 쓰기

외부에는 속성으로 노출되지만 그 변화를 반드시 인지해야하는 경우가 있습니다.
예를 들어 노출된 style객체는 속성으로 다양한 통제를 당하지만 실제 렌더링 타이밍에는 이 변화가 하나라도 일어나면 업데이트 대상이되도록 해야합니다.
이를 간단히 코드로 생각해보죠.

class Style{
  constructor(){
    //최초 업데이트없음
    this.isUpdated = false;
  }
}

let style = new Style();
style.width = 100;
style.height = 100;

console.log(style.isUpdated);
//true가 나오게 하고 싶다고!

기존에 이러한 절차를 위한 방법은 복잡한 get, set을 키마다 구현하는 것 외엔 답이 없었습니다.

class Style{
  constructor(){
    this.isUpdated = false;
  }
  set width(v){
    this.isUpdated = true;
    this._width = v;
  }
  get width(){
    return this._width;
  }
  //height 상동
}

방대한 속성을 전부 저렇게 구현해야하는 것도 힘들거니와 _width같은 부산물이 계속 생겨나게 됩니다.
이를 Proxy의 set, get 트랩을 이용하면 굉장히 쉽고 광범위하게 해결됩니다. 우선 isUpdated의 문제부터 해결을 해보죠.

class Style{
  constructor(){
    this.isUpdated = false;
  }
}

let style = new Proxy(
  new Style(), //첫번째인자가 target
  { //두번째인자가 handler
    set:(target, property, value, receiver)=>{ //set trap
      if(property == 'isUpdated'){
        target.isUpdated = value;
        return;
      }
      target.isUpdated = true;
      target['_' + property] = value;
    },
    get:(target, property, value, receiver)=>{ //get trap
      return property == 'isUpdated' ? target.isUpdated : target['_' + property];
    }
  }
);

console.log(style.isUpdated); //false

style.width = 100;

console.log(style.isUpdated); //true!!

처음 보신 분은 굉장히 생소하겠지만 찬찬히 코드를 볼까요. 우선 style에 할당된건 new Style이 아니라 그것을 소유하고 있는 new Proxy입니다.
개요에서 말씀드린대로 Proxy는 두 개의 인자를 받는데, target과 handler입니다. handler에 원하는 trap을 넣어주는 식이죠.

여기서는 get, set trap을 handler에 배치했습니다. set trap의 내용을 가만히 보면 속성값이 isUpdated일 때는 직접 그 키에 쓰고 아닌 경우는 _가 붙은 속성으로 기록하게 하면서 동시에 isUpdatd를 true로 만들고 있습니다.

get trap에서는 isUpdated를 건드릴 필요는 없으나 나머지 속성은 _를 붙여서 값을 보여주고 있습니다.

여기서 약간 리펙토링하여 Style클래스의 예외적인 속성이 되어버린 isUpdated를 _isUpdated로 고쳐보죠. 이를 통해 get, set trap의 if를 제거할 수 있게 됩니다.

class Style{
  constructor(){
    this._isUpdated = false; //_로 수정
  }
}

let style = new Proxy(new Style(), {
  set:(target, property, value, receiver)=>{
    if(property != 'isUpdated') target._isUpdated = true;
    target['_' + property] = value;
  },
  get:(target, property, value, receiver)=>{
    return target['_' + property];
  }
});

console.log(style.isUpdated); //false
style.width = 100;
console.log(style.isUpdated); //true!!

예외를 제거하고 모든 속성을 _로 인식하게 고쳐서 훨씬 로직이 단순하게 되었습니다. 하지만 _로 노출되는 속성들이 맘에 걸리죠. 이전 심볼글에서 다룬 것처럼 중첩영역을 이용해 은닉된 심볼을 만들면 손쉽게 외부에서 속성을 건드릴 수 없게 됩니다. 이를 모두 조합하여 완전한 private를 구현하고 _속성을 제거해보죠.

let Style;
{
  const PROP = Symbol(); //은닉된 심볼
  const handler = {
    set:(target, property, value, receiver)=>{
      target[PROP].isUpdated = true;
      target[PROP][property] = value;
    },
    get:(target, property, value, receiver)=>target[PROP][property]
  };
  Style = class{
    constructor(){
      this[PROP] = {isUpdated:false};
      return new Proxy(this, handler); //프록시를 반환함
    }
  };
}

let style = new Style();

console.log(style.isUpdated); //false
style.width = 100;
console.log(style.isUpdated); //true!!

음 2단계에서 비해서 코드가 너무 많이 점프했나요 ^^;

  1. 우선 Style클래스를 정의할 때 중첩영역을 이용하여 PROP나 handler를 지역변수로 선언하고 Style클래스를 정의합니다. 이를 통해 Style클래스와 handler는 PROP심볼을 인식할 수 있지만 외부에서는 접근할 수 없게 됩니다.
  2. Style의 생성자에서는 이제 PROP심볼을 통해 {}를 하나 만들어 거기에서 속성을 관리하게 되고 본인에겐 직접 쓰지 않게 됩니다.
  3. get, set trap내부에서는 더이상 target에 직접 쓰지 않고 target[PROP]객체를 이용하게 됩니다.
  4. 마지막으로 constructor가 Proxy객체를 반환하게 되므로 외부에서는 단순히 new Style만으로 프록시를 얻게 됩니다.

현재 이 예제를 실행해볼 수 있는 브라우저는 엣지 뿐이었습니다. 파폭은 44까지 class가 안되고 크롬은 48까지 Proxy가 안됩니다.

결론

몇 가지 전통적인 프록시 예제를 더 전개하려고 했으나 글이 길어지네요. 결정적으로 프록시는 현재 크롬에서 지원안하다보니 많은 개발자에게 소외당하고 있는 현실이지만 실은 굉장히 강력한 통제 수단으로 apply의 경우 함수 호출자체를 통제할 수도 있습니다.
이는 언어레벨의 프레임웍 제작을 가능하게 하여 기존의 함수나 클래스로만 통제하던 레벨보다 더욱 강력한 로우레벨로 프로그래밍을 통제할 수 있게 됩니다.
물론 큰 힘에는 큰 책임이 따르지만요..
다음 프록시 포스팅에는 리모트를 비롯하여 기존 디자인패턴의 프록시 사용에 대한 각각의 경우와 특수 상황에 대한 프록시를 다루도록 하겠습니다. 하아..이것도 몇회에 걸치게 되겠군요^^;;

%d 블로거가 이것을 좋아합니다: