자바스크립트/개념

자바스크립트의 Proxy 란?

Jake Seo 2022. 6. 30. 11:27

프록시(Proxy)란?

Proxy 오브젝트는 다른 오브젝트를 감싸 연산을 가로챈다. 이를테면 프로퍼티를 읽고 쓰거나 다른 자체적인 오브젝트 조작 연산등을 가로챈다. 혹은 아무런 핸들러도 등록하지 않아 오브젝트가 프로퍼티를 다루는 것을 투명하게 허용할 수도 있다.

일반적으로는 중간에 get 과 같은 연산을 가로채 프로퍼티 접근 로그를 남기거나 set 으로 값이 입력 되기 전 값 검증하기, 값 포맷팅하기, 입력된 값을 깔끔하게 만드는 등의 작업을 추가하여 해당 연산이 일어날 때마다 개발자가 입력한 추가 작업이 수행되도록 하는 방식으로 많이 사용한다.

많은 라이브러리나 브라우저 프레임워크에서 사용된다.

문법

let proxy = new Proxy(target, handler);
  • target: 감쌀 오브젝트이다. 함수를 포함해 어떤 것이든 될 수 있다.
  • handler: 소위 "트랩" 이라 불리는 메서드가 담긴 오브젝트이다. "트랩" 이란 연산을 가로채는 메서드이다.
    • ex) get 트랩은 target 의 프로퍼티를 읽는 연산을 가로챈다.
    • ex) set 트랩은 target 에 프로퍼티가 쓰여지는 것을 가로챈다.

위에서 설명한 "트랩" 이란 단어에 대해 MDN 에서 사용하는 공식적인 명칭은 Hanlder function 이다. Handler functions are sometimes called traps, presumably because they trap calls to the target object.

어떤 메서드를 가로챌 수 있는가?

MDN 문서 의 왼쪽에 Proxy/handler 하위 Methods 라고 적혀있는 부분에 있는 모든 메서드를 활용해 가로챌 수 있다.

 

 

Internal Method Handler Method Triggers when...
[[Get]] get reading a property
[[Set]] set writing to a property
[[HasProperty]] has in operator
[[Delete]] deleteProperty delete operator
[[Call]] apply function call
[[Construct]] construct new operator
[[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf
[[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf
[[IsExtensible]] isExtensible Object.isExtensible
[[PreventExtensions]] preventExtensions Object.preventExtensions
[[DefineOwnProperty]] defineProperty Object.defineProperty, Object.defineProperties
[[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries

주의점

가로채고 난 이후의 주의점도 있는데,

  • [[SET]] 메서드는 값이 올바르게 쓰여졌다면, true 를 반환해야 한다.
  • [[Delete]] 메서드는 올바르게 값이 지워졌다면 true 를 반환해야 한다.
  • [[GetPropertyOf]] 는 항상 타겟 오브젝트의 프로토타입을 반환해야 한다는 것이다.

트랩으로 연산을 가로채어 개발자가 원하는 연산을 추가적으로 넣는 것은 문제가 되지 않지만, 반드시 MDN 문서 를 보고 주의사항을 지켜주어야 한다.

 

예제

프록시에서 일어나는 연산 살펴보기

let target = {};
let proxy = new Proxy(target, {}); // empty handler

proxy.test = 5; // writing to proxy (1)

alert(target.test); // 5, the property appeared in target!
alert(proxy.test); // 5, we can read it from proxy too (2)

for (let key in proxy) alert(key); // test, iteration works (3)
  • 위는 아무런 역할도 하지 않는 빈 핸들러가 추가된 프록시이다.
    • 빈 핸들러가 추가되어, 그냥 평범한 오브젝트와 똑같은 상태이다.

그러나 위에서 일어나는 연산들에 대해서는 한 번 볼만하다.

  • proxy.test = 5 라고 할 때 쓰기(set) 연산이 일어난다.
  • proxy.test 를 가져올 때 읽기(get) 연산이 일어난다.
  • for 문 안에서 반복(iteration) 연산이 일어난다.

get 으로 기본 값 적용해보기

let target = {};
let proxy = new Proxy(target, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    }

    return 5555;
  },
});

proxy.a = 100;

console.log(proxy.a); // 100
console.log(proxy.b); // 5555

위와 같이 get 연산이 일어났을 때 특정한 기본 값(5555)을 설정해 적용할 수 있다.

set 으로 값 검증하기

let numbers = [];

numbers = new Proxy(numbers, {
  set(target, prop, val) {
    if (typeof val === "number") {
      target[prop] = val;
      return true;
    }

    throw "값은 숫자만 입력 가능합니다.";
    return false;
  },
});

numbers.push(100);
numbers.push("안녕"); // Uncaught 값은 숫자만 입력 가능합니다.

console.log(numbers); // Proxy {0: 100}

numbers 라는 배열에 숫자 이외의 타입이 들어올 수 없도록 하게 하는 예제이다.

프록시 set 메서드의 함정

위의 코드가 잘 작동했으니, 아래의 코드의 출력 결과를 예측해보자.

let numbers = [];

numbers = new Proxy(numbers, {
  set(target, prop, val) {
    if (typeof val === "number") {
      target[prop] = val;
      console.log("값이 입력되었습니다.");
      return true;
    }

    throw "값은 숫자만 입력 가능합니다.";
    return false;
  },
});

numbers.push(1);
  • "값이 입력되었습니다." 가 두번 출력된다.
    • 배열의 protype 메서드인 push() 에는 length 를 수정하는 내용도 포함되어 있기 때문이다.

오브젝트에 값을 직접 할당하는 것이 아닌 내부 프로토타입 메서드를 이용할 때는 어떤 연산들을 거쳐서 해당 prototype 메서드가 수행되는 것인지 제대로 알아야 내가 원하지 않은 동작이 일어나는 것을 예방할 수 있다.

set 연산을 이용할 때는 성공 시 ture 를 반환하는 것도 잊지 말자.

ownKeys 로 접근 제어하기

let user = {
  name: "John",
  age: 30,
  _password: "***",
};

user = new Proxy(user, {
  ownKeys(target) {
    return Object.keys(target).filter((key) => !key.startsWith("_"));
  },
});

// "ownKeys" filters out _password
for (let key in user) alert(key); // name, then: age

// same effect on these methods:
alert(Object.keys(user)); // name,age
alert(Object.values(user)); // John,30
  • Object.keys() 혹은 Object.values() 로 자바스크립트 오브젝트의 키나 밸류를 추출할 때, 자동으로 ownKeys 연산이 수행된다.
  • 위는 _ 로 시작하는 key 인 경우에 ownKeys 에서 false 를 반환하여 iteration 이 일어나지 않게 하는 예제이다.
    • _password 라는 필드명을 가진 패스워드는 iteration 에 걸리지 않는다.

Object.keys(), Object.values(), Object.getOwnPropertyNames(), for ... in 루프는 [[OwnPropertyKeys]] 내부 메서드를 사용한다.

range 오브젝트 만들기

let range = {
  start: 1,
  end: 10,
};

range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end;
  },
});

alert(5 in range); // true
alert(50 in range); // false

실제로 오브젝트에 해당 숫자가 없더라도, 범위 내의 숫자라면 true 가 반환된다.

apply 를 통해 사용자가 정의한 딜레이를 가진 프록시 만들기

function delay(f, ms) {
  return new Proxy(f, {
    apply(target, thisArg, args) {
      setTimeout(() => target.apply(thisArg, args), ms);
    },
  });
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 1 (*) proxy forwards "get length" operation to the target
sayHi("John"); // Hello, John! (after 3 seconds)

사용자가 정의한 함수인 sayHi 에 대해 3초의 딜레이를 가지는 새로운 함수를 만들었다.

레퍼런스

반응형