Proxy와 Reflect
Proxy 트랩으로 객체 조회·할당·호출을 가로채고 Reflect의 기본 연산에 위임해 메타 프로그래밍을 구현합니다.
우리는 8장에서 let, const, 화살표 함수, 구조 분해 할당, 스프레드 연산자,
그리고 Symbol, Map, Set까지 최신 핵심 기능을 학습했습니다.
이번에는 더 추상적이지만 실무적으로 중요한 주제인 Proxy와 Reflect를 다룹니다.
Proxy는 객체의 기본 동작(프로퍼티 접근, 할당, 함수 호출 등)을
가로채서(intercept) 원하는 방식으로 제어하게 해줍니다.
Reflect는 이런 동작을 표준 메서드 형태로 제공해
Proxy와 함께 쓸 때 안정적인 구현을 돕습니다.
이 장에서는 Proxy의 트랩(trap) 동작과 Reflect의 역할을 연결해
자바스크립트 메타 프로그래밍(Meta-programming)의 기반을 정리하겠습니다.
Proxy는 객체 앞의 관문이고 Reflect는 관문 안에서 원래 객체 연산을 표준 방식으로 이어 주는 도구다.
| 구간 | 역할 | 좋은 사용 | 깨지기 쉬운 지점 |
|---|---|---|---|
| target | 원본 객체와 prototype 규칙 보관 | 실제 데이터는 target에 둠 | proxy와 target을 섞어 전달 |
| handler | 필요한 trap만 정의 | get/set/has/delete 감시 | 모든 연산을 무리하게 가로챔 |
| Reflect | 원래 내부 연산에 위임 | receiver, boolean 반환 규칙 보존 | trap 안에서 proxy를 다시 호출 |
| invariant | 언어 불변식 우선 | non-configurable 규칙 확인 | 숨길 수 없는 속성을 숨기려 함 |
Proxy: 객체의 동작 가로채기
Proxy 객체는 다른 객체(타겟 객체)의 프록시(대리인) 역할을 합니다.
Proxy를 사용하면 타겟 객체에 대한 모든 연산(프로퍼티 읽기/쓰기, 함수 호출 등)을 가로채어 원하는 로직을 추가할 수 있습니다.
Proxy 생성하기
new Proxy(target, handler) 생성자를 사용하여 Proxy 객체를 생성합니다.
target: 프록시할 대상 객체 (일반 객체, 배열, 함수 등).handler: 객체의 동작을 가로챌 트랩(trap) 메서드들을 정의하는 객체.
const targetObject = {
message1: '안녕하세요',
message2: '반갑습니다'
};
const handler = {
// get 트랩: 프로퍼티를 읽으려 할 때 호출됨
get: function(target, property, receiver) {
console.log(`[Proxy] '${String(property)}' 프로퍼티를 읽으려 합니다.`);
// 원래의 프로퍼티 값을 반환
return target[property];
},
// set 트랩: 프로퍼티에 값을 할당하려 할 때 호출됨
set: function(target, property, value, receiver) {
console.log(`[Proxy] '${String(property)}' 프로퍼티에 '${value}'를 쓰려 합니다.`);
// 원래의 할당 동작 수행
target[property] = value;
return true; // 성공적으로 할당되었음을 나타냄
}
};
const proxyObject = new Proxy(targetObject, handler);
// proxyObject를 통해 프로퍼티에 접근하거나 값을 할당하면 handler의 트랩이 동작합니다.
console.log(proxyObject.message1); // get 트랩 호출 후 "안녕하세요" 출력
proxyObject.message1 = '안녕'; // set 트랩 호출
console.log(proxyObject.message1); // get 트랩 호출 후 "안녕" 출력
console.log(proxyObject.message2); // get 트랩 호출 후 "반갑습니다" 출력주요 트랩(Traps) 종류
handler 객체에는 다양한 종류의 트랩 메서드를 정의할 수 있습니다.
각 트랩은 특정 객체 연산을 가로챕니다.
| 트랩 메서드 | 설명 | 가로채는 연산 |
|---|---|---|
get(target, property, receiver) | 프로퍼티 읽기 | proxy.foo, proxy['bar'] |
set(target, property, value, receiver) | 프로퍼티 쓰기 | proxy.foo = 1, proxy['bar'] = 2 |
has(target, property) | in 연산자 | 'foo' in proxy |
deleteProperty(target, property) | delete 연산자 | delete proxy.foo |
apply(target, thisArg, argumentsList) | 함수 호출 (Proxy 대상이 함수일 때) | proxy(...args) |
construct(target, argumentsList, newTarget) | new 연산자 (생성자로 호출할 때, Proxy 대상이 생성자 함수일 때) | new proxy(...args) |
ownKeys(target) | Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() 등 | Object.keys(proxy) |
getPrototypeOf(target) | Object.getPrototypeOf(), __proto__ 등 | Object.getPrototypeOf(proxy) |
setPrototypeOf(target, prototype) | Object.setPrototypeOf() | Object.setPrototypeOf(proxy, proto) |
isExtensible(target) | Object.isExtensible() | Object.isExtensible(proxy) |
preventExtensions(target) | Object.preventExtensions() | Object.preventExtensions(proxy) |
defineProperty(target, property, descriptor) | Object.defineProperty(), Object.defineProperties() | Object.defineProperty(proxy, 'foo', descriptor) |
getOwnPropertyDescriptor(target, property) | Object.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor(proxy, 'foo') |
const userProfile = {
name: "Unknown",
age: 0
};
const userValidator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (typeof value !== 'number' || value < 0 || value > 150) {
console.error("나이는 0에서 150 사이의 숫자여야 합니다.");
return false; // 할당 실패
}
} else if (prop === 'name') {
if (typeof value !== 'string' || value.length < 2) {
console.error("이름은 최소 2글자 이상이어야 합니다.");
return false; // 할당 실패
}
}
obj[prop] = value; // 유효성 검사 통과 시 실제 할당
return true;
}
};
const proxyUser = new Proxy(userProfile, userValidator);
proxyUser.name = "John Doe"; // 유효성 통과
proxyUser.age = 30; // 유효성 통과
console.log(proxyUser); // 결과: { name: 'John Doe', age: 30 }
proxyUser.age = -5; // 결과: 나이는 0에서 150 사이의 숫자여야 합니다.
proxyUser.name = "A"; // 결과: 이름은 최소 2글자 이상이어야 합니다.
console.log(proxyUser); // 여전히 { name: 'John Doe', age: 30 } (변경되지 않음)이처럼 Proxy를 사용하면 객체의 내부 구현을 건드리지 않고도 외부에서 동작을 제어할 수 있습니다.
이는 Vue 3의 반응성 시스템(Reactivity System)이나 MobX 같은 상태 관리 라이브러리에서 데이터 변경 감지에 활용됩니다.
Reflect: 객체 연산의 내장 헬퍼 객체
읽기, 쓰기, 존재 확인, 삭제는 각각 다른 trap으로 들어온다. Reflect를 함께 쓰면 가로챈 뒤 원래 의미를 보존하기 쉽다.
| 연산 | trap | Reflect 위임 | 대표 용도 |
|---|---|---|---|
| 읽기 | get | Reflect.get(target, key, receiver) | 기본값, 접근 로그, 반응성 추적 |
| 쓰기 | set | Reflect.set(target, key, value, receiver) | 값 검증, 권한 검사 |
| 존재 확인 | has | Reflect.has(target, key) | private key 숨김, 정책 적용 |
| 삭제 | deleteProperty | Reflect.deleteProperty(target, key) | 삭제 허용 범위 제한 |
Reflect는 ES2015(ES6)에서 도입된 내장 객체로, 객체에 대한 다양한 내부 연산들을 메서드 형태로 제공합니다.
Reflect는 Proxy와 밀접한 관련이 있으며, 다음과 같은 특징을 가집니다.
- 메서드 제공:
Object객체의 일부 메서드(Object.keys(),Object.defineProperty())와 유사하지만,Reflect는 대부분의 객체 내부 연산을Function.prototype.apply()나new연산자처럼 '메서드 호출' 형태로 제공합니다. - Proxy 핸들러와의 시너지:
Proxy트랩 내에서Reflect메서드를 사용하면, 원래의 기본 객체 연산을 깔끔하게 호출할 수 있습니다. 이는target[property]와 같은 직접적인 접근보다 권장됩니다. - 불리언 반환: 많은
Reflect메서드는 성공 여부를 불리언 값으로 반환하여, 연산의 성공/실패 여부를 쉽게 판단할 수 있도록 합니다. (예:Reflect.defineProperty,Reflect.deleteProperty)
Reflect의 주요 메서드
Reflect의 메서드들은 대부분 Proxy 핸들러 트랩과 1:1로 매칭됩니다.
Reflect.get(target, property, receiver)Reflect.set(target, property, value, receiver)Reflect.has(target, property)Reflect.deleteProperty(target, property)Reflect.apply(target, thisArgument, argumentsList)Reflect.construct(target, argumentsList, newTarget)- ... 그 외
Object객체와 유사한 메서드들
Proxy 핸들러 내에서 Reflect를 사용하면, 기본 동작을 호출하면서도 this 바인딩(receiver 인자)과 같은 복잡한 내부 로직을 정확하게 처리할 수 있습니다.
const user = {
firstName: "Jane",
lastName: "Doe",
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
};
const loggingHandler = {
get(target, prop, receiver) {
console.log(`[Reflect] '${String(prop)}' 프로퍼티에 접근했습니다.`);
// Reflect.get을 사용하여 원래의 getter를 정확하게 호출
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`[Reflect] '${String(prop)}' 프로퍼티를 '${value}'로 설정했습니다.`);
// Reflect.set을 사용하여 원래의 setter를 정확하게 호출
return Reflect.set(target, prop, value, receiver);
}
};
const proxiedUser = new Proxy(user, loggingHandler);
console.log(proxiedUser.firstName); // get 트랩, Reflect.get 호출
proxiedUser.lastName = "Smith"; // set 트랩, Reflect.set 호출
console.log(proxiedUser.fullName); // get 트랩 (fullName getter가 this.firstName/lastName을 참조하므로 다시 get 트랩 호출)위 예시에서 Reflect.get(target, prop, receiver)는 단순히 target[prop]을 하는 것보다 receiver 인자를 통해 fullName과 같은 getter의 this 컨텍스트를 프록시 객체 자신으로 정확하게 유지시켜줍니다.
이는 Proxy의 동작을 훨씬 견고하게 만듭니다.
Reflect의 다른 유용한 메서드
Reflect는 Object 객체에서 에러를 던지던 일부 연산들을 불리언 값을 반환하도록 하여 더욱 유연한 에러 핸들링을 가능하게 합니다.
const obj = {};
// Object.defineProperty는 실패 시 에러를 던짐
try {
Object.defineProperty(obj, 'prop', { value: 1, writable: false });
Object.defineProperty(obj, 'prop', { value: 2 }); // TypeError: Cannot redefine property
} catch (e) {
console.error(e.message);
}
// Reflect.defineProperty는 실패 시 false를 반환
const success1 = Reflect.defineProperty(obj, 'anotherProp', { value: 10 });
console.log(success1); // 결과: true
const success2 = Reflect.defineProperty(obj, 'prop', { value: 2 }); // 실패
console.log(success2); // 결과: false (에러를 던지지 않음)이러한 특성은 특히 복잡한 객체 조작 로직이나 라이브러리 구현 시 유용합니다.
Proxy 트랩을 직접 작성할 때는 "무엇을 가로챌지"만큼 "기본 객체 규칙을 어떻게 보존할지"가 중요합니다.
아래 다이어그램은 무한 재귀, 잘못된 반환값, receiver 누락처럼 실수하기 쉬운 지점을 구현 전 체크리스트로 정리합니다.
Proxy 트랩은 자유도가 높지만, 객체의 불변 규칙과 반환 규칙을 어기면 런타임 오류나 예측하기 어려운 동작으로 이어집니다.
- 재귀 방지프록시를 다시 읽지 않기
트랩 내부에서 proxy[prop]를 호출하면 같은 트랩에 다시 들어갑니다. target과 Reflect에 위임합니다.
- 반환 규칙성공 여부를 맞추기
set, deleteProperty, defineProperty는 성공 여부를 boolean으로 돌려줘야 호출부가 올바르게 판단합니다.
- receiver 유지getter의 this 보존
Reflect.get(target, prop, receiver)를 쓰면 상속과 getter가 바라보는 this를 안전하게 유지합니다.
- 불변 조건객체 규칙을 속이지 않기
동결된 객체나 non-configurable 속성은 실제 상태와 다른 결과를 반환할 수 없습니다.
브라우저 화면 상태를 다룰 때는 Proxy로 변경을 감지하고 Reflect로 기본 동작을 보존한 뒤 렌더링을 예약하는 흐름으로 이해할 수 있습니다.
상태 객체 앞에 Proxy를 두면 set 트랩에서 기본 할당, 변경 비교, 렌더 예약을 한 경계에서 처리할 수 있다.
| 지점 | 역할 | 주의 | 검증 |
|---|---|---|---|
| Action 상태 쓰기 | 클릭, 입력, 비동기 응답이 상태 프로퍼티 변경을 만든다. | DOM을 직접 바꾸면 상태와 화면의 기준이 갈라진다. | 모든 변경이 상태 쓰기를 통해 시작되는지 추적한다. |
| set trap 감지 경계 | 어떤 key가 어떤 값으로 바뀌는지 한곳에서 본다. | 중첩 객체는 별도 Proxy가 없으면 내부 변경을 놓친다. | 변경 로그가 key, old value, new value를 남긴다. |
| Reflect.set 기본 동작 | 원래 객체의 할당 규칙과 반환값을 보존한다. | 반환값을 무시하면 실패한 할당도 성공처럼 보일 수 있다. | 성공한 할당에서만 이후 렌더 예약이 실행된다. |
| Render queue 화면 갱신 | 여러 변경을 묶어 다음 프레임 DOM 갱신으로 보낸다. | 매 변경마다 즉시 렌더하면 입력 중 화면이 흔들린다. | 같은 프레임의 중복 변경이 한 번의 갱신으로 합쳐진다. |
| DevTools 원인 추적 | 상태 변경 로그와 브레이크포인트로 화면 변경 원인을 찾는다. | 렌더 결과만 보면 어떤 상태가 원인인지 늦게 찾는다. | 문제 화면에서 마지막 상태 변경자를 확인할 수 있다. |
Proxy와 Reflect 통합 정리
이번 장에서는 ES2015(ES6)에서 도입된 객체 동작의 메타 프로그래밍 기능인 Proxy와 Reflect를 학습했습니다.
검증, 로깅, 반응성은 원본 객체 의미를 대체하는 기능이 아니라 접근 순간에 붙는 부가 규칙이다.
| 단계 | 무슨 일이 일어나는가 | 확인할 것 | 실패 신호 |
|---|---|---|---|
| 1 접근 | proxy.name 또는 proxy.name = value | 외부에 proxy가 전달됐는가 | target을 직접 만짐 |
| 2 trap | handler가 연산을 받음 | 필요한 연산만 잡는가 | 숨은 동작이 너무 많음 |
| 3 Reflect | 기본 객체 동작으로 위임 | 반환값 규칙이 맞는가 | strict mode에서 TypeError |
| 4 effect | 로그, 검증, 화면 갱신 연결 | 부작용 위치가 보이는가 | 디버깅 경로가 불명확 |
Proxy는 target 객체의 프로퍼티 읽기/쓰기, 함수 호출 같은 연산을 handler로 가로챌 수 있습니다.
이를 이용하면 유효성 검사, 로깅, 접근 제어를 객체의 핵심 구현과 분리해 둘 수 있습니다.
또한, Reflect 객체가 객체 내부 연산을 위한 메서드들을 제공하며, 특히 Proxy 핸들러 내에서 target[prop] 대신 Reflect 메서드를 사용함으로써 보다 정확하고 견고한 동작을 보장한다는 것을 배웠습니다.
Reflect는 연산 성공 여부를 불리언으로 반환하여 에러 핸들링을 유연하게 할 수 있도록 돕습니다.
Proxy와 Reflect는 객체 연산을 가로채거나 표준 내부 연산에 맞춰 위임할 때 사용합니다.
Vue.js 3 같은 프레임워크의 반응성 구조를 읽을 때도 이 개념이 기준이 됩니다.
Proxy는 강력하지만 디버깅 난도와 성능 비용을 만든다. 목적이 검증·로깅·반응성처럼 명확할 때만 선택한다.
| 질문 | A에 가까운 답 | 위험한 답 | 대안 |
|---|---|---|---|
| 가로챌 연산이 분명한가 | get/set 등 특정 trap만 필요 | 모든 접근을 막연히 감시 | 명시적 setter |
| 기본 동작을 유지하나 | Reflect로 위임 | 직접 흉내 내며 규칙 누락 | 일반 함수 래퍼 |
| 오류가 예측 가능한가 | invariant와 strict 실패 처리 | 실패가 조용히 무시됨 | 스키마 검증 |
| 디버깅 이득이 큰가 | 정책이 한곳에 모임 | 호출자가 동작을 추적 못함 | 명시적 API |
마지막 다이어그램은 Proxy 생성, 주요 트랩, Reflect 위임, receiver와 반환값 주의점을 기준으로 객체 연산을 안전하게 가로채는 방법을 정리합니다.
target은 데이터, handler는 접근 정책, Reflect는 원래 의미를 이어 주는 표준 호출이다. 세 역할이 섞이면 코드가 불투명해진다.
| 역할 | 책임 | 좋은 신호 | 나쁜 신호 |
|---|---|---|---|
| target | 원본 상태 보관 | 직접 노출하지 않음 | 일부 코드는 target, 일부는 proxy 사용 |
| handler | 읽기·쓰기 정책 정의 | trap이 짧고 목적이 명확 | trap이 비즈니스 로직을 모두 품음 |
| Reflect | 기본 동작 호출 | prototype/receiver 의미 유지 | 수동 구현으로 edge case 누락 |
| caller | proxy를 객체처럼 사용 | 실패가 명확히 보임 | 가로채기를 모른 채 의존 |
Proxy가 연산을 멈춰 세우고, Reflect가 같은 연산의 기본 경로를 호출한다. 둘을 함께 보면 메타프로그래밍의 경계가 선명하다.
| 객체 동작 | Proxy가 하는 일 | Reflect가 하는 일 | 주의점 |
|---|---|---|---|
| property read | get trap으로 관찰 | Reflect.get으로 기본 읽기 | receiver this 바인딩 |
| property write | set trap으로 검증 | Reflect.set으로 기본 쓰기 | boolean 반환 |
| membership | has trap으로 제한 | Reflect.has로 in 동작 유지 | 숨겨도 불변식은 유지 |
| delete | deleteProperty로 통제 | Reflect.deleteProperty 위임 | 삭제 실패 처리 |