Proxy
Baseline Widely available
This feature is well established and works across many devices and browser versions. It’s been available across browsers since September 2016.
Proxy
객체를 사용하면 한 객체에 대한 기본 작업을 가로채고 재정의하는 프록시를 만들 수 있습니다.
설명
Proxy
객체를 사용하면 원래 Object
대신 사용할 수 있는 객체를 만들지만, 이 객체의 속성 가져오기, 설정 및 정의와 같은 기본 객체 작업을 재정의할 수 있습니다. 프록시 객체는 일반적으로 속성 액세스를 기록하고, 입력의 유효성을 검사하고, 형식을 지정하거나, 삭제하는 데 사용됩니다.
두 개의 매개변수를 사용하여 Proxy
를 생성합니다.
target
: 프록시할 원본 객체handler
: 가로채는 작업과 가로채는 작업을 재정의하는 방법을 정의하는 객체입니다.
예를 들어 아래 코드는 속성이 두 개뿐인 간단한 대상과 속성이 없는 훨씬 더 간단한 핸들러를 정의합니다.
const target = {
message1: "hello",
message2: "everyone",
};
const handler1 = {};
const proxy1 = new Proxy(target, handler1);
핸들러가 비어 있기 때문에 이 프록시는 원래 대상처럼 작동합니다.
console.log(proxy1.message1); // hello
console.log(proxy1.message2); // everyone
프록시를 커스텀하기 위해 처리기 객체에 함수를 정의합니다.
const target = {
message1: "hello",
message2: "everyone",
};
const handler2 = {
get(target, prop, receiver) {
return "world";
},
};
const proxy2 = new Proxy(target, handler2);
대상 객체의 속성 액세스를 가로채는 get()
처리기를 제공했습니다.
처리기 함수는 대상 객체에 대한 호출을 잡아내기 때문에 트랩(traps) 이라고도 부릅니다. 위의 handler2
에 있는 매우 간단한 트랩은 모든 속성 접근자를 재정의합니다.
console.log(proxy2.message1); // world
console.log(proxy2.message2); // world
Reflect
클래스의 도움으로 일부 접근자에게 원래 동작을 제공하고 다른 접근자를 재정의할 수 있습니다.
const target = {
message1: "hello",
message2: "everyone",
};
const handler3 = {
get(target, prop, receiver) {
if (prop === "message2") {
return "world";
}
return Reflect.get(...arguments);
},
};
const proxy3 = new Proxy(target, handler3);
console.log(proxy3.message1); // hello
console.log(proxy3.message2); // world
생성자
Proxy()
-
새
Proxy
객체를 생성합니다.
정적 메서드
Proxy.revocable()
-
취소 가능한
Proxy
객체를 생성합니다.
예제들
기본 예제
이 간단한 예에서는 속성 이름이 객체에 없으면 숫자 37
을 기본값으로 반환합니다. get()
처리기를 사용합니다.
const handler = {
get(obj, prop) {
return prop in obj ? obj[prop] : 37;
},
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b);
// 1, undefined
console.log("c" in p, p.c);
// false, 37
No-op 포워딩 프록시
이 예에서는 기본 JavaScript 객체를 사용하여 프록시가 모든 작업을 대상 객체에게 전달합니다.
const target = {};
const p = new Proxy(target, {});
p.a = 37;
// 대상 객체에게 작업 전달
console.log(target.a);
// 37
// (작업이 제대로 전달됨!)
이 "no-op"는 일반 JavaScript 객체에 대해 작동하지만 DOM 요소, Map
객체 또는 내부 슬롯이 있는 모든 기본 객체에는 작동하지 않습니다. 자세한 내용은 프라이빗 속성 포워딩 없음을 참조하십시오.
프라이빗 속성 포워딩 없음
프록시는 여전히 다른 ID를 가진 또 다른 객체로, 래핑된 객체와 외부 사이에서 작동하는 프록시일 뿐입니다. 따라서 프록시는 원래 객체의 프라이빗 속성에 직접 접근할 수 없습니다.
class Secret {
#secret;
constructor(secret) {
this.#secret = secret;
}
get secret() {
return this.#secret.replace(/\d+/, "[REDACTED]");
}
}
const aSecret = new Secret("123456");
console.log(aSecret.secret); // [REDACTED]
// no-op 포워딩 같지만...
const proxy = new Proxy(aSecret, {});
console.log(proxy.secret); // TypeError: Cannot read private member #secret from an object whose class did not declare it
이는 프록시의 get
트랩이 호출될 때 this
값이 원래 secret
이 아닌 proxy
이므로 #secret
에 액세스할 수 없기 때문입니다. 이 문제를 해결하려면 원래 secret
을 this
로 사용하세요.
const proxy = new Proxy(aSecret, {
get(target, prop, receiver) {
// 기본적으로 'this'값이 다른
// Reflect.get(target, prop, receiver)처럼 보입니다.
return target[prop];
},
});
console.log(proxy.secret);
메서드의 경우 메서드의 this
값도 원래 객체로 리디렉션해야 합니다.
class Secret {
#x = 1;
x() {
return this.#x;
}
}
const aSecret = new Secret();
const proxy = new Proxy(aSecret, {
get(target, prop, receiver) {
const value = target[prop];
if (value instanceof Function) {
return function (...args) {
return value.apply(this === receiver ? target : this, args);
};
}
return value;
},
});
console.log(proxy.x());
일부 기본 JavaScript 객체에는 JavaScript 코드에서 액세스할 수 없는 내부 슬롯 이라는 속성이 있습니다. 예를 들어, Map
객체에는 맵의 키-값 쌍을 저장하는 [[MapData]]
라는 내부 슬롯이 있습니다. 따라서 맵에 대한 전달 프록시는 간단하게 만들 수 없습니다.
const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy
이 문제를 해결하려면 위에 설명된 "this
-recovering" 프록시를 사용해야 합니다.
검증
프록시를 사용하면 객체에 대해 전달된 값을 쉽게 확인할 수 있습니다. 이 예제에서는 set()
처리기를 사용합니다.
const validator = {
set(obj, prop, value) {
if (prop === "age") {
if (!Number.isInteger(value)) {
throw new TypeError("The age is not an integer");
}
if (value > 200) {
throw new RangeError("The age seems invalid");
}
}
// 값을 저장하는 기본 동적
obj[prop] = value;
// 성공 표시
return true;
},
};
const person = new Proxy({}, validator);
person.age = 100;
console.log(person.age); // 100
person.age = "young"; // 예외 발생
person.age = 300; // 예외 발생
생성자 확장하기
함수 프록시는 새 생성자로 생성자를 쉽게 확장할 수 있습니다. 이 예제에서는 construct()
과 apply()
처리기를 사용합니다.
function extend(sup, base) {
base.prototype = Object.create(sup.prototype);
base.prototype.constructor = new Proxy(base, {
construct(target, args) {
const obj = Object.create(base.prototype);
this.apply(target, obj, args);
return obj;
},
apply(target, that, args) {
sup.apply(that, args);
base.apply(that, args);
},
});
return base.prototype.constructor;
}
const Person = function (name) {
this.name = name;
};
const Boy = extend(Person, function (name, age) {
this.age = age;
});
Boy.prototype.gender = "M";
const Peter = new Boy("Peter", 13);
console.log(Peter.gender); // "M"
console.log(Peter.name); // "Peter"
console.log(Peter.age); // 13
DOM 노드 조작
이 예에서는 Proxy
를 사용하여 두 개의 다른 요소의 속성을 토글합니다. 따라서 한 요소에 속성을 설정하면 다른 요소에 속성이 설정되지 않습니다.
selected
속성을 가진 객체에 대한 프록시인 view
객체를 생성합니다. 프록시 핸들러는 set()
처리기를 정의합니다.
view.selected
에 HTML 요소를 할당하면 요소의 'aria-selected'
속성이 true
로 설정됩니다. 그런 다음 view.selected
에 다른 요소를 할당하면 이 요소의 'aria-selected'
속성이 true
로 설정되고 이전 요소의 'aria-selected'
속성이 자동으로 false
로 설정됩니다.
const view = new Proxy(
{
selected: null,
},
{
set(obj, prop, newval) {
const oldval = obj[prop];
if (prop === "selected") {
if (oldval) {
oldval.setAttribute("aria-selected", "false");
}
if (newval) {
newval.setAttribute("aria-selected", "true");
}
}
// 값을 저장하는기본 동작
obj[prop] = newval;
// 성공 표시
return true;
},
},
);
const item1 = document.getElementById("item-1");
const item2 = document.getElementById("item-2");
// item1 선택
view.selected = item1;
console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: true
// item2를 선택하고 item1은 선택 해제
view.selected = item2;
console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: false
console.log(`item2: ${item2.getAttribute("aria-selected")}`);
// item2: true
값 수정 및 추가 속성
products
프록시 객체는 전달된 값을 계산하고 필요한 경우 배열로 변환합니다. 또한 객체는 getter 및 setter 모두에게 latestBrowser
라는 추가 속성을 지원합니다.
const products = new Proxy(
{
browsers: ["Internet Explorer", "Netscape"],
},
{
get(obj, prop) {
// 추가 속성
if (prop === "latestBrowser") {
return obj.browsers[obj.browsers.length - 1];
}
// 값을 저장하는 기본 동작
return obj[prop];
},
set(obj, prop, value) {
// 추가 속성
if (prop === "latestBrowser") {
obj.browsers.push(value);
return true;
}
// 값이 배열이 아닌 경우 배열로 변환
if (typeof value === "string") {
value = [value];
}
// 값을 저장하는 기본 동작
obj[prop] = value;
// 성공 표시
return true;
},
},
);
console.log(products.browsers);
// ['Internet Explorer', 'Netscape']
products.browsers = "Firefox";
// (실수로) 문자열을 넘겨줌
console.log(products.browsers);
// ['Firefox'] <- 문제없이 값은 배열로 변환됨
products.latestBrowser = "Chrome";
console.log(products.browsers);
// ['Firefox', 'Chrome']
console.log(products.latestBrowser);
// 'Chrome'
속성으로 배열 아이템 객체 찾기
이 프록시는 일부 유틸리티 기능으로 배열을 확장합니다. 보시다시피 Object.defineProperties()
를 사용하지 않고도 속성을 유연하게 "정의"할 수 있습니다. 이 예제는 해당 셀로 테이블 행을 찾는 데 적용할 수 있습니다. 이 경우 대상은 table.rows
가 됩니다.
const products = new Proxy(
[
{ name: "Firefox", type: "browser" },
{ name: "SeaMonkey", type: "browser" },
{ name: "Thunderbird", type: "mailer" },
],
{
get(obj, prop) {
// 값을 저장하는 기본 동작. prop은 보통 int
if (prop in obj) {
return obj[prop];
}
// product의 수를 가져옴. products.length에 대한 별칭
if (prop === "number") {
return obj.length;
}
let result;
const types = {};
for (const product of obj) {
if (product.name === prop) {
result = product;
}
if (types[product.type]) {
types[product.type].push(product);
} else {
types[product.type] = [product];
}
}
// 이름으로 product 가져오기
if (result) {
return result;
}
// type으로 product 가져오기
if (prop in types) {
return types[prop];
}
// types로 product 가져오기
if (prop === "types") {
return Object.keys(types);
}
return undefined;
},
},
);
console.log(products[0]); // { name: 'Firefox', type: 'browser' }
console.log(products["Firefox"]); // { name: 'Firefox', type: 'browser' }
console.log(products["Chrome"]); // undefined
console.log(products.browser); // [{ name: 'Firefox', type: 'browser' }, { name: 'SeaMonkey', type: 'browser' }]
console.log(products.types); // ['browser', 'mailer']
console.log(products.number); // 3
완전한 trap 예제
교훈적인 목적으로 완전한 샘플 traps
목록을 생성하기 위해 이 유형의 작업에 특히 적합한 네이티브 객체가 아닌 간단한 쿠키 프레임워크에 의해 생성된 전역 객체 docCookies
를 프록시화하려고 합니다.
/*
const docCookies = ... 여기서 "docCookies"를 가져옵니다.
https://reference.codeproject.com/dom/document/cookie/simple_document.cookie_framework
*/
const docCookies = new Proxy(docCookies, {
get(target, key) {
return target[key] || target.getItem(key) || undefined;
},
set(target, key, value) {
if (key in target) {
return false;
}
return target.setItem(key, value);
},
deleteProperty(target, key) {
if (!(key in target)) {
return false;
}
return target.removeItem(key);
},
ownKeys(target) {
return target.keys();
},
has(target, key) {
return key in target || target.hasItem(key);
},
defineProperty(target, key, descriptor) {
if (descriptor && "value" in descriptor) {
target.setItem(key, descriptor.value);
}
return target;
},
getOwnPropertyDescriptor(target, key) {
const value = target.getItem(key);
return value
? {
value,
writable: true,
enumerable: true,
configurable: false,
}
: undefined;
},
});
/* 쿠키 테스트 */
console.log((docCookies.myCookie1 = "First value"));
console.log(docCookies.getItem("myCookie1"));
docCookies.setItem("myCookie1", "Changed value");
console.log(docCookies.myCookie1);
명세
Specification |
---|
ECMAScript Language Specification # sec-proxy-objects |
브라우저 호환성
BCD tables only load in the browser