Proxy in Javascript

sooni·2023년 5월 12일
0

Proxy

Proxy 객체를 사용하면 한 객체에 대한 기본 작업을 가로채고 재정의하는 프록시를 만들 수 있다.

프록시 객체를 사용하면 속성 가져오기, 설정 및 정의와 같은 기본 객체 작업을 재정의 할 수 있다. 프록시 객체는 일반적으로 속성 액세스를 기록하고, 입력의 유효성을 검사하고, 형식을 지정하거나, 삭제하는 데 사용된다.

즉, 프록시 객체는 대상 객체를 트랩을 통해 기본 명령을 재정의해서 사용한다.

매개변수

  • target: 프록시할 원본 객체
  • handler: 가로채는 작업과 가로채는 작업을 재정의하는 방법을 정의하는 객체

사용 방법

대상 객체인 target 객체를 만들고, new Proxy() 로 프록시 객체를 생성한다.

const target = {
  message1: "hello",
  message2: "everyone"
};

const handler1 = {};

const proxy1 = new Proxy(target, handler1);

프록시 객체인 proxy1 의 모양은 다음과 같다.

Proxy {}
  > [[Handler]]: Object
  > [[Target]]: Object
  > [[IsRevoked]]: false

핸들러에서 아무런 작업을 하지 않기 때문에 원래 대상처럼 작동한다.

console.log(proxy1.message1); // hello
console.log(proxy1.message2); // everyone

프록시를 커스텀하기 위해서 handler를 정의한다. (트랩 함수 추가)

const target = {
  message1: "hello",
  message2: "everyone"
};

const handler2 = {
  get(target, prop, receiver) {
    return "world";
  }
};

const proxy2 = new Proxy(target, handler2);

handler에서 get하는 동시에 world를 return하게 했으므로 기존 값이 아닌 world가 출력된다.

console.log(proxy2.message1); // world
console.log(proxy2.message2); // world

Reflect 를 이용해 다음과 같이 재정의할 수 있다.

const target = {
  message1: "hello",
  message2: "everyone"
};

const handler3 = {
  get(target, prop, receiver) {
    if (prop === "message2") {
      return "world";
    }
    return Reflect.get(...arguments); // target[propertyKey] 값을 반환, target[prop] 값이 undefined가 아닌 TypeError 발생
  },
};

const proxy3 = new Proxy(target, handler3);

console.log(proxy3.message1); // hello
console.log(proxy3.message2); // world

Reflect 🤔 ?
하나의 네임스페이스에 모인 API로 Object 보다 더 깔끔하게 구현 가능하다.
ESLint Rule Preset 사용 시 no-prototype-builtins 규칙으로 인해 obj.hasOwnProperty 사용을 못하고 Object.prototype.hasOwnProperty.call 를 사용해야하는데, Relect를 사용한다면 깔끔하게 코드 작성 가능하다.
Object.prototype.hasOwnProperty.call(obj, 'prop'); -> Reflect.has(obj, 'prop');

Proxy in Vue

Composition API - 반응형 데이터 에서 'reactive는 프록시 객체를 반환한다.' 에서의 프록시 객체가 이때까지 살펴봤던 프록시 객체이다.

@vue > reactivity.cjs.js 파일에서 reactive와 ref 코드를 살펴보자.

reactive

createReactiveObject 에서 살펴볼 수 있듯이 new Proxy 로 프록시 객체를 반환하는 것을 볼 수 있다.

function reactive(target) {
    // if trying to observe a readonly proxy, return the readonly version.
    if (isReadonly(target)) {
        return target;
    }
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    if (!shared.isObject(target)) {
        {
            console.warn(`value cannot be made reactive: ${String(target)}`);
        }
        return target;
    }
    // target is already a Proxy, return it.
    // exception: calling readonly() on a reactive object
    if (target["__v_raw" /* ReactiveFlags.RAW */] &&
        !(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) {
        return target;
    }
    // target already has corresponding Proxy
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
        return existingProxy;
    }
    // only specific value types can be observed.
    const targetType = getTargetType(target);
    if (targetType === 0 /* TargetType.INVALID */) {
        return target;
    }
    const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
    proxyMap.set(target, proxy);
    return proxy;
}

간단히 pseudo-code 로 나타내면 다음과 같다.

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    }
  })
}

ref

reactive 와 다르게 프록시 객체를 반환하지 않고 getter/setter 를 사용하는 것을 볼 수 있다.

function ref(value) {
    return createRef(value, false);
}

function createRef(rawValue, shallow) {
    if (isRef(rawValue)) {
        return rawValue;
    }
    return new RefImpl(rawValue, shallow);
}

class RefImpl {
    constructor(value, __v_isShallow) {
        this.__v_isShallow = __v_isShallow;
        this.dep = undefined;
        this.__v_isRef = true;
        this._rawValue = __v_isShallow ? value : toRaw(value);
        this._value = __v_isShallow ? value : toReactive(value);
    }
    get value() {
        trackRefValue(this);
        return this._value;
    }
    set value(newVal) {
        const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
        newVal = useDirectValue ? newVal : toRaw(newVal);
        if (shared.hasChanged(newVal, this._rawValue)) {
            this._rawValue = newVal;
            this._value = useDirectValue ? newVal : toReactive(newVal);
            triggerRefValue(this, newVal);
        }
    }
}

간단히 pseudo-code 로 나타내면 다음과 같다.

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}

📚 참고

0개의 댓글