자바스크립트에서 객체를 다루다 보면, 프로퍼티에 접근하거나 값을 변경할 때 “자동으로 추가 동작을 실행”하고 싶은 순간이 있습니다. 예를 들어, 객체의 값이 바뀔 때마다 로그를 남기거나, 유효하지 않은 값이 들어오면 막아버리거나, 값이 바뀌면 화면을 자동으로 갱신하고 싶은 경우가 대표적이죠.
이전 글에서 살펴본 내부 메서드는 자바스크립트 엔진이 내부적으로 사용하는 동작이라, 개발자가 직접 가로채거나 변경할 수 없었는데요. Proxy를 사용하면 바로 이 내부 동작을 가로채서 원하는 로직을 끼워 넣을 수 있고, Reflect는 그 과정에서 원래의 기본 동작을 안전하게 수행할 수 있도록 도와줍니다. 이번 글에서는 Proxy와 Reflect가 무엇인지, 그리고 실무에서는 어떻게 활용되는지까지 함께 살펴보겠습니다.
미리 요점만 콕 집어보면?
- Proxy는 객체의 기본 동작을 가로채서 커스텀 로직을 추가할 수 있는 도구이고, Reflect는 원래 동작을 안전하게 수행하도록 돕는 짝꿍입니다.
- get 트랩과 set 트랩을 통해 프로퍼티 읽기와 쓰기를 가로채 유효성 검사나 반환 값 제어가 가능하며, 트랩은 내부 메서드와 상당 부분 1:1로 대응하도록 설계되어 있습니다.
- 트랩 안에서 Reflect를 통해 원래 동작을 위임하는 것이 가장 안전하고 권장되는 패턴이며, 성능 오버헤드와 일부 내장 객체와의 호환성 문제에 주의해야 합니다.
Proxy는 영어로 “대리인”이라는 뜻을 가지고 있는데요, 자바스크립트에서의 Proxy도 비슷합니다. 어떤 객체에 대한 작업을 직접 처리하는 대신, 중간에 대리인을 두고 그 대리인이 작업을 가로채서 추가적인 동작을 수행할 수 있게 해주는 객체입니다.

Proxy를 이해하려면 세 가지 핵심 용어를 알아야 합니다. target은 Proxy가 감는 원본 객체, handler는 가로채는 동작을 정의하는 객체, 그리고 trap은 handler 안에 정의하는 메서드로, 말 그대로 특정 동작을 가로채는 “함정”입니다.
const user = {
name: "홍길동",
age: 27,
};
const handler = {
// 여기에 트랩(Trap)을 정의합니다
};
const proxy = new Proxy(user, handler);
handler에 아무런 트랩도 정의하지 않으면, Proxy는 원본 객체에 대한 동작을 그대로 통과시킵니다.
아래 코드는 get 트랩을 사용해서, 프로퍼티를 읽을 때마다 로그를 남기는 예제입니다.
const user = {
name: "홍길동",
age: 27,
};
const handler = {
get(target, prop) {
console.log(`${prop} 프로퍼티에 접근했습니다.`);
return target[prop];
},
};
const proxy = new Proxy(user, handler);
console.log(proxy.name);
// "name 프로퍼티에 접근했습니다."
// "홍길동"
get 트랩의 첫 번째 매개변수 target은 원본 객체, 두 번째 prop은 접근하려는 프로퍼티의 이름입니다. 이렇게 Proxy를 사용하면, 객체 접근 자체를 가로채서 원하는 로직을 추가할 수 있습니다.
Proxy에서 사용할 수 있는 트랩은 get 외에도 다양합니다. 프로퍼티를 읽을 때, 쓸 때, 삭제할 때 등 객체에 대한 거의 모든 동작을 가로챌 수 있는데요, 실무에서 자주 사용되는 트랩들을 살펴보겠습니다.
get 트랩은 객체의 프로퍼티를 읽는 동작을 가로채는 트랩입니다. 앞서 간단한 로그 예제를 살펴보았는데요, get 트랩의 핵심은 프로퍼티를 읽을 때 원래 값을 그대로 반환하는 것이 아니라, 개발자가 원하는 방식으로 반환 값을 가공하거나 제어할 수 있다는 점입니다.
예를 들어, 존재하지 않는 프로퍼티에 접근했을 때 undefined 대신 의미 있는 메시지를 반환하도록 만들어 볼 수 있습니다.
const user = {
name: "홍길동",
age: 27,
};
const handler = {
get(target, prop) {
if (prop in target) {
return target[prop];
}
return `"${prop}" 프로퍼티는 존재하지 않습니다.`;
},
};
const proxy = new Proxy(user, handler);
console.log(proxy.name); // "홍길동"
일반 객체였다면 proxy.address는 undefined를 반환했을 텐데요, get 트랩 덕분에 프로퍼티 읽기 동작 자체를 가로채서, 원하는 방식으로 결과를 바꿀 수 있게 된 것입니다.
set 트랩은 객체의 프로퍼티에 값을 할당하는 동작을 가로채는 트랩입니다. get 트랩이 읽기를 가로챘다면, set 트랩은 쓰기를 가로채는 것이죠. 값이 저장되기 전에 원하는 로직을 먼저 실행할 수 있기 때문에, 대표적으로 유효성 검사에 활용됩니다.
const user = {
name: "홍길동",
age: 27,
};
const handler = {
set(target, prop, value) {
if (prop === "age" && (typeof value !== "number" || value < 0)) {
throw new Error("나이는 0 이상의 숫자여야 합니다.");
}
target[prop] = value;
return true;
},
};
const proxy =
set 트랩은 target, prop, value 세 가지 매개변수를 받으며, 반드시 true 또는 false를 반환해야 합니다.
앞서 내부 슬롯과 내부 메서드를 다룰 때, 자바스크립트 엔진이 객체를 처리하기 위해 호출하는 내부 메서드들을 살펴보았는데요, 사실 Proxy의 트랩은 프로퍼티 접근/할당/삭제처럼 객체의 기본 동작을 수행하는 내부 메서드와 상당 부분 1:1로 대응하도록 설계되어 있습니다.

즉, Proxy는 자바스크립트 엔진이 내부적으로 수행하는 동작을 개발자가 가로챌 수 있도록 열어주는 공식적인 방법이라고 할 수 있습니다.
트랩 안에서 “원래 동작을 그대로 수행”해야 하는 경우가 자주 있는데요, 이때 사용하는 것이 바로 Reflect입니다. Reflect는 자바스크립트의 내장 객체로, 객체에 대한 기본 동작을 메서드 형태로 제공합니다. 메서드 이름은 Proxy의 트랩 이름과 정확히 동일합니다.

“트랩 안에서 target[prop]처럼 직접 접근하면 되지 않나?”라고 생각할 수 있는데요, 간단한 경우에는 문제가 없지만, 상속 관계에서 문제가 발생합니다.
const parent = {
_name: "부모",
get name() {
return this._name;
},
};
const handler = {
get(target, prop, receiver) {
return target[prop]; // Reflect를 사용하지 않음
},
};
const proxy = new Proxy(parent, handler);
const child = Object.create(proxy);
child._name = "자식";
console.log(child.name);
target[prop]을 사용하면 getter 안의 this가 parent를 가리키게 되어, child._name이 아닌 parent._name이 반환됩니다.
Reflect.get()의 세 번째 인자 receiver는 getter 안에서 this로 사용될 객체를 지정합니다.
const handler = {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
};
const proxy = new Proxy(parent, handler);
const child = Object.create(proxy);
child._name = "자식";
console.log(child.name); // "자식" (정상 동작)
이처럼 트랩 안에서 원하는 로직을 수행한 후, Reflect를 통해 원래 동작을 위임하는 것이 가장 안전하고 권장되는 패턴입니다.
Proxy는 단순히 개념적인 도구가 아니라, 실제 프론트엔드 프레임워크에서도 핵심적으로 사용되고 있습니다. 대표적인 활용 사례를 살펴보겠습니다.
가장 대표적인 활용 사례가 바로 반응형 시스템입니다. Vue 3의 reactive()는 내부적으로 Proxy를 기반으로 구현되어 있는데요. 원리를 간단하게 살펴보면, set 트랩에서 값 변경을 감지하고 화면 갱신 로직을 실행하는 구조입니다.
function reactive(target) {
return new Proxy(target, {
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
console.log(`${prop} 값이 변경되었습니다. 화면을 갱신합니다.`);
return result;
},
});
}
const state = reactive({ count: 0 });
state.count = 1;
// "count 값이 변경되었습니다. 화면을 갱신합니다."
Vue 3가 이전 버전에서도 사용하던 Object.defineProperty() 대신 Proxy를 선택한 이유도, Proxy가 객체에 대한 대부분의 기본 동작을 가로챌 수 있어 더 유연하고 강력하기 때문입니다.
Proxy는 강력한 도구이지만, 몇 가지 주의할 점이 있습니다.
지금까지 자바스크립트의 Proxy와 Reflect에 대해 살펴보았습니다. Proxy는 객체의 기본 동작을 가로채서 커스텀 로직을 추가할 수 있는 도구이고, Reflect는 그 과정에서 원래 동작을 안전하게 수행하도록 돕는 짝꿍입니다.
특히 Proxy의 트랩은 자바스크립트 엔진의 내부 메서드와 상당 부분 1:1로 대응되기 때문에, 이전에 살펴본 내부 슬롯과 내부 메서드의 개념을 함께 이해하고 있다면 훨씬 깊이 있게 활용할 수 있습니다. Proxy는 이미 우리가 사용하는 프레임워크 곳곳에서 활용되고 있는데요, 이번 글을 통해 Proxy와 Reflect의 기본 원리를 이해했다면, 다음에는 직접 활용해 보시길 바랍니다.
©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.