ES2015 프록시 (Chrome 49 이상)는 JavaScript에 중재 API를 제공하므로 대상 객체의 모든 작업을 트랩하거나 가로채고 이 대상의 작동 방식을 수정할 수 있습니다.
프록시에는 다음을 비롯한 다양한 용도가 있습니다.
- 인터셉트
- 객체 가상화
- 리소스 관리
- 디버깅을 위한 프로파일링 또는 로깅
- 보안 및 액세스 제어
- 객체 사용 계약
Proxy API에는 지정된 타겟 객체와 핸들러 객체를 사용하는 프록시 생성자가 포함되어 있습니다.
var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);
프록시의 동작은 핸들러에 의해 제어되며, 핸들러는 여러 가지 유용한 방법으로 타겟 객체의 원래 동작을 수정할 수 있습니다. 핸들러에는 프록시에서 상응하는 작업이 실행될 때 호출되는 선택적 트랩 메서드 (예: .get()
, .set()
, .apply()
)가 포함되어 있습니다.
인터셉트
먼저 일반 객체를 가져와 Proxy API를 사용하여 가로채기 미들웨어를 추가해 보겠습니다. 생성자에 전달되는 첫 번째 매개변수는 타겟 (프록시되는 객체)이고 두 번째 매개변수는 핸들러 (프록시 자체)입니다. 여기에서 getter, setter 또는 기타 동작의 후크를 추가할 수 있습니다.
var target = {};
var superhero = new Proxy(target, {
get: function(target, name, receiver) {
console.log('get was called for:', name);
return target[name];
}
});
superhero.power = 'Flight';
console.log(superhero.power);
Chrome 49에서 위의 코드를 실행하면 다음과 같은 결과가 표시됩니다.
get was called for: power
"Flight"
실제로 볼 수 있듯이 프록시 객체에서 속성 가져오기 또는 속성 설정을 올바르게 실행하면 핸들러의 상응하는 트랩이 메타 수준으로 호출됩니다. 핸들러 작업에는 속성 읽기, 속성 할당, 함수 적용이 포함되며, 모두 상응하는 트랩으로 전달됩니다.
트랩 함수는 원하는 경우 작업을 임의로 구현할 수 있습니다 (예: 작업을 타겟 객체로 전달). 실제로 트랩이 지정되지 않으면 기본적으로 이렇게 됩니다. 예를 들어 다음은 바로 이 작업을 실행하는 노옵스 전달 프록시입니다.
var target = {};
var proxy = new Proxy(target, {});
// operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been forwarded
console.log(target.paul);
지금까지는 일반 객체의 프록시를 살펴봤지만 함수가 타겟인 함수 객체도 마찬가지로 쉽게 프록시할 수 있습니다. 이번에는 handler.apply()
트랩을 사용합니다.
// Proxying a function object
function sum(a, b) {
return a + b;
}
var handler = {
apply: function(target, thisArg, argumentsList) {
console.log(`Calculate sum: ${argumentsList}`);
return target.apply(thisArg, argumentsList);
}
};
var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3
프록시 식별
프록시의 ID는 JavaScript 등식 연산자 (==
및 ===
)를 사용하여 확인할 수 있습니다. 아시다시피 이러한 연산자는 두 객체에 적용될 때 객체 ID를 비교합니다. 다음 예는 이 동작을 보여줍니다. 두 개의 서로 다른 프록시를 비교하면 기본 타겟이 동일하더라도 false가 반환됩니다. 마찬가지로 대상 객체는 모든 프록시와 다릅니다.
// Continuing previous example
var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false
프록시를 배치해도 앱의 결과에 실제로 영향을 미치지 않도록 프록시를 프록시가 아닌 객체와 구분할 수 없는 것이 이상적입니다. 이것이 Proxy API에 객체가 프록시인지 확인하는 방법이 포함되어 있지 않고 객체의 모든 작업에 트랩을 제공하지 않는 이유 중 하나입니다.
사용 사례
앞서 언급한 바와 같이 프록시의 사용 사례는 다양합니다. 액세스 제어 및 프로파일링과 같은 위의 많은 기능은 동일한 주소 '공간'에 다른 객체를 래핑하는 프록시인 일반 래퍼에 속합니다. 가상화도 언급되었습니다. 가상 객체는 다른 객체가 동일한 주소 공간에 있지 않아도 다른 객체를 에뮬레이션하는 프록시입니다. 예를 들어 원격 객체 (다른 공간의 객체를 에뮬레이션함)와 투명한 future (아직 계산되지 않은 결과를 에뮬레이션함)가 있습니다.
프록시를 핸들러로 사용
프록시 핸들러의 매우 일반적인 사용 사례는 래핑된 객체에 작업을 실행하기 전에 유효성 검사 또는 액세스 제어 검사를 실행하는 것입니다. 확인이 성공한 경우에만 작업이 전달됩니다. 아래의 유효성 검사 예는 이를 보여줍니다.
var validator = {
set: function(obj, prop, value) {
if (prop === 'yearOfBirth') {
if (!Number.isInteger(value)) {
throw new TypeError('The yearOfBirth is not an integer');
}
if (value > 3000) {
throw new RangeError('The yearOfBirth seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
}
};
var person = new Proxy({}, validator);
person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception
이 패턴의 더 복잡한 예에서는 프록시 핸들러가 가로챌 수 있는 다양한 작업을 모두 고려할 수 있습니다. 각 트랩에서 액세스 확인 및 작업 전달 패턴을 복제해야 하는 구현을 상상할 수 있습니다.
각 작업을 다르게 전달해야 할 수 있으므로 쉽게 추상화하기가 까다로울 수 있습니다. 이상적인 시나리오에서는 모든 작업을 단 하나의 트랩을 통해 균일하게 전달할 수 있다면 핸들러는 단일 트랩에서 유효성 검사를 한 번만 실행하면 됩니다. 프록시 핸들러 자체를 프록시로 구현하면 됩니다. 이 내용은 안타깝게도 이 도움말의 범위를 벗어납니다.
객체 확장
프록시의 또 다른 일반적인 사용 사례는 객체에 대한 작업의 시맨틱을 확장하거나 재정의하는 것입니다. 예를 들어 핸들러가 작업을 로깅하거나, 관찰자에게 알리거나, 정의되지 않음을 반환하는 대신 예외를 발생시키거나, 저장을 위해 작업을 다른 대상로 리디렉션할 수 있습니다. 이 경우 프록시를 사용하면 대상 객체를 사용할 때와 매우 다른 결과가 나올 수 있습니다.
function extend(sup,base) {
var descriptor = Object.getOwnPropertyDescriptor(base.prototype,"constructor");
base.prototype = Object.create(sup.prototype);
var handler = {
construct: function(target, args) {
var obj = Object.create(base.prototype);
this.apply(target,obj, args);
return obj;
},
apply: function(target, that, args) {
sup.apply(that,args);
base.apply(that,args);
}
};
var proxy = new Proxy(base, handler);
descriptor.value = proxy;
Object.defineProperty(base.prototype, "constructor", descriptor);
return proxy;
}
var Vehicle = function(name){
this.name = name;
};
var Car = extend(Vehicle, function(name, year) {
this.year = year;
});
Car.prototype.style = "Saloon";
var Tesla = new Car("Model S", 2016);
console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year); // 2016
액세스 제어
액세스 제어도 프록시의 좋은 사용 사례입니다. 신뢰할 수 없는 코드에 대상 객체를 전달하는 대신 일종의 보호 멤브레인으로 래핑된 프록시를 전달할 수 있습니다. 앱이 신뢰할 수 없는 코드가 특정 작업을 완료했다고 판단하면 프록시를 타겟에서 분리하는 참조를 취소할 수 있습니다. 멤브레인은 정의된 원래 타겟에서 도달할 수 있는 모든 객체로 이 분리를 재귀적으로 확장합니다.
프록시와 함께 리플렉션 사용
Reflect는 가로챌 수 있는 JavaScript 작업을 위한 메서드를 제공하는 새로운 내장 객체로, 프록시 작업에 매우 유용합니다. 사실 Reflect 메서드는 프록시 핸들러의 메서드와 동일합니다.
Python 또는 C# 과 같은 정적으로 유형이 지정된 언어는 오래전부터 리플렉션 API를 제공해 왔지만 JavaScript는 동적 언어이므로 리플렉션 API가 필요하지 않았습니다. ES5에는 이미 다른 언어에서 리플렉션으로 간주되는 Array.isArray()
또는 Object.getOwnPropertyDescriptor()
와 같은 리플렉션 기능이 많이 있다고 주장할 수 있습니다. ES2015에서는 이 카테고리의 향후 메서드를 보관하고 더 쉽게 추론할 수 있도록 하는 Reflection API를 도입합니다. 이는 객체가 리플렉션 메서드의 버킷이 아닌 기본 프로토타입이어야 하기 때문입니다.
Reflect를 사용하면 이전의 Superhero 예시를 개선하여 get 및 set 트랩에서 올바른 필드 가로채기를 다음과 같이 실행할 수 있습니다.
// Field interception with Proxy and the Reflect API
var pioneer = new Proxy({}, {
get: function(target, name, receiver) {
console.log(`get called for field: ${name}`);
return Reflect.get(target, name, receiver);
},
set: function(target, name, value, receiver) {
console.log(`set called for field: ${name} and value: ${value}`);
return Reflect.set(target, name, value, receiver);
}
});
pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName
다음이 출력됩니다.
set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName
다른 예로는 다음과 같은 경우가 있습니다.
특정 로직을 사용할 때마다 새 프록시를 수동으로 만들지 않도록 커스텀 생성자 내에 프록시 정의를 래핑합니다.
데이터가 실제로 수정된 경우에만 변경사항을 '저장'하는 기능을 추가합니다 (가정적으로 저장 작업이 매우 비용이 많이 들기 때문에).
function Customer() {
var proxy = new Proxy({
save: function(){
if (!this.dirty){
return console.log('Not saving, object still clean');
}
console.log('Trying an expensive saving operation: ', this.changedProperties);
},
}, {
set: function(target, name, value, receiver) {
target.dirty = true;
target.changedProperties = target.changedProperties || [];
if(target.changedProperties.indexOf(name) == -1){
target.changedProperties.push(name);
}
return Reflect.set(target, name, value, receiver);
}
});
return proxy;
}
var customer = new Customer();
customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation: ["name", "surname"]
customer.save();
Reflect API 예시는 Tagtree의 ES6 Proxies를 참고하세요.
Object.observe() 다각형 채우기
Object.observe()
는 종료되었지만 이제 ES2015 프록시를 사용하여 폴리필할 수 있습니다. Simon Blackwell은 최근 확인해 볼 만한 프록시 기반 Object.observe() 시임을 작성했습니다. 에릭 아르비드손은 2012년에 이미 상당히 사양 완성 버전을 작성했습니다.
브라우저 지원
ES2015 프록시는 Chrome 49, Opera, Microsoft Edge, Firefox에서 지원됩니다. Safari에서는 이 기능에 대한 공개 신호가 엇갈렸지만 Google은 낙관적인 입장을 유지하고 있습니다. Reflect는 Chrome, Opera, Firefox에서 사용할 수 있으며 Microsoft Edge용으로 개발 중입니다.
Google은 프록시용 제한된 폴리필을 출시했습니다. 프록시가 생성될 때 알려진 속성만 프록시할 수 있으므로 일반 래퍼에만 사용할 수 있습니다.