Proxy
: 특정 객체를 감싸 프로퍼티 읽기, 쓰기와 같은 객체에 가해지는 작업을 중간에서 가로채는 객체
: 가로채진 작업은 Proxy 자체에서 처리되거나 원래 객체가 처리하도록 그대로 전달도 가능
let proxy = new Proxy(target, handler)
- target
: 감싸게 될 객체로, 함수를 포함한 모든 객체 가능
- handler
: 동작을 가로채는 메서드인 trap이 담긴 객체
: 여기서 프락시를 설정
=> proxy에 작업이 가해지고 handler 작업과 상응하는 트랩이 있으면
트랩이 실행되어 프락시가 작업을 처리
트랙이 없으면 target에 작업이 직접 수행됨
trap이 없는 프락시
let target = {};
let proxy = new Proxy(target, {}); // 빈 핸들러
proxy.test = 5; // 프락시에 값을 씁니다. -- (1)
alert(target.test); // 5, target에 새로운 프로퍼티가 생겼네요!
alert(proxy.test); // 5, 프락시를 사용해 값을 읽을 수도 있습니다. -- (2)
for(let key in proxy) alert(key); // test, 반복도 잘 동작합니다. -- (3)
: 위 예시의 프락시에는 트랩이 없기 때문에 프락시에 가해지는 모든 작업은 타겟에 전달됨
: proxy.test = 를 이용해 값을 쓰면 target에 새로운 값이 설정됨
: proxy.test를 이용해 값을 읽으면 target에서 값을 읽어옴
: proxy를 대상으로 반복 작업을 하면 target을 둘러싸는 투명한 래퍼가 됨
객체 관점의 Proxy
: Proxy는 일반 객체와는 다른 행동 양상을 보이는 특수 객체
: 프로퍼티가 없음
: handler가 비어있으면 proxy에 가해지는 작업은 target에 곧바로 전달됨
트랩을 추가하여 Proxy의 기능을 활성화 하기
: 객체가 어떤 작업을 할 대는 자바스크립트 명세서에 적의된 내부 메서드가 깊숙한 곳에서 관여함
: 프로퍼티를 읽을 때는 [[Get]]이라는 내부 메서드가,
프로퍼티를 쓸 때는 [[Set]]이라는 내부 메서드가 관여
: 이런 내부 메서드들은 명세서에만 정의 되었기 때문에 개발자가 호출할 수 없음
: 프락시의 트랩은 내부 메서드의 호출을 가로챔
내부 메서드에 대응하는 트랩
프락시의 규칙
: 값을 쓰는게 성공적으로 처리되었으면 [[Set]]은 반드시 true와 false를 반환
: 값을 지우는게 성공적으로 처리되었으면 [[Delete]]는 반드시 true 혹은 false를 반환
: 기타 등등 (refference 참조)
get 트랩으로 프로퍼티 기본 값 설정하기
: 프로퍼티 읽기를 가로채기 위해서는 handler에 get (target, property, receiver) 메서드가 필요
- target
: 동작을 전달할 객체로 new Proxy의 첫 번째 인자
- property
: 프로퍼티의 이름
- receiver
: 타깃 프로퍼티가 getter라면 receiver는 getter가 호출될 때의 this
: 대개는 proxy 객체 자신이 this가 됨
: 프락시 객체를 상속받은 객체가 있다면 해당 객체가 this가 되기도 함
- get을 이용해 트랩을 만드는 예시
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // 기본값
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (해당하는 요소가 배열에 없으므로 0이 반환됨)
- 구절과 번역문이 저장되어 있는 dictionary에 대한 예시
let dictionary = {
'Hello': '안녕하세요',
'Bye': '안녕히 가세요'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // 프로퍼티를 읽기를 가로챕니다.
if (phrase in target) { // 조건: 사전에 구절이 있는 경우
return target[phrase]; // 번역문을 반환합니다
} else {
// 구절이 없는 경우엔 구절 그대로를 반환합니다.
return phrase;
}
}
});
// 사전을 검색해봅시다!
// 사전에 없는 구절을 입력하면 입력값이 그대로 반환됩니다.
alert( dictionary['Hello'] ); // 안녕하세요
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (입력값이 그대로 출력됨)
set 트랩으로 프로퍼티 값 검증하기
: 프로퍼티에 값을 쓰려고 할 때 이를 가로채는 set 트랩을 사용
: set (target, property, value, receiver)
- target
: 동작을 전달할 객체로 new Proxy의 첫 번째 인자
- property
: 프로퍼티의 이름
- value
: 프로퍼티의 값
- receiver
: setter 프로퍼티에만 관여하는 this 객체
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // 프로퍼티에 값을 쓰는 동작을 가로챕니다.
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // 추가가 성공했습니다.
numbers.push(2); // 추가가 성공했습니다.
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // Error: 'set' on proxy
alert("윗줄에서 에러가 발생했기 때문에 이 줄은 절대 실행되지 않습니다.");
=> true를 반드시 반환해야함
ownKeys와 getOwnPropertyDescriptor로 반복 작업하기
: Object.keys, for in 반복문을 비롯한 프로퍼티 순환 관련 메서드 대다수는
내부 메서드 [[OwnPropertyKeys]]를 사용해 프로퍼티 목록을 얻음
let user = { };
user = new Proxy(user, {
ownKeys(target) { // 프로퍼티 리스트를 얻을 때 딱 한 번 호출됩니다.
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // 모든 프로퍼티를 대상으로 호출됩니다.
return {
enumerable: true,
configurable: true
/* 이 외의 플래그도 반환할 수 있습니다. "value:..."도 가능합니다. */
};
}
});
alert( Object.keys(user) ); // a, b, c
deleteProperty와 여러 트랩을 사용해 프로퍼티 보호하기
- 필요한 트랩
: get = 프로퍼티를 읽으려고 하면 에러를 던져줌
: set = 프로퍼티에 쓰려고 하면 에러를 던져줌
: deleteProperty = 프로퍼티를 지우려고 하면 에러를 던져줌
: ownKeys = for..in이나 Object.keys같은 프로퍼티 순환 메서드를 사용할 때 _로 시작하는 메서드는 제외함
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("접근이 제한되어있습니다.");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, val) { // 프로퍼티 쓰기를 가로챕니다.
if (prop.startsWith('_')) {
throw new Error("접근이 제한되어있습니다.");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // 프로퍼티 삭제를 가로챕니다.
if (prop.startsWith('_')) {
throw new Error("접근이 제한되어있습니다.");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // 프로퍼티 순회를 가로챕니다.
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" 트랩이 _password 읽기를 막습니다.
try {
alert(user._password); // Error: 접근이 제한되어있습니다.
} catch(e) { alert(e.message); }
// "set" 트랩이 _password에 값을 쓰는것을 막습니다.
try {
user._password = "test"; // Error: 접근이 제한되어있습니다.
} catch(e) { alert(e.message); }
// "deleteProperty" 트랩이 _password 삭제를 막습니다.
try {
delete user._password; // Error: 접근이 제한되어있습니다.
} catch(e) { alert(e.message); }
// "ownKeys" 트랩이 순회 대상에서 _password를 제외시킵니다.
for(let key in user) alert(key); // name
has 트랩으로 범위 내 여부 확인하기
: has(target, property)
- target
: new Proxy의 첫 번째 인자로 전달되는 타깃 객체
- property
: 프로퍼티 이름
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
apply 트랩으로 함수 감싸기
: apply(target, thisArg, args)
: 프락시를 함수처럼 호출하려고 할 때 동작
- target
: 타깃 객체(자바스크립트에서 함수는 객체임)
- thisArg
: this의 값
- args
: 인수 목록
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) 프락시는 "get length" 연산까지 타깃 객체에 전달해줍니다.
sayHi("John"); // Hello, John! (3초 후)
: proxy 객체는 타깃 객체의 모든 것을 전달해 주므로 훨씬 강력함
Reflect
: Refference 참조
Refference
'Programming > JavaScript' 카테고리의 다른 글
모던 자바스크립트 (문서) 18-2. DOM 트리 (0) | 2022.04.24 |
---|---|
모던 자바스크립트 (문서) 18-1. 브라우저 환경과 다양한 명세서 (0) | 2022.04.24 |
모던 자바스크립트 16. BigInt (0) | 2022.04.23 |
모던 자바스크립트 15. 참조 타입 (0) | 2022.04.23 |
모던 자바스크립트 14. 커링 (0) | 2022.04.23 |