JS Proxy (feat. Vue3)

4 minute read

개요

Vue3에서 Proxy를 통해 reactive object를 만든다고 들었다.1 reactive 변수를 console.log로 찍어보면 Proxy로 감싸져있는걸 볼 수 있다. 이 Proxy가 도대체 뭐길래 활용했다는걸까? Vue3 쪽을 자세히 보기전에 ES6에 등장한 Proxy에 대해 공부하고 기록하려한다.

Proxy

Proxy는 ES6에 새로 등장했다. “대리”라는 뜻을 가진 Proxy 이름에서도 알 수 있듯이, 객체의 여러 동작을 중간에 가로채 다른 동작을 할 수 있게 한다. 여기서 말하는 객체는 Object, Array, Dictionary와 같이 JS 모든 자료형이 대상이다. Proxy는 2개의 파라미터로 생성한다.2

const proxy = new Proxy(target, handler);
  • target: proxy로 감싸질 객체
  • handler: target이 가지고 있는 동작을 가로채서 다른 동작을 할 수 있게 정의하는 객체

handler functions (traps)

handler 안에 여러 함수를 정의할 수 있다. 이 함수들을 traps라고 부른다. 그렇다면, traps는 어떤 작업을 가로챌 수 있을까? 3

JS 명세서(specification) 4 내부 메서드가 정의되어있는데, 이 내부 메서드는 객체에 행해지는 작업들이 low level에서 어떻게 동작하는지 관여한다. 예를들어, 객체 프로퍼티를 읽을 때에는 [[Get]]이라는 내부 메서드가 호출된다. 객체 프로퍼티에 쓸 때에는 [[Set]]이라는 내부 메서드가 호출된다. 이러한 내부 메서드를 proxytrap이 가로챌 수 있다. proxy가 가로채는 내부 메서드와 각 handler 메서드는 아래와 같다.

Internal Method Handler Method
[[GetPrototypeOf]] getPrototypeOf
[[SetPrototypeOf]] setPrototypeOf
[[IsExtensible]] isExtensible
[[PreventExtensions]] preventExtensions
[[GetOwnProperty]] getOwnPropertyDescriptor
[[DefineOwnProperty]] defineProperty
[[HasProperty]] has
[[Get]] get
[[Set]] set
[[Delete]] deleteProperty
[[OwnPropertyKeys]] ownKeys
[[Call]] apply
[[Construct]] construct

예제 - empty handler

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

const handler1 = {};

const proxy1 = new Proxy(target, handler1);

위 handler는 빈 객체다. handler가 비어있으면 proxy에 가해지는 작업은 곧바로 target에 전달된다. 빈 handler를 가진 proxy는 원래 target과 동작이 똑같다.

예제 - get method

var handler = {
  get(target, prop) {
    return prop in target ? target[prop] : "email";
  }
};

let personA = new Proxy({}, handler);
personA.age = 30; 
console.log(personA.age, personA.loginType); // 30, "email"

property가 존재하면 값을 리턴(target[prop])하고, 존재하지 않을 때에는(undefined) “email”이라는 값이 나온다.

예제 - has method (feat.private properties)

handler.has()[[HasProperty]] 내부 메서드를 가로채는 trap이다. in operator와 같은 곳에서 사용된다.

JS에서는 private하게 사용하는 변수/프로퍼티 이름에 _(underscore) prefix를 붙일 때가 많다. private한 프로퍼티는 밖에서 읽을 수 없게 하고 싶을 때 다음과 같이 has trap을 사용할 수 있다.

const handler1 = {
  has(target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  },
};

const monster1 = {
  _secret: 'easily scared',
  eyeCount: 4,
};

const proxy1 = new Proxy(monster1, handler1);
console.log('eyeCount' in proxy1); // true
console.log('_secret' in proxy1); // false
console.log('_secret' in monster1); // true (proxy가 아닌 원래 객체)

proxy가 아닌 monster1 객체에는 _secret 프로퍼티를 읽을 수 있다 (리턴값 true). 하지만 proxy를 통해 _secret 프로퍼티를 읽으려고 하면 false가 리턴된다.

예제 - set method

앞의 has 예제에 이어 set trap을 이용해 _ prefix가 붙은 private 프로퍼티의 값을 바꿀(재할당) 수 없게 할 수 있다.

const handler = {
  // get(target, prop) {
  //   if (prop[0] === '_') {
  //     return undefined;
  //   }
  //   return target[prop];
  // },
  has(target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  },
  set(target, key, value) {
    if (key[0] === '_') {
      return false;
    }
    target[key] = value;
  },
};

const monster1 = {
  _secret: 'easily scared',
  eyeCount: 4,
};

const monster = new Proxy(monster1, handler);

예제 - isExtensible, preventExtensions (feat.Reflect)

isExtensible, preventExtensions trap은 각각 [[IsExtensible]], [[PreventExtensions]] 내부 메서드를 가로챈다. [[IsExtensible]] 내부 메서드는 Object.isExtensible() 메서드를 호출할 때 실행된다. [[PreventExtensions]] 내부 메서드는 Object.preventExtensions() 메서드를 호출할 때 실행된다.

Object.isExtensible()은 객체가 새로운 프로퍼티를 가져서 확장 가능한지를 리턴하는 static 메서드다. Object.preventExtensions()는 객체에 새로운 프로퍼티를 추가할 수 없게 만드는 static 메서드다. 해당 객체의 prototype까지 재할당 못하게 막는다.5 블로그 글에 따르면 npm 라이브러리를 만들때 Object.preventExtensions()를 사용하는 경우가 많다고 한다. 해당 라이브러리를 사용하는 사람이 라이브러리의 객체를 확장할 수 없게 하기 위해서다.

const monster1 = {
  canEvolve: true,
};

const handler1 = {
  isExtensible(target) {
    return Reflect.isExtensible(target);
  },
  preventExtensions(target) {
    target.canEvolve = false;
    return Reflect.preventExtensions(target);
  },
};

const proxy1 = new Proxy(monster1, handler1);

console.log(Object.isExtensible(proxy1)); // true
console.log(monster1.canEvolve); // true

Object.preventExtensions(proxy1);

console.log(Object.isExtensible(proxy1)); // false
console.log(monster1.canEvolve); // false

mdn사이트에서 가져온 예제를 보면, isExtensiblepreventExtensions trap 안에서 Reflect함수들을 부른다. 위처럼 Proxy는 Reflect 객체와 함께 사용하는 경우가 많다. Proxy trap으로 사용가능한 모든 내부 메서드는 Reflect 객체에도 매칭되는 메서드가 존재한다. 이러한 Reflect객체의 메서드는 Proxy trap 메서드와 동일한 이름과 파라미터를 가진다.

Proxy handler 내부에서 Reflect메서드를 호출해 원래 객체에 해당 operation을 던질 수 있다. (참고: 원래 [[Get]], [[Set]], [[IsExtensible]], [[PreventExtensions]]와 같은 내부 메서드는 명세서에만 정의되어 있고 개발자가 직접 호출할 수 없다. Reflect객체는 이러한 내부 메서드를 호출할 수 있게 해준다. Reflect의 메서드는 내부 메서드를 minimal하게 감싸고 있다. )

예제 - has,set (feat.Reflect)

위에서 set예제에서 보여줬던 코드를 Reflect을 사용해서 다음과 같이 바꿀 수 있다.



const handler = {
  has(target, key) {
    if (key[0] === '_') {
      return false;
    }
    return Reflect.has(target, key); // (1)
  },
  set(target, key, value) {
    if (key[0] === '_') {
      return false;
    }
    return Reflect.set(target, key, value); // (2)
  },
};

const monster1 = {
  _secret: 'easily scared',
  eyeCount: 4,
};
const monster = new Proxy(monster1, handler);

(1)과 (2)로 표시된 부분이 Reflect를 사용해 변경한 부분이다.

예제 - apply method (feat. log)

apply trap은 [[Call]] 내부 메서드를 가로챈다. [[Call]] 내부 메서드는 함수를 호출할 때 실행된다. apply trap은 객체가 함수일 때 사용해서, 함수가 호출될 때마다 로그를 찍거나, 사전 동작을 수행할 수 있다.6


const proxy = new Proxy(function(){}, {
  apply(target, thisArg, argList) {
    console.log(`target`, target);
    console.log(`thisArg`, thisArg);
    console.log(`argArray`, argList);
    console.log(`[INFO] 로그로그 호출됨`, argList.join(', ')); // [INFO] 로그로그 호출됨 1, 2, 3
    return target.apply(thisArg, argList);
  }
});

console.log(proxy(1,2,3));

SideNote - Vue3에서 toRaw()

Vue3에서 toRaw()7라는 함수를 사용하면, Vue가 Proxy로 감싸서 만든 reactive 변수를 unwrap하여 원래 object를 리턴해준다는걸 알게되었다. Vue3에서 아래와 같이 쓸 수 있다.


<script setup>
import { reactive, toRaw } from "vue";

const helloObj = reactive({
  name: "hello",
  age: 20,
});

console.log(`helloObj`, helloObj);
console.log(`toRaw(helloObj)`, toRaw(helloObj));
</script>

콘솔 결과는 아래와 같다.

20231012_proxy_vue.png

References

Leave a comment