프록시 (Proxy) 객체란?
프로그래밍적 정의
- 타겟 오브젝트를 정하고 타겟 오브젝트를 감싸는 객체이다.
- 타겟 객체의
기본 작업 (fundamental operation)
을 가로채 재정의하는 기능이 있다.
- 타겟 객체의
- 프록시에 의해 해당 객체의 기본 작업 수행 시
트랩 (trap)
이라 불리는 핸들러 함수가 호출된다.
현실 세계와의 비유
프록시는 대리라는 뜻으로 현실에 비유하자면 연예인의 매니저 같은 것이다. 학교 축제에 블랙핑크의 제니를 섭외하고 싶다고 해서 제니의 연락처를 직접 알 수는 없다. 소속사를 통해 제니의 매니저와 통화를 먼저 거치고 가격, 시간 등에 대한 협의가 되어야 비로소 학교 축제에서 제니를 볼 수 있을 것이다.
기본 작업 (fundamental operation) 이란?
- 기본 작업을 가로챈다고 했는데, 기본 작업이 무엇인지 용어만 듣고는 알기 힘들다.
- 참고로
기본 작업 (fundamental operation)
이란 용어는 MDN 공식문서 에서 차용한 것이다.
- 참고로
- 기본 작업의 예시는 아래와 같다.
- 프로퍼티 값 접근 (access)
- 프로퍼티 값 할당 (assign)
- 객체 내부 메서드 호출 등 (function invocation)
트랩 (trap) 이란?
- 기본 작업이 일어날 때 동작하는 함수를 말하는 것이다.
간단 예제 코드
const target = {
message: "hello",
message2: "whole new",
};
const trapHandler = {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver) + " world";
},
};
const proxy = new Proxy(target, trapHandler);
console.log(proxy.message); // hello world
console.log(proxy.message2); // whole new world
console.log(target.message); // hello
console.log(target.message2); // whole new
- 프록시 생성자인
new Proxy()
를 통해 새로운 프록시 객체를 생성할 수 있다.- 인자로는 차례로
target
,trap handler
가 들어간다. target
은 프록시를 적용할 원본 대상 객체를 말한다.trap handler
는 기본 작업 이전에 추가될 동작을 정의하는 객체이다.
- 인자로는 차례로
- 프록시 객체를 통해 프로퍼티에 접근하면
" world"
라는 문자열을 추가로 반환하게 한 예제이다. Reflect
객체는 이전 포스팅에서 설명했듯이 프록시 트랩 핸들러에서 '기본 동작'을 그대로 제공한다.
프록시 트랩의 종류
- 트랩은 여러가지 기본동작을 가로챌 수 있다.
- 이전 간단 예제 코드에서는 프로퍼티 접근을 가로채는
get
을 살펴봤다.
- 이전 간단 예제 코드에서는 프로퍼티 접근을 가로채는
모든 오브젝트에 해당하는 트랩
모든 오브젝트에 있는 기본 작업들이다.
왼쪽이 내부 메서드, 오른쪽이 그에 대응하는 트랩이다.
[[GetPrototypeOf]]
:getPrototypeOf()
[[SetPrototypeOf]]
:setPrototypeOf()
[[IsExtensible]]
:isExtensible()
[[PreventExtensions]]
:preventExtensions()
[[DefineOwnProperty]]
:defineProperty()
[[HasProperty]]
:has()
[[Get]]
:get()
[[Set]]
:set()
[[Delete]]
:deleteProperty()
[[OwnPropertyKeys]]
:ownKeys()
함수 오브젝트에 해당하는 트랩
함수 오브젝트만 사용할 수 있는 트랩이다.
[[Call]]
:apply()
[[Construct]]
:construct()
프록시의 사용 사례
- 객체에서 발생하는 작업 기록 (logging)
- 제한된 속성만 읽게 하기 (권한 설정)
- 속성에 제한된 값만 넣게 하기 (validation)
- 두 코드 영역 사이 경계 제공 (ex. API와 컨슈머)
- 객체 정보 숨기기
- 객체가 실제보다 더 많은 정보를 가진 것처럼 보이게 하기
작업 기록하기 (logging)
가장 간단한 예제
const obj = {
testing: "abc",
};
const proxy = new Proxy(
obj,
// trap handler object
{
get(target, name, receiver) {
console.log(`(getting property '${name}') ${new Date().toString()}`);
return Reflect.get(target, name, receiver);
},
}
);
console.log("Getting 'testing' directly...");
console.log(`Got ${obj.testing}`);
console.log("Getting 'testing' via proxy...");
console.log(`Got ${proxy.testing}`);
console.log("Getting non-existent property 'foo' via proxy...");
console.log(`Got ${proxy.foo}`);
/*
출력 결과:
Getting 'testing' directly...
Got abc
Getting 'testing' via proxy...
(getting property 'testing') Wed Mar 29 2023 22:08:57 GMT+0900 (한국 표준시)
Got abc
Getting non-existent property 'foo' via proxy...
(getting property 'foo') Wed Mar 29 2023 22:08:57 GMT+0900 (한국 표준시)
Got undefined
*/
proxy.foo
의 결과는 존재하지 않지만, 역시 동일하게 트랩을 거친다.
거의 모든 트랩 로깅해보기
- 트랩이 발생하는 조건을 생각해보면 모든 트랩을 로깅할 수 있다.
getter
와 setter
그리고 defineProperty()
// 각 객체가 가진 식별자를 저장하기 위한 용도
const names = new WeakMap();
/**
* `log()` 함수는 객체를 받아 객체의 키와 키 내부에 있는 객체의 값을 반환해준다.
*/
const log = (label, params) => {
console.log(
`${label} : ${Object.getOwnPropertyNames(params)
.map((key) => {
const value = params[key];
const name = names.get(value);
const display = name ? name : JSON.stringify(value);
return `${key} = ${display}`;
})
.join(", ")}`
);
};
const example = { answer: 42 };
names.set(example, "example");
log("Testing 1 2 3", { value: example });
// 모든 기본행동에 `trap` 을 하는 핸들러이다.
const handlers = {
apply(target, thisValue, args) {
log("apply", { target, thisValue, args });
return Reflect.apply(target, thisValue, args);
},
construct(target, args, newTarget) {
log("construct", { target, args, newTarget });
return Reflect.construct(target, args, newTarget);
},
defineProperty(target, propName, descriptor) {
log("defineProperty", { target, propName, descriptor });
return Reflect.defineProperty(target, propName, descriptor);
},
deleteProperty(target, propName) {
log("deleteProperty", { target, propName });
return Reflect.deleteProperty(target, propName);
},
get(target, propName, receiver) {
log("get", { target, propName, receiver });
return Reflect.get(target, propName, receiver);
},
getOwnPropertyDescriptor(target, propName) {
log("getOwnPropertyDescriptor", { target, propName });
return Reflect.getOwnPropertyDescriptor(target, propName);
},
getPrototypeOf(target) {
log("getPrototypeOf", { target });
return Reflect.getPrototypeOf(target);
},
has(target, propName) {
log("has", { target, propName });
return Reflect.has(target, propName);
},
isExtensible(target) {
log("isExtensible", { target });
return Reflect.isExtensible(target);
},
ownKeys(target) {
log("ownKeys", { target });
return Reflect.ownKeys(target);
},
preventExtensions(target) {
log("preventExtensions", { target });
return Reflect.preventExtensions(target);
},
set(target, propName, value, receiver) {
log("set", { target, propName, value, receiver });
return Reflect.set(target, propName, value, receiver);
},
setPrototypeOf(target, newProto) {
log("setPrototypeOf", { target, newProto });
return Reflect.setPrototypeOf(target, newProto);
},
};
// 카운터 클래스 정의
class Counter {
constructor(name) {
this.value = 0;
this.name = name;
}
increment() {
return ++this.value;
}
}
// 인스턴스 생성 및 `names` 맵에 저장하기
const counter = new Counter("counter");
const counterProxy = new Proxy(counter, handlers);
names.set(counter, "counter");
names.set(counterProxy, "counterProxy");
console.log("--- Getting counterProxy.value (before increment):");
console.log(`counterProxy.value (before) = ${counterProxy.value}`);
/*
출력결과:
--- Getting counterProxy.value (before increment):
get : target = counter, propName = "value", receiver = counterProxy
counterProxy.value (before) = 0
*/
console.log("--- Calling counterProxy.increment():");
counterProxy.increment();
/*
--- Calling counterProxy.increment():
get : target = counter, propName = "increment", receiver = counterProxy
get : target = counter, propName = "value", receiver = counterProxy
set : target = counter, propName = "value", value = 1, receiver = counterProxy
getOwnPropertyDescriptor : target = counter, propName = "value"
defineProperty : target = counter, propName = "value", descriptor = {"value":1}
*/
- 처음엔
getter
밖에 실행하지 않아서trap
에 걸린 것은 하나 뿐이다. - 두번째엔
increment()
함수가 실행되며++
연산이 수행되었다.- 먼저
increment()
메서드를 가져오기 위해[[Get]]
이 발생되었다. ++
연산의 경우엔 쪼개서 보면, 값 가져오기 ([[Get]]
), 값 넣기 ([[Set]]
) 로 이루어져있다.- 뒤의
getOwnPropertyDescriptor
와defineProperty
는 우리가 설정한 값이데이터 속성
이기 때문이다. 접근자
는setter 함수
가 호출되고 끝나지만,데이터 속성
인 경우,[[GetOwnProperty]]
로descriptor
를 가져온 뒤, 값을 설정하고[[DefineOwnProperty]]
를 통해 값을 업데이트한다.
- 먼저
[[DefineOwnProperty]]
에 트랩을 걸면, 모든 세터 함수에 관여할 수 있다.
ownKeys
트랩
console.log("--- Getting counterProxy's own enumerable string-named keys:");
console.log(Object.keys(counterProxy));
/*
ownKeys : target = counter
getOwnPropertyDescriptor : target = counter, propName = "value"
getOwnPropertyDescriptor : target = counter, propName = "name"
(2) ['value', 'name']
*/
deleteProperty
트랩
console.log("--- Deleting counterProxy.value:");
delete counterProxy.value;
/*
--- Deleting counterProxy.value:
deleteProperty : target = counter, propName = "value"
*/
has
트랩
console.log("--- Checking whether counterProxy has a 'value' property:");
console.log(`"value" in counterProxy? ${"value" in counterProxy}`);
/*
has : target = counter, propName = "value"
"value" in counterProxy? false
*/
프록시 트랩 예제 코드로 알아보기
- 각 프록시 트랩을 몇가지 예제 코드로 알아보자.
defineProperty
트랩 예제: writable
을 false
로 바꾸지 못하게 하기
const obj = {};
const p = new Proxy(obj, {
defineProperty(target, propName, descriptor) {
if ("writable" in descriptor && !descriptor.writable) {
const currentDescriptor = Reflect.getOwnPropertyDescriptor(
target,
propName
);
if (currentDescriptor && currentDescriptor.writable) {
return false;
}
}
return Reflect.defineProperty(target, propName, descriptor);
},
});
p.a = 1;
console.log(`p.a = ${p.a}`);
console.log("Trying to make p.a non-writable...");
console.log(
`Result of defineProperty: ${Reflect.defineProperty(p, "a", {
writable: false,
})}`
);
console.log("Setting pa.a to 2...");
p.a = 2;
console.log(`p.a = ${p.a}`);
/*
출력결과:
p.a = 1
Trying to make p.a non-writable...
Result of defineProperty: false
Setting pa.a to 2...
p.a = 2
*/
- descriptor 에 대한 이해가 선행되어야 이해되는 예제이다.
if ("writable" in descriptor && !descriptor.writable)
부분에서descriptor.writable
이false
일 때 동작 자체를 막는다.Reflect.defineProperty(p, "a", { writable: false })
가 아니라Object.defineProperty()
를 썼다면, 타입 에러가 던져진다.Reflect
는 오류가 발생하면false
를 반환한다.
defineProperty
트랩은descriptor
인자로 온 객체를 직접 받는 것이 아니라, 유효한propertyName
을 가져가서 특수한 객체로 변환한다.
deleteProperty
트랩 예제: 특정 프로퍼티의 삭제 막기
const obj = { value: 42 };
const p = new Proxy(obj, {
deleteProperty(target, propName, descriptor) {
if (propName === "value") {
return false; // `value` 라는 이름의 프로퍼티를 삭제할 수 없게 막음
}
return Reflect.deleteProperty(target, propName, descriptor);
},
});
console.log(`p.value = ${p.value}`);
console.log("deleting 'value' from p in loose mode:...");
console.log(delete p.value); // false
console.log(p.value); // 42
(() => {
"use strict";
console.log("deleting 'value' from p in strict mode:");
try {
delete p.value;
} catch (error) {
console.error(error);
}
})();
value
프로퍼티에 대한 삭제를 시도했을 때 무조건false
를 반환하게 하여 삭제를 하지 못하도록 했다.- 엄격모드에서는 삭제 실패가 에러를 던지기 때문에 에러가 던져진다.
preventExtensions
트랩 예제: 오브젝트 확장 못막게 하기
const obj = { value: 42 };
const p = new Proxy(obj, {
// preventExtensions(target) {
// return false;
// },
});
console.log(Reflect.isExtensible(p));
p.value2 = 30;
console.log(Reflect.preventExtensions(p));
p.value3 = 50;
console.log(Reflect.isExtensible(p));
console.log(p); // Proxy(Object) {value: 42, value2: 30}
Reflect.preventExtensions()
메서드는 오브젝트의 확장을 막는다.- 확장이 막혀
p.value3 = 50
을 통해value3
프로퍼티를 추가했지만, 반응이 없다.
const obj = { value: 42 };
const p = new Proxy(obj, {
preventExtensions(target) {
return false;
},
});
console.log(Reflect.isExtensible(p));
p.value2 = 30;
console.log(Reflect.preventExtensions(p));
p.value3 = 50;
console.log(Reflect.isExtensible(p));
console.log(p); // Proxy(Object) {value: 42, value2: 30, value3: 50}
Reflect.preventExtensions()
메서드의 결과가 핸들러에서 무조건false
를 반환하게 만들어 확장을 못막게 한다.- 확장덕에
p.value3 = 50
을 통해value3
프로퍼티를 추가가 그대로 적용된다.
취소 가능한 프록시 만들기 (Proxy.revocable()
)
- 취소 가능한 프록시를 만들 수 있다.
- 시간이 되면 프록시를 취소할 수 있다.
- 프록시가 취소되면 프록시에 대한 모든 작업이 오류와 함께 실패한다.
예제 코드
const obj = { answer: 42 };
const { proxy, revoke } = Proxy.revocable(obj, {});
console.log(proxy.answer);
revoke();
console.log(proxy.answer);
console.log(obj.answer);
/*
출력결과:
42
Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
at <anonymous>:5:19
42
*/
- 취소된 이후에는 프록시 객체가 동작하지 않는다.
- 에러와 함께 실패한다.
- 원본 객체에는 여전히 접근 가능하다.
프록시로 해결 가능한 문제
set 권한을 주지 않기 위해 get 을 별도로 만들지 말고 프록시 객체를 사용하자
- 프로퍼티를 소비하는 API 를 따로 만들지 말고 프록시 객체를 이용하자.
구현 코드와 부가 관심사 코드를 분리하자
- 객체 내부에 스며들어야 하는 로직이 아니면, 프록시로 분리된 레이어를 구성하여 관심사를 나누어 로직 작성이 가능하다.
- 트랩 핸들러는 다른 객체에 재활용도 가능하니 재활용 할 수 있다면 재활용 하면 좋다.
'자바스크립트 > 모던 자바스크립트' 카테고리의 다른 글
모던 자바스크립트, 모듈 3 - 트리 셰이킹과 번들링 (0) | 2023.03.26 |
---|---|
모던 자바스크립트, 모듈 2 - 모듈의 동작 방식 (0) | 2023.03.26 |
모던 자바스크립트, 모듈 1 - import 와 export 방식 (0) | 2023.03.26 |
모던 자바스크립트, 위크 맵(WeakMap) 과 위크 셋(WeakSet) (0) | 2023.03.24 |
모던 자바스크립트, 셋 혹은 세트 (Set) (0) | 2023.03.23 |