Code Logs

21.10.25

Javascript ES6 Proxy, Proxy, Trap, 프락시를 이용한 객체 조작의 제어
고양이 빵 Cat bread - 세마리의 고양이와 홈베이킹 이야기

Proxy

Javascript ES6 Proxy, Proxy, Trap, 프락시를 이용한 객체 조작의 제어

Table of contents

  1. Proxy?
  2. 용법
    1. Trap의 종류
  3. Sample
    1. Get
    2. Set
    3. Apply
    4. Construct
  4. Revocable proxy
  5. 마치며

Proxy?

프락시라는 말은 오늘 기록하려고 하는 javascript에서뿐 아니라 다양한 분야에서 범용적으로 사용되는 용어인듯하다. 나에게 익숙한 용어는 Proxy server인데 웹서버 구성시 Reverse proxy 설정을 유용히 사용하면서 익숙해졌다.

Reverse proxy?

클라이언트로부터의 요청을 프락시 서버가 직접 받아 내부 네트워크에서 자원을 다운받고 응답하는 네트워크 구조

WAS가 요청을 처리하는 것이 아닌 요청과 응답 사이에 대리인 (Proxy)이 개입하여 이를 중계함

Proxy의 사전적 의미는 '대리'로 중계의 역할을 수행하는 것에 보통 사용된다.

javascript에서의 프락시 또한 어떤 객체의 조작이 발생할 때 이를 가로채서 Proxy 내부의 로직을 통해 대상 객체를 조작하는 것을 말한다.

용법

const target = {}
const handler = {
  get(target, prop) {
    if (prop in target) {
      return target[prop]
    } else {
      return `타겟 오브젝트에 해당 프로퍼티 (${prop})가 존재하지 않습니다.`
    }
  },
}

const proxy = new Proxy(target, handler)
console.log(proxy.name) /* 타겟 오브젝트에 해당 프로퍼티 (name)가 존재하지 않습니다. */

Proxy 객체는 두개의 파라미터를 인자로 받는다. 첫번째는 Proxy를 적용할 대상 객체이고 두번째는 대상 객체를 조작할 때 호출될 핸들러 객체이다. 핸들러는 trap이라고 불리는 매서드를 가질 수 있으며, trap은 대상 객체의 조작이 발생 할 경우 그것을 가로채어 처리하는 역할을 한다.

생성된 proxy에 조작이 발생 했을때 handler 내부에 해당 조작을 가로챌 trap이 존재한다면 trap 매서드가 호출된다.

예제에서 본 것과 같이 trap은 정해진 명칭을 통해 구현해야한다.

Trap의 종류

내부 메서드 핸들러 메서드 명칭 트리거 구현 규칙
[[Get]] get 프로퍼티 읽기
[[Set]] set 프로퍼티 쓰기 프로퍼티 쓰기 성공여부를 true/false로 리턴 해야함
[[HasProperty]] has in 연산자 사용
[[Delete]] deleteProperty 프로퍼티 삭제 프로퍼티 삭제 성공여부를 true/false로 리턴 해야함
[[Call]] apply 함수 호출
[[Construct]] construct new 연산자 사용 반드시 생성자를 통해 호출된 객체를 리턴 해야함
[[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, Object.values, Object.entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys, Object.values, Object.entries

Trap의 기본적인 규칙은 Proxy를 통해 호출되지 않았을 때와 동일한 유형의 값을 return 해야 한다는데 있다.

Sample

Get

다음 예제는 Proxy를 이용하여 객체의 프로퍼티를 조회할 때 해당 프로퍼티가 없을 경우 기본값을 반환하도록 한다.

const defaultValueProxy = (target, defaultValue) => {
  const handler = {
    get(target, prop) {
      if (prop in target) {
        return prop[target]
      } else {
        return defaultValue
      }
    },
  }

  return new Proxy(target, handler)
}

const target = { prop1: 'value 1' }
const defaultValueTarget = defaultValueProxy(target, 'default value')
console.log(defaultValueTarget.prop1) /* value 1 */
console.log(defaultValueTarget.prop2) /* default value */

Set

다음 예제는 Proxy를 이용하여 객체의 프로퍼티를 쓰기 전 Validation을 수행한다.

const validatorProxy = (target, validator) => {
  const handler = {
    set(target, prop, value) {
      try {
        if (prop in validator) {
          validator[prop](value)
        }

        target[prop] = value
        return true
      } catch (e) {
        console.error(e.message)
        return false
      }
    },
  }

  return new Proxy(target, handler)
}

const profile = validatorProxy(
  {},
  {
    age(age) {
      if (typeof age !== 'number') throw new Error('나이는 반드시 숫자 유형이여야 합니다.')
      if (age < 0) throw new Error('나이는 0 보다 작을 수 없습니다.')
    },
  }
)

profile.name = '홍길동'
profile.age = '스물'
console.log(`Age: ${profile.age}`) /* Age: undefined */
profile.age = -20
console.log(`Age: ${profile.age}`) /* Age: undefined */
profile.age = 20
console.log(`Age: ${profile.age}`) /* Age: 20 */

Apply

다음 예제는 함수 호출시 함수 동작 시간을 출력한다.

const timestampProxy = (target) => {
  const handler = {
    apply(target, thisArgs, args) {
      console.time('소요시간')
      target.apply(thisArgs, args)
      console.timeEnd('소요시간')
    },
  }
  return new Proxy(target, handler)
}

const sleep = (second) => {
  const startedAt = Date.now()
  while ((Date.now() - startedAt) / 1000 < second) {} // sleep

  console.log('Done')
}

const sleepTimestamp = timestampProxy(sleep)
sleepTimestamp(3)
/*
  Done
  소요시간: 3000ms 
*/

Proxy의 대상은 객체이기 때문에 함수도 그 대상이 될 수 있다.

Construct

const strictConstructor = (target, paramCount) => {
  const handler = {
    construct(target, args) {
      if (args.length < paramCount) throw new Error('생성자 호출을 위해 필요한 모든 파라미터를 전달 받지 못했습니다.')
      return new target(...args)
    },
  }

  return new Proxy(target, handler)
}

class Person {
  constructor(name, age, nickname, hobby) {
    this.name = name
    this.age = age
    this.nickname = nickname
    this.hobby = hobby
  }

  profile() {
    console.table(this)
  }
}

const person = new Person()
person.profile()
/*
  | (index)  |  Values   |
  ------------------------
  |   name   | undefined |
  |   age    | undefined |
  | nickname | undefined |
  |  hobby   | undefined |
*/
const StrictPerson = strictConstructor(Person, 2)
new StrictPerson('John')
/*
  Error: 생성자 호출을 위해 필요한 모든 파라미터를 전달 받지 못했습니다.
*/
const strictPerson = new StrictPerson('John', 20)
strictPerson.profile()
/*
  | (index)  |  Values   |
  ------------------------
  |   name   |  'John'   |
  |   age    |    20     |
  | nickname | undefined |
  |  hobby   | undefined |
*/

Revocable proxy

Proxy를 통해 생성된 객체는 GC의 대상에서 제외된다. 이것을 GC의 대상으로 포함시키기 위해서는 아래와 같은 방식으로 Proxy를 생성해야한다.

GC (Garbage Collection)

메모리 관리 기법 중의 하나로, 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요없게 된 영역을 해제하는 기능 - wiki

const target = {}
const handler = {}
const revocableProxy = Proxy.revocable(target, handler)

new 연산자를 통해 Proxy를 생성하는 것이 아닌 static method인 revocable을 호출하는 것으로 Proxy 객체를 생성하면 된다. 그 외의 사용법은 new 연산자를 통해 생성한 Proxy와 동일하다.

revocable proxy의 경우 proxy 프로퍼티를 통해 대상 객체의 인자에 접근 할 수 있다. revocable proxy의 사용이 완료되고 GC의 대상으로 포함시키기 위해서는 아래와 같이 명시적으로 revoke를 호출해주면 된다. revoke가 호출된 이후 대상 객체에 대한 조작을 시도하면 TypeError를 발생시키고 revocable proxyGC의 대상이 되어 폐기된다.

const revocableProxy = (target) => {
  const handler = {
    get(target, prop) {
      console.log('Proxy Getter')
      return target[prop]
    },
  }

  return Proxy.revocable(target, handler)
}

const revocable = revocableProxy({ name: '홍길동' })
revocable.proxy.name
/*
  Proxy Getter
  홍길동
*/
revocable.revoke()
revocable.proxy.name
/*
  TypeError: Cannot perform 'get' on a proxy that has been revoked
*/

마치며

Proxy는 객체 내부 인자에 대한 조작을 감지 할 수 있다는 것 자체로도 많은 잠재력을 가지고 있다. 상황에 맞게 Proxy를 이용하면 객체의 상태 감지를 통한 특별한 작업들을 할 수 있다. (상태 관리 도구와 같은)

카테고리 더보기

    댓글