Proxy란
Proxy는 ES2015에서 도입된 메타프로그래밍 기능입니다. 대상 객체(target) 위에 가상 레이어 를 씌워, 속성 접근, 할당, 열거 등 거의 모든 기본 작업을 가로챌 수 있습니다.
const proxy = new Proxy(target, handler);target, 감쌀 원본 객체 (어떤 객체든 가능)handler, 가로챌 작업을 정의하는 객체. handler의 메서드를 트랩(trap) 이라 합니다
handler가 빈 객체 {}이면 모든 작업이 target에 그대로 전달됩니다. 트랩을 정의하면 해당 작업만 가로채고, 나머지는 원래대로 동작합니다.
핵심 트랩
ECMAScript 스펙에서 정의하는 Proxy 트랩은 13가지입니다. 가장 자주 쓰이는 트랩을 먼저 살펴보겠습니다.
get / set
속성 읽기와 쓰기 를 가로챕니다.
const handler = {
get(target, prop, receiver) {
console.log(`읽기: ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`쓰기: ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
},
};
const obj = new Proxy({ name: "Ray" }, handler);
obj.name; // 읽기: name → "Ray"
obj.age = 25; // 쓰기: age = 25has / deleteProperty
in 연산자와 delete 연산자를 가로챕니다.
const handler = {
has(target, prop) {
// _로 시작하는 속성은 숨김
if (prop.startsWith("_")) return false;
return Reflect.has(target, prop);
},
deleteProperty(target, prop) {
if (prop.startsWith("_")) {
throw new Error("비공개 속성은 삭제할 수 없습니다");
}
return Reflect.deleteProperty(target, prop);
},
};
const obj = new Proxy({ _secret: 42, name: "Ray" }, handler);
"name" in obj; // true
"_secret" in obj; // false (숨겨짐)apply / construct
함수 호출과 new 연산자를 가로챕니다. target이 함수 일 때만 사용할 수 있습니다.
function sum(a, b) {
return a + b;
}
const proxiedSum = new Proxy(sum, {
apply(target, thisArg, args) {
console.log(`호출: sum(${args.join(", ")})`);
return Reflect.apply(target, thisArg, args);
},
});
proxiedSum(1, 2); // 호출: sum(1, 2) → 3전체 트랩 목록
| 트랩 | 가로채는 작업 |
|---|---|
get | 속성 읽기 |
set | 속성 쓰기 |
has | in 연산자 |
deleteProperty | delete 연산자 |
apply | 함수 호출 |
construct | new 연산자 |
getPrototypeOf | Object.getPrototypeOf() |
setPrototypeOf | Object.setPrototypeOf() |
isExtensible | Object.isExtensible() |
preventExtensions | Object.preventExtensions() |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() |
ownKeys | Object.keys(), for...in 등 |
Reflect
Reflect는 Proxy 트랩과 1:1로 대응하는 13개의 정적 메서드를 가진 내장 객체입니다. 각 메서드는 해당 작업의 기본 동작 을 수행합니다.
// Reflect 없이, 기본 동작을 직접 구현
const handler = {
get(target, prop) {
return target[prop]; // 단순하지만 receiver를 무시
},
};
// Reflect 사용, 기본 동작을 정확하게 위임
const handler2 = {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver); // receiver 전달
},
};Reflect를 사용하는 이유:
- 정확한 기본 동작 ,
target[prop]대신Reflect.get(target, prop, receiver)를 사용하면 getter의this가 올바르게 설정됩니다 - 일관된 반환값 ,
Reflect.set()은 성공 여부를 boolean으로 반환합니다. strict mode에서 set 트랩은 반드시 boolean을 반환해야 합니다 - 명확한 의도 , "기본 동작을 수행한다"는 의도가 코드에 드러납니다
실용적 활용
유효성 검사
Proxy의 가장 대표적인 활용입니다. 객체에 투명한 검증 계층 을 추가합니다.
function createValidated(target, validators) {
return new Proxy(target, {
set(target, prop, value) {
const validate = validators[prop];
if (validate && !validate(value)) {
throw new TypeError(
`"${prop}"에 대한 유효성 검사 실패: ${value}`
);
}
return Reflect.set(target, prop, value);
},
});
}
const person = createValidated(
{ age: 25, name: "Ray" },
{
age: (v) => typeof v === "number" && v >= 0,
name: (v) => typeof v === "string" && v.length > 0,
}
);
person.age = 30; // OK
person.age = -1; // TypeError: "age"에 대한 유효성 검사 실패: -1
person.name = ""; // TypeError: "name"에 대한 유효성 검사 실패:로깅 / 디버깅
모든 속성 접근을 자동으로 기록합니다.
function withLogging(target) {
return new Proxy(target, {
get(target, prop, receiver) {
console.log(`[GET] ${String(prop)}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`[SET] ${String(prop)} = ${value}`);
return Reflect.set(target, prop, value, receiver);
},
});
}반응형 시스템 (Vue 3)
Vue 3의 반응형 시스템은 Proxy를 기반으로 합니다. reactive()가 반환하는 객체는 Proxy입니다.
// Vue 3 반응형의 핵심 원리 (간략화)
function reactive(target) {
return new Proxy(target, {
get(target, prop, receiver) {
track(target, prop); // 의존성 추적
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
trigger(target, prop); // 변경 알림
return result;
},
});
}get 트랩에서 "이 속성을 누가 읽었는지" 기록하고, set 트랩에서 "이 속성이 바뀌었으니 다시 렌더링하라" 알립니다. Vue 2에서 사용하던 Object.defineProperty는 새 속성 추가를 감지하지 못했지만, Proxy는 모든 작업을 가로채므로 이 한계가 해결됩니다.
MobX도 observable() 함수에서 Proxy를 사용하여 객체의 속성 변경을 자동으로 감지하고 관련 reaction을 실행합니다. 이처럼 Proxy 기반 반응형 시스템은 현대 프론트엔드 상태 관리의 핵심 패턴이 되었습니다.
Revocable Proxy
Proxy.revocable()은 취소 가능한 Proxy 를 생성합니다. revoke()를 호출하면 Proxy가 비활성화되어 이후 모든 작업이 TypeError를 발생시킵니다.
const { proxy, revoke } = Proxy.revocable(
{ data: "민감한 정보" },
{
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
}
);
proxy.data; // "민감한 정보"
revoke(); // Proxy 비활성화
proxy.data; // TypeError: Cannot perform 'get' on a proxy
// that has been revoked임시 접근 권한을 부여하거나, 특정 시점 이후 객체 접근을 차단할 때 유용합니다.
성능 고려사항
Proxy는 강력하지만, 모든 작업에 트랩 함수 호출이 추가 됩니다. 고려할 점:
- 핫 패스 주의 , 초당 수백만 번 접근하는 속성에 Proxy를 쓰면 성능에 영향이 있습니다
- 트랩 최소화 , 필요한 트랩만 정의하세요. 정의하지 않은 트랩은 target으로 직접 전달됩니다
- 중첩 Proxy 주의 , Proxy 위에 Proxy를 겹치면 트랩 호출이 누적됩니다
- V8 최적화 제한 , Proxy 객체는 hidden class 기반의 인라인 캐싱 최적화를 받기 어렵습니다
일반적인 애플리케이션 코드에서는 성능 차이가 체감되지 않지만, 라이브러리 내부의 핫 패스에서는 벤치마크를 통해 확인하는 것이 좋습니다.
정리
| 개념 | 역할 |
|---|---|
| Proxy | 객체 위에 가상 레이어를 씌워 작업을 가로챔 |
| Handler | 트랩 메서드를 정의하는 객체 |
| Trap | 특정 작업을 가로채는 handler의 메서드 (13종) |
| Reflect | 각 트랩의 기본 동작을 수행하는 정적 메서드 (13종) |
| Revocable | 취소 가능한 Proxy. revoke()로 비활성화 |
다음 단계
지금까지 JavaScript의 고급 기능들을 살펴봤습니다. 다음 마지막 글에서는 실무에서 자주 혼동하는 패턴들 을 정리하고, 시리즈를 마무리합니다.