PUBLISHED
Proxy와 Reflect에 대한 개인적인 오해
작성일: 2025.04.24

기존 자바스크립트 사용자들은 객체의 속성 접근을 감지하거나 동작을 변경하기 위해 Object.defineProperty() 같은 저수준 API를 활용해 왔다. 이 방식은 특정 프로퍼티의 getter나 setter를 재정의하는 데에는 유용했지만, 동적으로 추가되는 속성이나 함수 호출, 삭제 연산 같은 보다 포괄적인 동작을 감지하는 데에는 명확한 한계가 있었다.
이러한 제약을 해결하기 위해 도입된 기능이 바로 Proxy 처리기다. Proxy는 get, set, has, deleteProperty, apply, construct 등 다양한 트랩(trap)을 제공하며, 객체에 가해지는 거의 모든 연산을 가로채고 재정의할 수 있도록 한다. 이 덕분에 객체의 속성 접근, 값 변경, 삭제, 함수 호출까지도 자유롭게 제어할 수 있다.
아마도 이것이 Proxy 처리기에 대한 가장 일반적인 설명일 것이다. 나 역시 큰 틀에서는 이 설명에 동의한다. 다만 한 가지 사소하지만 결정적인 문제가 있었다. 나는 “객체의 속성 접근을 감지하거나 동작을 변경해야 할 필요성” 자체를 거의 느껴본 적이 없었다는 점이다.
조금 더 구체적으로 설명해보자. 예를 들어 name과 age라는 두 프로퍼티를 가진 단순한 객체가 있고, 비즈니스 정책상 이름은 최대 5글자, 나이는 최대 120살까지만 허용해야 한다고 가정해보자. 이 요구사항을 Proxy로 구현하면 다음과 같은 코드가 된다.
function createUser(name, age) {
const user = { name, age };
return new Proxy(user, {
set(target, prop, value) {
if (prop === "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");
}
}
if (prop === "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");
}
}
target[prop] = value;
return true;
},
});
}하지만 이 코드를 처음 작성했을 때 든 생각은 단순했다. “이걸 왜 Proxy로 해야 하지?” 우리에게는 이미 ES6부터 제공되는 클래스 문법이 있다. 동일한 요구사항은 훨씬 익숙한 방식으로도 표현할 수 있다.
class User {
#name;
#age;
constructor(name, age) {
this.name = name;
this.age = age;
}
get name() {
return this.#name;
}
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;
}
get age() {
return this.#age;
}
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가 사실상 의미가 없다. 클래스와 캡슐화로 충분히 해결 가능하기 때문이다.
하지만 상황이 달라지는 지점이 있다. 객체를 생산하는 주체가 내가 아니라 라이브러리인 경우다. 라이브러리가 반환한 객체는 내부 구현을 직접 수정할 수 없다. 클래스를 상속할 수도 없고, setter를 추가할 수도 없다. 그 객체가 어떤 방식으로 동작하는지는 라이브러리 작성자의 결정이다. 이때, 해당 객체의 동작을 우리 코드의 규칙에 맞게 감시하거나 제어할 수 있는 거의 유일한 수단이 Proxy다.
const externalUser = someLibrary.createUser();
const safeUser = new Proxy(externalUser, {
set(target, prop, value) {
if (prop === "age" && value > 120) {
throw new Error("Invalid age");
}
target[prop] = value;
return true;
},
});이 순간 Proxy는 단순한 “감싸기 도구”가 아니다. 외부에서 들어온 객체를 우리 시스템의 규칙에 맞게 적응시키는 어댑터(Adapter) 가 된다.
이 관점에서 보면 Proxy는 객체의 속성을 감지하는 도구가 아니라, 외부 세계와 내부 세계 사이의 인터페이스를 조율하는 장치다. 라이브러리나 프레임워크가 제공하는 객체를 그대로 신뢰하지 않고, 우리 코드의 경계에서 한 번 더 통제할 수 있게 해주는 안전장치인 셈이다. 이 지점에서 Proxy의 역할과 가능성은 완전히 다르게 보이기 시작했다.
Proxy 이야기를 하다 보면 자연스럽게 함께 등장하는 객체가 있다. 바로 Reflect다. 일반적으로 Reflect는 “Proxy 처리기 안에서 기본 동작을 위임하기 위한 보조 도구” 정도로 소개된다. 나 역시 한동안은 Reflect가 Proxy 내부에서만 의미를 갖는 특수한 객체라고 오해하고 있었다.
하지만 Reflect는 Proxy와 무관하게도 충분히 유용한 API다. Reflect의 핵심 가치는 객체의 기본 동작을 명령형이고 일관된 방식으로 호출할 수 있게 해준다는 점에 있다. 대표적인 예가 프로퍼티 삭제다. 일반적으로 우리는 delete 연산자를 사용한다.
delete obj.prop;하지만 이 연산은 상황에 따라 동작이 달라진다. 특히 엄격 모드에서, 혹은 non-configurable 속성을 삭제하려고 할 경우 예외를 던질 수 있다.
try {
delete obj.nonConfigurableProp;
} catch (e) {
console.error("삭제 실패");
}반면 Reflect.deleteProperty()는 훨씬 일관된 인터페이스를 제공한다.
if (!Reflect.deleteProperty(obj, "nonConfigurableProp")) {
console.error("삭제 실패");
}예외를 던지는 대신 항상 boolean 값을 반환한다. 이 차이는 코드의 안정성과 가독성에 꽤 큰 영향을 미친다. Reflect는 이 외에도 get, set, has 같은 메서드를 제공한다.
Reflect.get(obj, "name");
Reflect.set(obj, "age", 30);
Reflect.has(obj, "id");이 메서드들은 모두 Proxy의 트랩 이름과 정확히 대응된다. 하지만 Proxy 내부에서만 쓰라고 존재하는 것은 아니다. 오히려 객체 조작을 함수 호출 형태로 명시적으로 표현하고 싶을 때, Reflect는 매우 깔끔한 대안이 된다.
정리해보면, Proxy와 Reflect는 단순히 “객체를 건드리는 신기한 문법”이 아니다. Proxy는 우리가 소유하지 않은 객체를 다루는 방법이고, Reflect는 객체의 기본 동작을 예측 가능하게 호출하는 도구다. 이 둘을 함께 바라보지 않고 각각 따로 이해하면, Proxy는 과한 도구처럼 보이고 Reflect는 쓸모없는 API처럼 느껴지기 쉽다.
하지만 라이브러리 경계, 시스템 경계, 외부 객체와 내부 규칙이 만나는 지점에 이르면 이 둘은 갑자기 매우 현실적인 도구가 된다. Proxy에 대한 나의 인식 변화는 문법을 새로 배웠기 때문이 아니라, 사용 맥락을 다시 생각했기 때문이었다. 자바스크립트의 많은 기능이 그렇듯, Proxy 역시 “언제 쓰느냐”를 이해했을 때 비로소 제 역할을 드러낸다.