JavaScript Proxy 객체의 힘: 프로처럼 연산을 가로채기
Proxy는 ES6에 들어온 메타프로그래밍 도구입니다. 객체에서 일어나는 기본 연산, 즉 속성 읽기와 쓰기, in 검사, 함수 호출, new를 실제로 일어나기 전에 가로채서 원하는 동작을 끼워 넣습니다. 객체를 감싸는 보호막이자 확장 레이어입니다.
Proxy란
Proxy 객체는 인수 두 개로 만듭니다.
target: 프록시로 감쌀 객체. 함수, 배열, 다른 프록시도 됩니다.handler: 트랩(trap) 메서드를 담은 객체. 어떤 연산을 가로챌지 여기서 정합니다.
const target = {}; // 원본 객체
const handler = {}; // 트랩 메서드를 가진 객체
const proxy = new Proxy(target, handler);
handler가 비어 있으면 proxy는 target과 똑같이 동작합니다. 트랩을 하나씩 채울 때마다 그 연산만 가로채집니다.
주요 트랩 이해하기
get: 속성 읽기 가로채기
const person = {
name: "Alice",
age: 30
};
const personProxy = new Proxy(person, {
get(target, property) {
console.log(`속성 ${property}에 접근 중...`);
return target[property];
}
});
console.log(personProxy.name);
// 출력: 속성 name에 접근 중...
// 출력: Alice
set: 속성 할당 가로채기
const validatedUser = new Proxy({}, {
set(target, property, value) {
if (property === 'age' && typeof value !== 'number') {
throw new TypeError('나이는 숫자여야 합니다!');
}
if (property === 'age' && value < 0) {
throw new RangeError('나이는 음수일 수 없습니다!');
}
target[property] = value;
return true; // 할당 성공을 나타냄
}
});
validatedUser.name = "Bob"; // 성공
validatedUser.age = 25; // 성공
validatedUser.age = -5; // RangeError
has: in 연산자 가로채기
const hiddenProps = new Proxy({ visible: true, _secret: 'hidden' }, {
has(target, property) {
if (property.startsWith('_')) {
return false; // 밑줄로 시작하는 속성은 숨긴다
}
return property in target;
}
});
console.log('visible' in hiddenProps); // true
console.log('_secret' in hiddenProps); // false (실제로는 존재한다)
실용적인 사용 사례
데이터 보안과 접근 제어
const secureData = new Proxy({
publicInfo: '모든 사용자가 접근 가능',
confidential: '기밀 정보'
}, {
get(target, property) {
if (property === 'confidential') {
throw new Error('인증되지 않은 접근입니다!');
}
return target[property];
}
});
console.log(secureData.publicInfo); // 접근 허용
console.log(secureData.confidential); // Error
기본값 제공
const defaultValues = {
name: "익명",
age: 0,
country: "미국"
};
const userWithDefaults = new Proxy({}, {
get(target, property) {
return property in target ? target[property] : defaultValues[property];
}
});
userWithDefaults.name = "Charlie";
console.log(userWithDefaults.name); // Charlie
console.log(userWithDefaults.age); // 0 (기본값)
console.log(userWithDefaults.country); // 미국 (기본값)
메모이제이션
계산 결과를 속성으로 저장해 두면, 두 번째 접근부터는 get 트랩이 캐시를 돌려줍니다.
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = new Proxy({}, {
get(target, property) {
const n = Number(property);
if (!(property in target)) {
console.log(`피보나치(${n}) 계산 중...`);
target[property] = fibonacci(n);
} else {
console.log(`피보나치(${n}) 캐시에서 반환`);
}
return target[property];
}
});
console.log(memoizedFib[10]); // 계산된다
console.log(memoizedFib[10]); // 캐시에서 반환
디버깅과 로깅
function createLogger(obj, name) {
return new Proxy(obj, {
get(target, property) {
console.log(`[${name}] 속성 '${property}' 읽기: ${target[property]}`);
return target[property];
},
set(target, property, value) {
console.log(`[${name}] 속성 '${property}' 설정: ${value}`);
target[property] = value;
return true;
}
});
}
const loggedUser = createLogger({ name: "Dave" }, "User");
loggedUser.age = 28; // [User] 속성 'age' 설정: 28
console.log(loggedUser.name); // [User] 속성 'name' 읽기: Dave
고급 기능
revocable: 철회할 수 있는 프록시
외부에 객체를 잠깐 빌려주고 나중에 끊고 싶을 때 씁니다. revoke()를 부른 뒤에는 어떤 연산도 통하지 않습니다.
const target = { message: "안녕하세요!" };
const { proxy, revoke } = Proxy.revocable(target, {
get(target, property) {
return `가로채짐: ${target[property]}`;
}
});
console.log(proxy.message); // "가로채짐: 안녕하세요!"
revoke(); // 프록시 비활성화
console.log(proxy.message); // TypeError: 철회된 프록시에서 'get' 수행 불가
Reflect와 함께 쓰기
트랩 안에서 원래 동작을 그대로 수행하고 싶을 때는 target[property] 대신 Reflect를 씁니다. 트랩 이름과 인수가 1:1로 대응합니다.
const smartProxy = new Proxy(target, {
get(target, property, receiver) {
console.log(`${property} 속성에 접근`);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`${property} 속성을 ${value}로 설정`);
return Reflect.set(target, property, value, receiver);
}
});
주의사항과 제한사항
- 성능. 연산마다 트랩이 한 겹 더 실행됩니다. 대부분은 체감이 안 되지만, 핫 루프 안의 객체를 감싸는 것은 신중해야 합니다.
- 브라우저 호환성. IE는 지원하지 않고 폴리필도 불가능합니다. 최신 브라우저와 Node.js에서는 완전히 지원됩니다.
- 투명성의 한계.
===비교는 가로챌 수 없습니다. 프록시와 원본은 서로 다른 객체입니다.Map,Date같은 내장 객체의 내부 슬롯도 프록시를 통과하지 못해 메서드 호출이 깨질 수 있습니다.
언제 Proxy를 써야 할까
쓰면 좋은 경우
- 객체 상호작용을 세밀하게 제어해야 할 때
- 로깅과 디버깅
- 유효성 검사 규칙 적용, 불변성 강제
- 동적 데이터 구조(예: 음수 인덱스가 되는 배열)
- 함수 동작의 확장과 래핑
피해야 하는 경우
- 평범한 객체 조작으로 충분할 때
- 성능이 매우 중요한 경로
- 코드 복잡도만 올리는 경우
결론
Proxy는 객체의 동작을 바꿔 끼울 수 있는 다재다능한 도구입니다. 로깅과 유효성 검사부터 동적 속성 생성과 특수한 경우 처리까지, 객체 뒤에 한 겹을 두는 것만으로 많은 일이 됩니다. 트랩의 종류와 쓰임을 알아 두면 더 정교하고 통제 가능한 코드를 쓸 수 있습니다.
다만 그 한 겹은 복잡도이기도 합니다. 프레임워크나 라이브러리를 만들 때, 또는 객체 동작에 대한 세밀한 제어가 정말 필요할 때 꺼내 쓰고, 평소에는 평범한 객체로 충분합니다.