Proxy는 ES6(ES2015)에 추가된 JavaScript 내장 객체로, 대상 객체(target)의 기본 동작을 가로채서 커스텀 동작을 정의할 수 있게 해줍니다.
Vue3의 반응성 시스템이 Proxy를 기반으로 동작하며, 이를 통해 자동 의존성 추적과 효율적인 UI 업데이트를 구현합니다.
// 기본 문법
const proxy = new Proxy(target, handler);
const target = { name: 'Kim', age: 25 };
const handler = {
get(target, property) {
console.log(`${property} 속성을 읽었습니다`);
return target[property];
},
set(target, property, value) {
console.log(`${property} 속성을 ${value}로 변경합니다`);
target[property] = value;
return true; // set은 반드시 true/false 반환
}
};
const proxy = new Proxy(target, handler);
// Proxy를 통해 접근
[proxy.name](http://proxy.name); // "name 속성을 읽었습니다" → "Kim"
proxy.age = 30; // "age 속성을 30로 변경합니다"
// 원본 객체는 변경됨
console.log(target.age); // 30
Proxy는 "구독"이 아니라 "가로채기(intercept)" 방식으로 동작합니다. 마치 Express 미들웨어나 네트워크 프록시 서버처럼, 모든 요청이 중간에서 가로채집니다.
사용자 코드
↓
proxy.count // 접근 시도
↓
[Proxy Handler] // ← 미들웨어!
↓
get 트랩 실행
- 로깅
- 검증
- 추적 등...
↓
target.count // 실제 객체 접근
↓
값 반환 ← ← ← ← ← 역순으로 되돌아감
// ❌ 구독 패턴: 이벤트를 수동으로 발생시켜야 함
class EventEmitter {
constructor() {
this.listeners = [];
}
subscribe(listener) {
this.listeners.push(listener);
}
emit(event) {
this.listeners.forEach(listener => listener(event));
}
}
const emitter = new EventEmitter();
emitter.subscribe(data => console.log(data));
emitter.emit('change'); // 수동으로 이벤트 발생
// ✅ Proxy: 접근이 자동으로 가로채짐
const proxy = new Proxy({}, {
set(target, property, value) {
console.log('자동으로 감지!'); // 자동 실행
target[property] = value;
return true;
}
});
[proxy.name](http://proxy.name) = 'Kim'; // set 트랩 자동 실행
Proxy는 13가지 트랩을 제공합니다. 주요 트랩들:
const handler = {
// 속성 읽기
get(target, property, receiver) {
return target[property];
},
// 속성 쓰기
set(target, property, value, receiver) {
target[property] = value;
return true;
},
// 속성 존재 확인 (in 연산자)
has(target, property) {
return property in target;
},
// 속성 삭제 (delete 연산자)
deleteProperty(target, property) {
delete target[property];
return true;
},
// 함수 호출
apply(target, thisArg, argumentsList) {
return target.apply(thisArg, argumentsList);
},
// new 연산자
construct(target, argumentsList, newTarget) {
return new target(...argumentsList);
},
// Object.keys(), for...in 등
ownKeys(target) {
return Object.keys(target);
},
// Object.getOwnPropertyDescriptor()
getOwnPropertyDescriptor(target, property) {
return Object.getOwnPropertyDescriptor(target, property);
}
};
function reactive(target) {
const handler = {
get(target, property, receiver) {
// 1. 의존성 추적
track(target, property);
// 2. 실제 값 반환
const value = Reflect.get(target, property, receiver);
// 3. 중첩된 객체도 reactive로 변환
if (typeof value === 'object' && value !== null) {
return reactive(value);
}
return value;
},
set(target, property, value, receiver) {
const oldValue = target[property];
const result = Reflect.set(target, property, value, receiver);
// 값이 실제로 변경되었다면 UI 업데이트
if (oldValue !== value) {
trigger(target, property);
}
return result;
}
};
return new Proxy(target, handler);
}
let activeEffect = null;
const targetMap = new WeakMap();
// 의존성 추적
function track(target, property) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(property);
if (!dep) {
depsMap.set(property, (dep = new Set()));
}
dep.add(activeEffect);
}
// 변경 알림
function trigger(target, property) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(property);
if (!dep) return;
dep.forEach(effect => effect());
}
Vue는 Proxy(미들웨어)와 track/trigger(구독 패턴)을 함께 사용합니다:
// 전체 동작 흐름
const state = reactive({ count: 0 });
// 1단계: 컴포넌트 렌더링
watchEffect(() => {
console.log(state.count); // ← get 트랩 발동
});
/*
실행 과정:
1. state.count 접근
2. Proxy의 get 트랩 실행 (미들웨어)
3. track() 호출 → effect를 구독자로 등록 (구독 패턴)
4. 값 반환
*/
// 2단계: 값 변경
state.count = 5;
/*
실행 과정:
1. state.count 변경 시도
2. Proxy의 set 트랩 실행 (미들웨어)
3. 실제 값 변경
4. trigger() 호출 → 구독자들에게 알림 발송 (구독 패턴)
*/
핵심 차이:
이 조합 덕분에 Vue는 자동으로 의존성을 추적하고 UI를 업데이트할 수 있습니다!
function createValidator(schema) {
return new Proxy({}, {
set(target, property, value) {
const validator = schema[property];
if (validator && !validator(value)) {
throw new Error(`Invalid value for ${property}`);
}
target[property] = value;
return true;
}
});
}
const userSchema = {
age: (value) => typeof value === 'number' && value > 0,
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
};
const user = createValidator(userSchema);
user.age = 25; // ✅ OK
user.age = -5; // ❌ Error!
function readonly(target) {
return new Proxy(target, {
set() {
console.warn('This object is readonly!');
return false;
},
deleteProperty() {
console.warn('Cannot delete property!');
return false;
}
});
}
const config = readonly({ apiUrl: 'https://api.example.com' });
config.apiUrl = 'other'; // 경고, 변경 안 됨
function withDefaults(target, defaults) {
return new Proxy(target, {
get(target, property) {
return property in target ? target[property] : defaults[property];
}
});
}
const settings = withDefaults(
{ theme: 'dark' },
{ theme: 'light', lang: 'en', fontSize: 14 }
);
console.log(settings.theme); // 'dark'
console.log(settings.lang); // 'en'
function negativeArray(array) {
return new Proxy(array, {
get(target, property) {
const index = Number(property);
if (Number.isInteger(index) && index < 0) {
return target[target.length + index];
}
return target[property];
}
});
}
const arr = negativeArray([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
Reflect는 Proxy의 트랩과 1:1 대응되는 메서드를 제공합니다.
const handler = {
// ❌ 문제: getter의 this가 target을 가리킴
get(target, property) {
return target[property];
},
// ✅ 해결: this가 proxy를 가리킴
get(target, property, receiver) {
return Reflect.get(target, property, receiver);
}
};
Reflect.get(target, property, receiver)
Reflect.set(target, property, value, receiver)
Reflect.has(target, property)
Reflect.deleteProperty(target, property)
Reflect.apply(func, thisArg, args)
// ❌ 에러
const proxy = new Proxy(5, {});
// ✅ Vue의 ref가 필요한 이유
function ref(value) {
return reactive({ value });
}
const obj = { x: 1 };
const proxy = new Proxy(obj, {
get(t, p) { return t[p]; }
});
// 수백만 번 반복 시 성능 차이 발생
for (let i = 0; i < 1000000; i++) {
obj.x; // 빠름
proxy.x; // 조금 느림
}
// Map, Set은 내부 슬롯 때문에 문제 발생
const map = new Map();
const proxiedMap = new Proxy(map, {});
proxiedMap.set('key', 'value'); // ❌ TypeError!
const obj = {};
const proxy1 = new Proxy(obj, {});
const proxy2 = new Proxy(obj, {});
console.log(proxy1 === proxy2); // false
console.log(proxy1 === obj); // false
function observe(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key];
Object.defineProperty(obj, key, {
get() { return value; },
set(newValue) { value = newValue; }
});
});
}
// ❌ 한계
const data = observe({ count: 0 });
data.newProp = 10; // 반응성 없음!
delete data.count; // 반응성 없음!
function reactive(obj) {
return new Proxy(obj, {
get(target, property) {
return target[property];
},
set(target, property, value) {
target[property] = value;
return true;
},
deleteProperty(target, property) {
delete target[property];
return true;
}
});
}
// ✅ 모든 동작이 반응형
const data = reactive({ count: 0 });
data.newProp = 10; // ✅ 반응성!
delete data.count; // ✅ 반응성!
Proxy는 Vue3 반응성 시스템의 핵심입니다:
$set, $delete 불필요React는 명시적으로 setState를 호출해야 하지만, Vue는 Proxy가 자동으로 모든 것을 추적합니다. 이것이 Vue가 "마법 같다"고 느껴지는 이유입니다!