Ayden's journal

Proxy와 Reflect에 대한 개인적인 오해

기존 자바스크립트 사용자들은 객체의 속성 접근을 감지하거나 동작을 변경하기 위해 Object.defineProperty() 같은 방법을 사용했다. 그러나 여기에는 동적 속성 추가나 함수 호출과 같은 동작을 탐지하는 데 어려움이 있었다. 이를 해결하기 위해 자바스크립트에 Proxy 처리기가 도입되었다. 이 클래스는 get, set, apply 등 다양한 트랩(trap)을 제공하여 객체의 속성 접근, 값 변경, 삭제, 함수 호출 등의 동작을 자유롭게 제어할 수 있도록 한다.

 

아마도 이것이 Proxy 처리기에 대한 일반적인 인식일 것이다. 나도 큰 틀에서는 이러한 인식에 동의한다. 사소한 문제가 하나 있다면, 나는 '객체의 속성 접근을 감지하거나 동작을 변경'해야할 필요를 느끼지 못했다는 것이다. 더 자세히 설명하면 이렇다. 만약 name과 age라는 프로퍼티로 이루어진 간단한 객체가 있다고 해보자. 비즈니스 정책으로 인해 이름은 최대 5글자까지만, 나이는 120살이 한계다.

Proxy 처리기를 사용해 이러한 요구사항을 처리한다면 아래와 같을 것이다. 하지만 이렇게 Proxy 처리기를 써야할 이유가 있을까? 우리에게는 이미 ─ 자바스크립트가 ES6 부터 제공해주던 ─ 클래스 문법이 있다.

// Proxy 처리기
function createUser(name, age) {
  const basicUserObj = { name, age }
  
  const newUser = new Proxy(basicUserObj, {
    set(obj, prop, value) {
      switch (prop) {
        case "name":
          if (typeof value !== "string") {
            throw new TypeError("Name must be a string")
          }
          if (value.length > 5) {
            throw new RangeError("Name must be less than 5 characters")
          }
          obj.name = value
          break
        case "age":
          if (typeof value !== "number") {
            throw new TypeError("Age must be a number")
          }
          if (value < 0 || value > 120) {
            throw new RangeError("Age must be between 0 and 120")
          }
          obj.age = value
          break
        default:
          throw new Error(`Property ${prop} is not allowed`)
      }
    }
  })

  return newUser
}
// Class 문법
class User {
  #name
  #age

  constructor(name, age) {
    this.#name = name
    this.#age = age
  }

  get name() {
    return this.#name
  }

  get age() {
    return this.#age
  }

  set name(value) {
    if (typeof value !== "string") {
      throw new TypeError("Name must be a string")
    }
    if (value.length > 5) {
      throw new RangeError("Name must be less than 5 characters")
    }
    this.#name = value
  }
  set age(value) {
    if (typeof value !== "number") {
      throw new TypeError("Age must be a number")
    }
    if (value < 0 || value > 120) {
      throw new RangeError("Age must be between 0 and 120")
    }
    this.#age = value
  }
}

 

나는 Proxy 처리기가 할 수 있는 모든 걸 사실상 클래스 문법으로 대체할 수 있으므로, Proxy 처리기는 사용할 필요가 없다고 생각했다. 그리고 이것이 내가 Proxy 처리기에 대해 가장 크게 오해한 부분이다. 나는 Proxy 처리기가 무엇을 하는지에만 집중했고, Proxy 처리기가 무엇이 될 수 있는지는 생각해보지 않았다.

위의 예시에서 볼 수 있는 것처럼 Proxy 처리기의 대상이 되는 객체를 생산하는 주체가 Proxy 처리기를 작성한 개발자라면, Proxy 처리기는 의미가 없다. 하지만 프로젝트에서 사용되는 객체를 생산하는 게 '라이브러리'라면 이야기가 달라진다. 라이브러리가 생산한 객체는 우리가 그 내부 구현을 직접 통제할 수 없기 때문에, 그 객체의 동작을 커스터마이징하거나 감시할 수 있는 유일한 수단이 Proxy 처리기일 수 있다.

이 경우 Proxy는 단순한 감싸기가 아니라, 외부 객체를 우리 코드의 규칙이나 요구사항에 맞게 '적응'시키는 어댑터의 역할을 하게 된다. 즉, Proxy 처리기는 외부 세계와 내부 세계 사이의 인터페이스를 조율하는 핵심 도구가 될 수 있으며, 이런 맥락에서 그 가능성과 역할을 새롭게 인식해야 한다.

 

 

Reflect는 Proxy 처리기 내에서 기존 객체의 기본 동작을 안전하게 위임할 수 있게 해주며, Proxy와 함께 객체의 속성 접근 및 함수 호출을 세밀하게 제어할 수 있는 유용한 도구로 알려져 있다. 이로 인해 나는 Reflect가 Proxy 처리기 내에서만 의미를 갖는 특수한 객체라 오해하고 있었다.

하지만 Reflect는 Proxy와의 조합 외에도 독립적으로 객체의 기본 동작을 직접 호출할 때에도 유용하게 사용될 수 있다. Reflect의 주요 장점은 객체의 기본 동작을 좀 더 명확하고 일관되게 처리할 수 있도록 도와준다는 점이다. 예를 들어, 객체 내의 프로퍼티를 삭제할 때, 일반적으로 delete 키워드를 사용하게 되는데, 이는 엄격 모드에서 예외를 발생시킬 수 있다. 특히, 객체의 속성이 non-configurable로 설정되어 있을 때 delete는 예외를 던지거나 동작하지 않을 수 있다. 이는 코드의 안정성을 저하시킬 수 있는 원인이 된다.
반면, Reflect.deleteProperty()를 사용하면, 속성이 삭제되지 않더라도 예외를 발생시키지 않고 false를 반환한다. 이 방식은 예외를 처리할 필요 없이 좀 더 안전하게 속성을 삭제할 수 있게 해준다. 또한 Reflect는 deleteProperty 외에도 get, set, has와 같은 메서드를 제공하여 객체의 속성에 대한 접근과 조작을 더욱 직관적이고 일관성 있게 처리할 수 있게 돕는다. 이를 통해 코드의 예외 처리를 간소화하고, 객체 동작을 보다 명확하게 제어할 수 있다.

// Object 방식: 예외 또는 false 반환 (엄격 모드에 따라 다름)
try {
  delete obj.nonConfigurableProp; // 엄격 모드에서는 예외
} catch (e) {
  console.error('삭제 실패');
}

// Reflect 방식: 항상 boolean 반환
if (!Reflect.deleteProperty(obj, 'nonConfigurableProp')) {
  console.error('삭제 실패');
}

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기