ECMAScript 6 프록시와 리플렉션 API

ECMAScript 6 프록시와 리플렉션 API

ECMAScript 5와 ECMAScript 6 모두 명확한 JavaScript 기능을 염두에 두고 개발되었습니다. 예를 들어, ECMAScript 5 이전에는 JavaScript 환경에 nonenumerable , nonwritable 객체 속성이 있었지만 개발자는 자체적으로 nonenumerable 또는 nonwritable 속성을 정의할 수 없었습니다. ECMAScript 5에는 JavaScript 엔진이 할 수 있는 것을 개발자가 수행할 수 있도록 Object.defineProperty() 메서드가 포함되어 있습니다.

ECMAScript 6은 이전에 Built-in 객체에만 사용할 수 있었던 JavaScript 엔진 기능에 대한 개발자의 액세스를 제공합니다. JavaScript는 프록시를 통해 객체의 내부 동작을 노출할 수 있습니다. 프록시는 JavaScript 엔진의 저수준 동작을 가로 채고 변경할 수있는 래퍼입니다. 이 장에서는 프록시에서 세부적으로 다루어야 할 문제를 설명한 다음 프록시를 효과적으로 만들고 사용하는 방법에 대해 설명합니다.

Array의 문제점

ECMAScript 6 이전에 JavaScript Array 객체는 개발자가 Array 객체를 모방할 수 없는 방식으로 동작합니다. Arraylength 프로퍼티는 특정 Array 항목에 값을 할당할 때 영향을 받으며 length 프로퍼티를 수정하여 Array 항목을 수정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"

colors Array는 3 개의 아이템으로 시작합니다. "black"colors [3]에 할당하면 length 프로퍼티가 4로 자동 증가합니다. length 프로퍼티를 2로 설정하면 Array의 마지막 두 항목이 제거되고 처음 두 항목만 남게됩니다. ECMAScript 5에서는 개발자가 작성한 객체에서 이러한 동작을 수행할 수 없지만 프록시는 이를 가능하게 합니다.

이 비표준 동작이 ECMAScript 6에서 Array가 독특한 객체로 간주되는 이유입니다.

프록시와 리플렉션은 무엇인가?

new Proxy()를 호출하여 다른 객체(대상(Target)이라고 함) 대신 사용할 프록시를 생성할 수 있습니다. 프록시는 대상을 가상화하여 프록시와 대상이 사용하는 기능을 동일한 객체로 표시되도록합니다.

프록시를 사용하면 JavaScript 엔진의 내부에 있는 대상에서 하위 수준의 객체 작업을 가로 챌 수 있습니다. 이러한 하위 수준의 작업은 특정 작업에 응답하는 기능인 Trap을 사용하여 가로 채어집니다.

Reflect 객체로 표현된 리플렉션 API는 프록시가 오버라이드 할 수있는 것과 동일한 로우 레벨 연산에 대한 기본 동작을 제공하는 메서드 컬렉션입니다. 모든 프록시 Trap에 대해 Reflect 메서드가 있습니다. 이러한 메서드는 동일한 이름을 가지며 각 프록시 Trap과 동일한 파라미터가 전달됩니다. 표 11-1에는 이 동작이 요약되어 있습니다.

Table 11-1: JavaScript 안의 프록시 Trap

프록시 Trap 동작을 재정의 기본 동작
get 프로퍼티 값을 읽음 Reflect.get()
set 프로퍼티 값을 기록 Reflect.set()
has in 연산자 Reflect.has()
deleteProperty delete 연산자 Reflect.deleteProperty()
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible Object.isExtensible() Reflect.isExtensible()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
defineProperty Object.defineProperty() Reflect.defineProperty
ownKeys Object.keys, Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() Reflect.ownKeys()
apply 함수 호출 Reflect.apply()
construct new를 이용한 함수 호출 Reflect.construct()

각 Trap은 JavaScript 객체의 Built-in 동작을 재정의하므로 동작을 가로 채고 수정할 수 있습니다. 그래도 Built-in 동작을 사용해야하는 경우 해당하는 리플렉션 API 메서드를 사용할 수 있습니다. 프록시 생성과 리플렉션 API 간의 관계는 프록시 생성을 시작할 때 명확 해집니다. 그래서 몇 가지 예를 살펴보는 것이 가장 좋습니다.

원래 ECMAScript 6 명세에는 for-inObject.keys()가 객체의 속성을 열거하는 방법을 변경하기 위해 고안된 enumerate라는 추가 Trap이 있습니다. 그러나 ECMAScript 7 (ECMAScript 2016이라고도 함)에서는 구현 중에 어려움이 발견되어 enumerate Trap이 제거되었습니다. enumerate Trap 더 이상 JavaScript 환경에 존재하지 않으므로 이 장에서 다루지 않습니다.

간단한 프록시 생성

Proxy 생성자를 사용하여 프록시를 만들 때, 두 개의 파라미터, 즉 대상과 핸들러를 넘깁니다. 핸들러는 하나 이상의 Trap을 정의하는 객체입니다. 프록시는 해당 작업에 대해 Trap이 정의된 경우를 제외하고 모든 작업에 대해 기본 동작을 사용합니다. 간단한 forwarding 프록시를 만들려면 Trap 없이 핸들러를 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"

이 예제에서 proxy는 모든 작업을 target에 직접 전달합니다. "proxy"proxy.name 프로퍼티에 할당되면 nametarget에 생성됩니다. 프록시 자체가 이 프로퍼티를 저장하지 않습니다. 이것은 단순히 작업을 target으로 전달하는 것입니다. 비슷하게, proxy.nametarget.name의 값은 target.name을 참조하기 때문에 동일합니다. 즉, target.name을 새로운 값으로 설정하면
proxy.name도 같은 변경을 반영합니다. 그리고 Trap이 없는 프록시는 별로 흥미롭지 않으므로 Trap을 정의하면 어떻게 될까요?

set Trap을 사용하여 프로퍼티 검증하기

프로퍼티 값이 숫자여야 하는 객체를 만들고 싶다고 가정 해보겠습니다. 즉, 객체에 추가된 모든 새로운 프로퍼티에 대해 유효성 검사를 해야하며 값이 숫자가 아닌 경우 오류가 발생되어야 합니다. 이것을 달성하기 위해, 값을 설정하는 기본 동작을 무시하는 set Trap을 정의 할 수 있습니다. set Trap은 네개의 파라미터를 받습니다.

  1. trapTarget - 프로퍼티를 수신하는 객체 (프록시의 타겟)
  2. key - 프로퍼티 키 (문자열 또는 Symbol)
  3. value - 프로퍼티 값
  4. receiver - 조작이 발생된 오브젝트 (일반적으로 프록시)

Reflect.set()set Trap에 대응하는 리플렉션 메서드이며, 이 연산의 기본 동작입니다. Reflect.set() 메서드는 set 프록시 Trap과 동일한 네개의 파라미터를 받아 Trap 내부에서 메서드를 사용하기 쉽게 만듭니다. Trap은 프로퍼티가 설정되면 true를 반환하고 그렇지 않으면 false를 반환합니다 (Reflect.set() 메서드는 작업이 성공했는지 여부에 따라 올바른 값을 반환합니다).

프로퍼티의 값을 검증하기 위해서는 set Trap을 사용하고 입력된 value를 검사 해야합니다. 다음은 그 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
// 기존 프로퍼티를 무시하므로 영향을주지 않습니다.
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("Property must be a number.");
}
}
// 프로퍼티를 추가합니다.
return Reflect.set(trapTarget, key, value, receiver);
}
});
// 새로운 프로퍼티를 추가합니다.
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
// 이미 대상에 존재하기 때문에 name에 지정할 수 있습니다.
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// throws an error
proxy.anotherName = "proxy";

이 코드는 target에 추가되는 새로운 프로퍼티의 값을 확인하는 프록시 Trap을 정의합니다. proxy.count=1이 실행되면 set Trap이 호출됩니다. trapTarget 값은 target과 같고 keycount, value1이며 receiver(이 예제에서는 사용되지 않음)는proxy입니다. targetcount라는 이름의 기존 프로퍼티가 없으므로 프록시는 isNaN()에 전달하여 값의 유효성을 검사합니다. 결과가 NaN의 경우, 숫자값이 아니기 때문에 에러가 발생됩니다. 이 코드는 count1로 설정하기 때문에, 프록시는 새 프로퍼티를 추가하기 위해 Trap에 전달된 네개의 파라미터를 사용하여 Reflect.set()을 호출합니다.

proxy.name에 문자열이 지정되어도 작업은 성공적으로 완료됩니다. target은 이미 name 프로퍼티를 가지고 있기 때문에, trapTarget.hasOwnProperty() 메서드를 호출함으로써 유효성 체크에서 그 프라퍼티를 생략합니다. 이렇게 하면 기존의 비숫자 프로퍼티 값이 계속 지원됩니다.

그러나 proxy.anotherName에 문자열이 할당되면 오류가 발생합니다. anotherName 프로퍼티는 target에 없으므로 해당 값의 유효성을 검사 해야합니다. 유효성 검사에서 "proxy"가 숫자 값이 아니기 때문에 오류가 발생합니다.

프로퍼티가 쓰여질 때 set 프록시 Trap이 가로챌수 있고, get 프록시 Trap은 프로퍼티가 읽혀질 때 가로 챌 수있습니다.

get Trap을 사용하여 객체 모양 유효성 검사

때때로 JavaScript의 흥미롭고 혼란스러운 부분 중 하나는 존재하지 않는 프로퍼티를 읽는 것이 오류를 발생시키지 않는다는 것입니다. 대신 다음 예와 같이 프로퍼티 값에 undefined 값이 사용됩니다.

1
2
3
let target = {};
console.log(target.name); // undefined

대부분의 다른 언어에서 target.name을 읽으려고 하면 프로퍼티가 존재하지 않기 때문에 오류가 발생합니다. 그러나 JavaScript는
target.name 프로퍼티 값에 undefined를 사용합니다. 대규모 코드 기반에서 작업한 적이 있다면, 특히 프로퍼티 이름에 오타가있을 때 이 동작이 어떻게 심각한 문제를 일으킬 수 있는지 보셨을 것입니다. 프록시를 사용하면 객체 모양의 유효성 검사를 통해 이 문제를 방지 할 수 있습니다.

객체 모양은 객체에서 사용할 수있는 프로퍼티 및 메서드의 모음입니다. JavaScript 엔진은 객체 모양을 사용하여 코드를 최적화하고 종종 객체를 나타내는 클래스를 만듭니다. 객체가 항상 동일한 프로퍼티 및 메서드 (Object.preventExtensions() 메서드, Object.seal() 메서드 또는 Object.freeze() 메서드로 적용 할 수있는 동작)를 항상 가지고 있다고 가정할 수 있는 경우, 존재하지 않는 프로퍼티에 액세스하려는 시도에 오류가 발생하면 도움이될 수 있습니다. 프록시는 객체 모양 유효성 검사를 쉽게 만들수 있게 합니다.

프로퍼티 검증은 프로퍼티가 읽혀질 때만 발생해야 하기때문에 get Trap을 사용합니다. get Trap은 프로퍼티가 객체 상에 존재하지 않더라도 프로퍼티가 읽힐 때 호출되며 세개의 파라미터를 받습니다.

  1. trapTarget - 프로퍼티를 읽어내는 객체 (프록시의 타겟)
  2. key - 프로퍼티 키 (문자열 또는 Symbol)
  3. receiver - 조작이 발생된 오브젝트 (일반적으로 프록시)

이 파라미터는 set Trap의 파라미터와 유사하며, 눈에 띄는 차이점이 하나 있습니다. get Trap은 값을 쓰지 않기 때문에 value는 여기서 아무런 가치가 없습니다. Reflect.get() 메서드는 get Trap과 동일한 세개의 파라미터를 받아들이고 프로퍼티의 기본값을 반환합니다.

다음과 같이 get Trap과 Reflect.get()를 사용하여 프로퍼티가 대상에 없을때 에러를 발생시킬 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(trapTarget, key, receiver);
}
});
// 프로퍼티 추가는 잘 됩니다.
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 존재하지 않는 프로퍼티는 에러를 던집니다.
console.log(proxy.nme); // throws error

이 예제에서, get Trap은 프로퍼티 읽기 연산을 가로 챕니다. in 연산자는 프로퍼티가 이미 receiver에 존재 하는지를 결정하는데 사용됩니다. receiverreceiverhas Trap을 가진 프록시인 경우에 trapTarget 대신에 in과 함께 사용됩니다. 다음 절에서 다루게될 타입입니다. 이 경우 trapTarget을 사용하면 has Trap을 회피하고 잠재적으로 잘못된 결과를 줄 수 있습니다. 프로퍼티가 없으면 오류가 발생하고 그렇지 않으면 기본 처리가 사용됩니다.

이 코드를 사용하면 proxy.name과 같은 새 프로퍼티는 아무 문제없이 추가하고 수정및 읽을 수 있습니다. 마지막 줄에는 오타가 있습니다. proxy.nmeproxy.name이어야합니다. nme가 프로퍼티로 존재하지 않으므로 오류가 발생합니다.

has Trap을 사용하여 프로퍼티 숨기기

in 연산자는 주어진 객체에 프로퍼티가 존재하는지 여부를 판단하고, 이름이나 Symbol과 일치하는 자체 프로퍼티나 프로토 타입 프로퍼티가 있으면 true를 반환합니다.

1
2
3
4
5
6
let target = {
value: 42;
}
console.log("value" in target); // true
console.log("toString" in target); // true

valuetoString 모두 object에 존재하기 때문에 in 연산자는 true를 리턴합니다. value 프로퍼티는 자신의 프로퍼티이고toString는 (Object에서 상속받은) 프로토 타입 프로퍼티입니다. 프록시를 사용하면 이 작업을 가로 채고hasTrap을 사용하여in`에 다른 값을 반환할 수 있습니다.

has Trap은 in 연산자가 사용될 때마다 호출됩니다. 호출될 때 두개의 파라미터가 has Trap에 전달됩니다.

  1. trapTarget - 프로퍼티의 read 객체 (프록시의 타겟)
  2. key - 체크 대상의 프로퍼티 키 (문자열 또는 Symbol)

Reflect.has() 메서드는 동일한 파라미터를 받아 들여 in 연산자에 대한 기본 응답을 반환합니다. has Trap과 Reflect.has()를 사용하면 일부 프로퍼티는 in의 동작을 변경하고 다른 프로퍼티는 기본 동작으로 되돌릴 수 있습니다. 예를 들어, value 프로퍼티를 숨기고 싶다고 가정 해보십시오. 이렇게 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
has(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.has(trapTarget, key);
}
}
});
console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true

proxyhas Trap은 keyvalue이면 false를 반환합니다. 그렇지 않으면 Reflect.has() 메서드를 호출하여 기본 동작을 사용합니다. 결과적으로 in 연산자는 value가 실제로 대상에 존재하더라도 value 프로퍼티에 대해 false를 반환합니다. name
toStringin 연산자와 함께 사용될 때 정확하게 true를 리턴합니다.

deleteProperty Trap을 사용하여 프로퍼티 삭제 방지

delete 연산자는 객체에서 프로퍼티를 제거하고, 성공하면 true를 실패하면 false를 반환합니다. strict 모드에서 deletenonconfigurable 프로퍼티를 지울때 에러를 던집니다. nonstrict 모드에서 delete는 단순히 false를 반환합니다. 다음 예제를 참고하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let target = {
name: "target",
value: 42
};
Object.defineProperty(target, "name", { configurable: false });
console.log("value" in target); // true
let result1 = delete target.value;
console.log(result1); // true
console.log("value" in target); // false
// Note: 아래 라인은 strict 모드에서 에러가 발생합니다.
let result2 = delete target.name;
console.log(result2); // false
console.log("name" in target); // true

value 프로퍼티는 delete 연산자를 사용하여 삭제되고 결과적으로 in 연산자는 세 번째 console.log () 호출에서 false를 반환합니다. nonconfigurable name 프로퍼티는 삭제될 수 없으므로 delete 연산자는 단순히 false를 반환합니다 (이 코드가 strict 모드로 실행되면 대신 에러가 발생합니다). 프록시에서 deleteProperty Trap을 사용하여 이 동작을 변경할 수 있습니다.

deleteProperty Trap은 객체 속성에서 delete 연산자가 사용될 때마다 호출됩니다. Trap에는 두개의 파라미터가 전달됩니다.

  1. trapTarget - 프로퍼티을 삭제해야 할 객체 (프록시의 대상)
  2. key - 삭제하는 프로퍼티 키 (문자열 또는 Symbol)

Reflect.deleteProperty() 메서드는 deleteProperty Trap의 기본 구현을 제공하고 동일한 두개의 파라미터를 받아들입니다. Reflect.deleteProperty()deleteProperty Trap을 결합하여 delete 연산자가 어떻게 동작 하는지를 변경할 수 있습니다. 예를 들어, value 프로퍼티를 삭제할수 없도록 할수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
deleteProperty(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.deleteProperty(trapTarget, key);
}
}
});
// Attempt to delete proxy.value
console.log("value" in proxy); // true
let result1 = delete proxy.value;
console.log(result1); // false
console.log("value" in proxy); // true
// Attempt to delete proxy.name
console.log("name" in proxy); // true
let result2 = delete proxy.name;
console.log(result2); // true
console.log("name" in proxy); // false

이 코드는 has Trap 예제와 매우 비슷합니다. deleteProperty Trap은 key"value"인지 확인하고, 그렇다면 false를 리턴합니다. 그렇지 않으면 Reflect.deleteProperty() 메서드를 호출하여 기본 동작을 계속합니다. 연산이 Trap 되었기 때문에 value 프로퍼티는 프록시를 통해 삭제할 수 없지만 name 프로퍼티는 예상대로 삭제됩니다. 이 접근법은 strict 모드에서 오류를 던지지 않고 프로퍼티를 삭제되지 않도록 보호하려는 경우에 특히 유용합니다.

Prototype 프록시 Trap

4 장에서는 ECMAScript 5의 Object.getPrototypeOf() 메서드를 보완하기 위해 ECMAScript 6에 추가한 Object.setPrototypeOf() 메서드를 소개했습니다. 프록시를 사용하면 setPrototypeOfgetPrototypeOf Trap을 통해 두 메서드의 실행을 가로챌 수 있습니다. 두 경우 모두Object 메서드는 프록시에서 해당 이름의 Trap을 호출하여 메서드의 동작을 변경할 수 있습니다.

프로토 타입 프록시와 관련된 두개의 Trap이 있고 각 Trap 유형과 관련된 메서드가 있습니다. setPrototypeOf Trap은 다음 파라미터를 받습니다.

  1. trapTarget - 프로토 타입을 설정해야하는 객체 (프록시의 대상)
  2. proto - 프로토 타입으로 사용하는 객체

이들은 Object.setPrototypeOf()Reflect.setPrototypeOf() 메서드에 전달되는 동일한 파라미터입니다. 반면에 getPrototypeOf Trap은 trapTarget 파라미터만 받습니다. 파라미터는 Object.getPrototypeOf()Reflect.getPrototypeOf() 메서드로 전달됩니다.

프로토 타입 프록시 Trap의 작동 방식

이 Trap들에는 몇가지 제한 사항이 있습니다. 첫째, getPrototypeOf Trap은 객체 또는 null을 반환해야하고, 다른 반환 값은 런타임 오류를 발생시킵니다. 반환값 검사는 Object.getPrototypeOf()가 항상 예상값을 반환하도록 보장합니다. 비슷하게, 연산이 성공하지 못하면
setPrototypeOf Trap의 반환값은 false이어야합니다. setPrototypeOffalse를 반환하면, Object.setPrototypeOf()는 에러를 던집니다. setPrototypeOffalse가 아닌 다른 값을 반환하면 Object.setPrototypeOf()는 연산이 성공했다고 가정합니다.

다음 예제는 항상 null을 반환하여 프록시의 프로토 타입을 숨기며 프로토 타입을 변경할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null;
},
setPrototypeOf(trapTarget, proto) {
return false;
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
// succeeds
Object.setPrototypeOf(target, {});
// throws error
Object.setPrototypeOf(proxy, {});

이 코드는 targetproxy의 동작 사이의 차이점을 강조합니다. Object.getPrototypeOf()target에 대한 값을 반환하는 동안
getPrototypeOf Trap이 호출되기 때문에 proxy에 대해 null을 리턴합니다. 비슷하게 Object.setPrototypeOf()target에서 사용될 때 성공하지만 setPrototypeOf Trap으로 인해 proxy에서 사용될 때 에러를 던집니다.

이 두 Trap의 기본 동작을 사용하려면 Reflect에서 해당 메서드를 사용해야 합니다. 예를 들어, 이 코드는 getPrototypeOf
setPrototypeOf Trap의 기본 동작을 구현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return Reflect.getPrototypeOf(trapTarget);
},
setPrototypeOf(trapTarget, proto) {
return Reflect.setPrototypeOf(trapTarget, proto);
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true
// succeeds
Object.setPrototypeOf(target, {});
// also succeeds
Object.setPrototypeOf(proxy, {});

이 예에서, getPrototypeOfsetPrototypeOf Trap이 기본 실행을 사용하기 위해 통과하고 있기 때문에 targetproxy를 교대로 사용할 수 있고 같은 결과를 얻을 수 있습니다. 이 예제는 몇가지 중요한 차이점 때문에 Object에서 같은 이름의 메서드보다는 Reflect.getPrototypeOf()Reflect.setPrototypeOf() 메서드를 사용하는 것이 중요합니다.

왜 두개의 Set 메서드일까요?

Reflect.getPrototypeOf()Reflect.setPrototypeOf()의 혼란스러운 부분은 Object.getPrototypeOf()
Object.setPrototypeOf() 메서드와 유사하게 보입니다. 두 세트의 메서드가 유사한 기능을 수행하지만, 두 메서드 사이에 뚜렷한 차이점이 있습니다.

우선, Object.getPrototypeOf()Object.setPrototypeOf()는 처음부터 개발자를 위해 만든 상위 수준의 기능입니다. Reflect.getPrototypeOf()Reflect.setPrototypeOf() 메서드는 개발자가 이전의 내부 전용 [[GetPrototypeOf]][[SetPrototypeOf]] 작업에 액세스할 수 있도록하는 하위 수준의 기능입니다. Reflect.getPrototypeOf() 메서드는 내부 [[GetPrototypeOf]] 연산의 래퍼 (일부 입력 유효성 검사 포함)입니다. Reflect.setPrototypeOf() 메서드와 [[SetPrototypeOf]]은 같은 관계입니다. Object의 해당 메서드는 [[GetPrototypeOf]][[SetPrototypeOf]]를 호출하지만 호출 전에 몇 단계를 수행하고 반환 값을 검사하여 동작 방법을 결정합니다.

Reflect.getPrototypeOf() 메서드는 파라미터가 객체가 아닌 경우 오류를 발생시키고 Object.getPrototypeOf()는 먼저 작업을 수행하기 전에 값을 객체에 강제 변환합니다. 각 메서드에 숫자를 전달하면 다른 결과가 나타납니다.

1
2
3
4
5
let result1 = Object.getPrototypeOf(1);
console.log(result1 === Number.prototype); // true
// error가 발생
Reflect.getPrototypeOf(1);

Object.getPrototypeOf() 메서드는 값을 Number 객체로 강제 변환한 다음 Number.prototype을 반환하기 때문에 1의 프로토 타입을 가져옵니다. Reflect.getPrototypeOf() 메서드는 값을 강제로 변환하지 않으며 1은 객체가 아니기 때문에 오류를 던집니다.

Reflect.setPrototypeOf() 메서드는 Object.setPrototypeOf() 메서드와 몇가지 다른 점이 있습니다. 첫째로, Reflect.setPrototypeOf()는 작업이 성공했는지를 나타내는 부울 값을 반환합니다. 성공하면 true 값이 반환되고 실패하면 false가 반환됩니다. Object.setPrototypeOf()는 실패하면 에러를 발생합니다.

“프로토 타입 프록시 Trap의 작동 방식”의 첫 번째 예제에서 보듯이 setPrototypeOf 프록시 Trap이 false를 반환하면
Object.setPrototypeOf()는 오류를 발생시킵니다. Object.setPrototypeOf() 메서드는 첫 번째 인자를 값으로 반환하므로setPrototypeOf 프록시 Trap의 기본 실행을 구현하는데 적합하지 않습니다. 다음 코드는 이러한 차이점을 보여줍니다.

1
2
3
4
5
6
7
8
let target1 = {};
let result1 = Object.setPrototypeOf(target1, {});
console.log(result1 === target1); // true
let target2 = {};
let result2 = Reflect.setPrototypeOf(target2, {});
console.log(result2 === target2); // false
console.log(result2); // true

이 예제에서 Object.setPrototypeOf()target1을 값으로 반환하지만 Reflect.setPrototypeOf()true를 반환합니다. 이 미묘한 차이는 매우 중요합니다. ObjectReflect에 중복된 메서드가 더많이 보일것입니다. 항상 프록시 Trap 내에서 Reflect 메서드를 사용해야합니다.

두 메서드는 프록시에서 사용될 때 getPrototypeOfsetPrototypeOf 프록시 Trap을 호출합니다.

Object 확장 Trap

ECMAScript 5는 Object.preventExtensions()Object.isExtensible() 메서드를 통해 객체 확장성 수정을 추가했으며 ECMAScript 6을 사용하면 프록시가 preventExtensionsisExtensible Trap을 통해 기본 객체에 대한 메서드 호출을 차단할 수 있습니다. 두 Trap 모두 메서드가 호출된 객체인 trapTarget이라는 단일 파라미터를 받습니다. isExtensible Trap은 객체가 확장 가능한지 여부를 나타내는 부울값을 반환해야 하고 preventExtensions Trap은 작업이 성공했는지를 나타내는 부울값을 반환해야합니다.

또한 기본 동작을 구현하기 위해 Reflect.preventExtensions()Reflect.isExtensible() 메서드가 있습니다. 둘 다 부울값을 반환하므로 해당 Trap에서 직접 사용할 수 있습니다.

기본 예제 두개

작업에서 객체 확장성 Trap을 확인 하기위해 isExtensiblepreventExtensions Trap에 대한 기본 동작을 구현하는 다음 코드를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return Reflect.preventExtensions(trapTarget);
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false

이 예제는 Object.preventExtensions()Object.isExtensible() 모두 proxy에서 target으로 정확하게 전달되는 것을 보여줍니다. 물론 동작을 바꿀 수도 있습니다. 예를 들어 Object.preventExtensions()가 프록시에서 성공하지 못하도록하려는 경우
preventExtensions Trap에서 false를 반환할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return false
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true

preventExtensions Trap이 false를 반환하기 때문에 Object.preventExtensions(proxy)에 대한 호출이 무시됩니다. 작업은 기본
target으로 전달되지 않으므로 Object.isExtensible()true를 반환합니다.

중복 확장 메서드

다시 한번, ObjectReflect에 중복된 메서드가 있다는 것을 눈치챘을 것입니다. 이 경우에는 더 유사합니다. Object.isExtensible()Reflect.isExtensible() 메서드는 비객체 값이 전달된 경우를 제외하고는 서로 비슷합니다. 이 경우 Reflect.isExtensible()이 오류를 발생하고 Object.isExtensible()은 항상 false를 반환합니다. 다음은 그 동작의 예제입니다.

1
2
3
4
5
let result1 = Object.isExtensible(2);
console.log(result1); // false
// error 발생
let result2 = Reflect.isExtensible(2);

이 제한은 Object.getPrototypeOf()Reflect.getPrototypeOf() 메서드의 차이와 유사합니다. 하위 레벨 기능의 메서드는 상위 레벨 기능보다 더 엄격한 오류 확인 기능을 가지고 있기 때문입니다.

Object.preventExtensions()Reflect.preventExtensions() 메서드도 매우 비슷합니다. Object.preventExtensions() 메서드는 값이 객체가 아닌 경우에 파라미터로 전달된 값을 항상 반환합니다. 반대로 Reflect.preventExtensions() 메서드는 파라미터가 객체가 아닌 경우 오류를 발생시킵니다. 파라미터가 객체인 경우 Reflect.preventExtensions()는 작업이 성공하면 true를 반환하고 그렇지 않으면 false를 반환합니다.

1
2
3
4
5
6
7
8
9
let result1 = Object.preventExtensions(2);
console.log(result1); // 2
let target = {};
let result2 = Reflect.preventExtensions(target);
console.log(result2); // true
// throws error
let result3 = Reflect.preventExtensions(2);

여기서 Object.preventExtensions()2가 객체가 아니더라도 2를 통과 시킵니다. Reflect.preventExtensions() 메서드는 객체가 전달될 때 true를 반환하고 2를 전달하면 오류를 발생시킵니다.

프로퍼티 Descriptor Trap

ECMAScript 5의 가장 중요한 기능중 하나는 Object.defineProperty() 메서드를 사용하여 프로퍼티 속성을 정의하는 기능이었습니다. 이전 버전의 JavaScript에서는 Accessor 프로퍼티를 정의하거나, 속성을 Read-only로 만들거나, 속성을 Nonenumerable하게 만들 수있는 방법이 없었습니다. 이 모든 작업은 Object.defineProperty() 메서드를 사용하여 수행할 수 있으며 Object.getOwnPropertyDescriptor() 메서드를 사용하여 이러한 속성을 검색할 수 있습니다.

프록시를 사용하면 definePropertygetOwnPropertyDescriptor Trap을 각각 사용하여 Object.defineProperty()Object.getOwnPropertyDescriptor()에 대한 호출을 가로 채게할 수 있습니다. defineProperty Trap은 다음 파라미터를 받습니다.

  1. trapTarget - 프로퍼티을 정의 할 필요가있는 객체 (프록시의 대상)
  2. key - 프로퍼티의 문자열 또는 Symbol
  3. descriptor - 프로퍼티 설명 객체

defineProperty Trap은 작업이 성공하면 true를, 그렇지 않으면 false를 반환합니다. getOwnPropertyDescriptor Trap은
trapTargetkey만 받으며, Descriptor를 리턴해야합니다. 상응하는 Reflect.defineProperty()
Reflect.getOwnPropertyDescriptor() 메서드는 프록시 Trap과 동일한 파라미터를 받습니다. 다음은 각 Trap의 기본 동작을 구현하는 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
return Reflect.defineProperty(trapTarget, key, descriptor);
},
getOwnPropertyDescriptor(trapTarget, key) {
return Reflect.getOwnPropertyDescriptor(trapTarget, key);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
console.log(descriptor.value); // "proxy"

이 코드는 Object.defineProperty() 메서드를 사용하여 프록시에서 "name"이라는 프로퍼티를 정의합니다. 그런 다음 해당 프로퍼티의 Descriptor가 Object.getOwnPropertyDescriptor() 메서드에 의해 검색됩니다.

Object.defineProperty() 잠그기

defineProperty Trap은 조작이 성공했는지 여부를 나타내기 위해 부울 값을 리턴하도록 요구합니다. true가 리턴되면, Object.defineProperty()는 평소대로 성공합니다; false가 리턴되면 Object.defineProperty()는 에러를 발생시킵니다. 이 기능을 사용하여 Object.defineProperty() 메서드가 정의할 수있는 프로퍼티의 종류를 제한할 수 있습니다. 예를 들어 Symbol 프로퍼티가 정의되지 않도록하려면 key가 문자열인지 확인하고 그렇지 않으면 false를 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
if (typeof key === "symbol") {
return false;
}
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let nameSymbol = Symbol("name");
// error 발생
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});

defineProperty 프록시 Trap은 keySymbol 일 때 false를 리턴하고 그렇지 않으면 기본 동작을 진행합니다. namekey
하여 Object.defineProperty()를 호출하면, key가 문자열이기 때문에 메서드가 성공합니다. Object.defineProperty()
nameSymbol과 함께 호출되면 defineProperty Trap이 false를 반환하기 때문에 에러가 발생합니다.

또한 Reflect.defineProperty() 메서드를 호출하지 않고 true를 리턴함으로써 Object.defineProperty()가 자동으로 실패하도록 할 수 있습니다. 실제로 프로퍼티를 정의하지 않는 동안 오류가 표시되지 않습니다.

Descriptor 객체 제한 사항

Object.defineProperty()Object.getOwnPropertyDescriptor() 메서드를 사용할 때 일관된 동작을 보장하기 위해
defineProperty Trap에 전달된 Descriptor 객체가 정규화됩니다. getOwnPropertyDescriptor Trap에서 반환된 객체는 같은 이유로 항상 유효성이 검사됩니다.

어떤 객체가 Object.defineProperty() 메서드의 세 번째 파라미터로 전달 되더라도, enumerable, configurable, value, writable, getset 속성들만 defineProperty Trap로 전달된 Descrptor 객체에 있을수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
console.log(descriptor.value); // "proxy"
console.log(descriptor.name); // undefined
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy",
name: "custom"
});

여기서, Object.defineProperty()는 세 번째 파라미터에 비표준 name 프로퍼티를 가지고 호출됩니다. defineProperty Trap이 호출되면, descriptor 객체는 name 프로퍼티를 갖지 않고 value 프로퍼티는 갖습니다. 왜냐하면 descriptorObject.defineProperty() 메서드에 전달된 실제 세 번째 파라미터에 대한 참조가 아니라 허용 가능한 프로퍼티만을 포함하는 새로운 객체이기 때문입니다. Reflect.defineProperty() 메서드는 또한 Descriptor의 비표준 특성을 무시합니다.

getOwnPropertyDescriptor Trap은 반환값이 null, undefined 또는 객체가되도록 약간 다른 제한이 있습니다. 객체가 반환되면 객체의 자체 프로퍼티로 enumerable, configurable, value, writable, getset 만 허용됩니다. 아래 코드와 같이 허용되지 않는 자체 프로퍼티를 가진 객체를 반환하면 오류가 발생합니다.

1
2
3
4
5
6
7
8
9
10
let proxy = new Proxy({}, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: "proxy"
};
}
});
// error 발생
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");

프로퍼티 Descriptor에서 name 프로퍼티를 사용할 수 없으므로 Object.getOwnPropertyDescriptor()가 호출되면
getOwnPropertyDescriptor 반환값은 오류를 발생시킵니다. 이 제약은, Object.getOwnPropertyDescriptor()에 의해 돌려 주어지는 값이, 프록시상에서의 사용에 관계없이 항상 신뢰할 수있는 구조를 가지는 것을 보증합니다.

중복된 Descriptor 메서드

다시 한번, ECMAScript 6는 Object.defineProperty()Object.getOwnPropertyDescriptor() 메서드가 Reflect.defineProperty()Reflect.getOwnPropertyDescriptor() 메서드와 동일한 기능을 수행하는 것처럼 혼동을 불러 일으키는 유사한 메서드를 가지고 있습니다. 이 장의 앞 부분에서 논의된 다른 메서드와 마찬가지로, 이들은 미묘하지만 중요한 차이점이 있습니다.

defineProperty() 메서드

Object.defineProperty()Reflect.defineProperty() 메서드는 반환 값을 제외하고 완전히 동일합니다. Object.defineProperty() 메서드는 첫 번째 피라미터를 반환하고 Reflect.defineProperty()는 작업이 성공하면 true를 반환하고 그렇지 않으면 false를 반환합니다.

1
2
3
4
5
6
7
8
9
let target = {};
let result1 = Object.defineProperty(target, "name", { value: "target "});
console.log(target === result1); // true
let result2 = Reflect.defineProperty(target, "name", { value: "reflect" });
console.log(result2); // true

target에서 Object.defineProperty()가 호출되면 반환값은 target입니다. Reflect.defineProperty()target에서 호출되면 반환값은 연산이 성공했음을 나타내는 true입니다. defineProperty 프록시 Trap은 반환될 부울 값을 필요로하기 때문에 필요할 때
Reflect.defineProperty()를 사용하여 기본 동작을 구현하는 것이 좋습니다.

getOwnPropertyDescriptor() 메서드

Object.getOwnPropertyDescriptor() 메서드는 Primitive 값이 전달될 때 첫 번째 파라미터를 객체로 강제 변환한 다음 작업을 계속합니다. 반면에 첫 번째 파라미터가 Primitive 값이면 Reflect.getOwnPropertyDescriptor() 메서드는 오류를 발생시킵니다. 다음은 이 두 가지를 보여주는 예입니다.

1
2
3
4
5
let descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1); // undefined
// throws an error
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");

Object.getOwnPropertyDescriptor() 메서드는 2를 객체로 강제변환하고 객체에는 name 프로퍼티가 없기 때문에 undefined를 반환합니다. 주어진 이름을 가진 프로퍼티가 객체에서 발견되지 않을때의 메서드 표준 동작입니다. 그러나 Reflect.getOwnPropertyDescriptor()가 호출되면 해당 메서드가 첫 번째 파라미터에 대한 Primitive 값을 허용하지 않기 때문에 오류가 즉시 발생합니다.

ownKeys Trap

ownKeys 프록시 Trap은 내부 메서드 [[OwnPropertyKeys]]를 가로 채고, 여러분이 값의 Array를 반환함으로써 동작을 오버라이드할 수 있도록합니다. 이 ArrayObject.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols()Object.assign()의 네 가지 메서드를 사용합니다. Object.assign() 메서드는 Array를 사용하여 복사할 프로퍼티를 결정합니다.

ownKeys Trap의 기본 동작은 Reflect.ownKeys() 메서드에 의해 구현되고 문자열과 Symbol을 포함한 모든 고유한 프로퍼티 키의 Array를 반환합니다. Object.getOwnProperyNames() 메서드 및 Object.keys() 메서드는 Array에서 Symbol을 필터링하고 결과를 반환하며 Object.getOwnPropertySymbols()Array에서 문자열을 필터링하여 결과를 반환합니다. Object.assign() 메서드는 문자열과 Symbol이 모두 포함된 Array를 사용합니다.

ownKeys Trap은 대상을 하나의 파라미터로 받고, 항상 Array이나 유사 Array와 같은 객체를 반환해야합니다; 그렇지 않으면 오류가 발생합니다. OwnKeys Trap을 사용하면, 예를 들어, Object.getOwnPropertyNames(), Object.getOwnPropertySymbols(), Object.getOwnPropertyNames(), 또는 Object.assign() 메서드가 사용됩니다. JavaScript에서 필드가 비공개임을 나타내는 일반적인 표기법인 밑줄 문자(underscore)로 시작하는 프로퍼티 이름은 포함하지 않으려는 경우로 가정하여 ownKeys Trap을 사용하여 다음과 같이 키를 걸러낼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let proxy = new Proxy({}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => {
return typeof key !== "string" || key[0] !== "_";
});
}
});
let nameSymbol = Symbol("name");
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy);
symbols = Object.getOwnPropertySymbols(proxy);
console.log(names.length); // 1
console.log(names[0]); // "name"
console.log(keys.length); // 1
console.log(keys[0]); // "name"
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"

이 예제는 Reflect.ownKeys()를 먼저 호출하여 대상의 기본 리스트를 얻는 ownKeys Trap을 사용합니다. 그런 다음 filter() 메서드는 문자열이며 밑줄 문자로 시작하는 키를 필터링하는데 사용됩니다. 그런 다음 proxy 객체에 세가지 프로퍼티가 추가됩니다.(name, _namenameSymbol). Object.getOwnPropertyNames()Object.keys()proxy에서 호출되면 name 프로퍼티만 반환됩니다. 비슷하게 Object.getOwnPropertySymbols()proxy에서 호출될 때 nameSymbol만 반환됩니다. _name 프로퍼티는 필터링되어서 어느 결과에도 나타나지 않습니다.

ownKeys Trap은 또한 for-in 루프에 영향을 미칩니다.이 루프는 Trap을 호출하여 루프 내부에서 사용할 키를 결정합니다.

apply와 construct Trap을 이용한 Function 프록시

모든 프록시 Trap 중, applyconstruct만이 프록시 대상으로 함수가 필요합니다. 3장에서 함수는 각각 new 연산자를 사용하지 않고 함수를 호출할 때 실행되는 [[Call]][[Construct]]라는 두가지 내부 메서드를 가지고 있음을 설명했습니다. applyconstruct Trap은 해당 내부 메서드를 재정의 하도록합니다. 함수가 new 없이 호출되면, apply Trap은 Reflect.apply()에 다음 파라미터가 필요합니다.

  1. trapTarget - 실행중인 함수 (프록시의 대상)
  2. thisArg - 호출 중 함수 안에있는 this의 값
  3. argumentsList - 함수에게 건네진 파라미터 Array

new를 사용하여 함수가 실행될 때 호출되는 construct Trap은 다음 파라미터를 받습니다.

  1. trapTarget - 실행중인 함수 (프록시의 대상)
  2. argumentsList - 함수에게 건네진 파라미터 Array

Reflect.construct() 메서드는 또한 이 두개의 파라미터를 받아들이며 newTarget이라는 선택적인 세 번째 파라미터를 받습니다. 주어진 경우, newTarget 파라미터는 함수 안에 new.target의 값을 지정합니다.

applyconstruct Trap은 모든 프록시 대상 함수의 동작을 완벽하게 제어합니다. 함수의 기본 동작을 모방하려면 다음과 같이 하면됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let target = function() { return 42 },
proxy = new Proxy(target, {
apply: function(trapTarget, thisArg, argumentList) {
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList);
}
});
// 함수를 대상으로하는 프록시는 함수처럼 보입니다.
console.log(typeof proxy); // "function"
console.log(proxy()); // 42
var instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true

이 예제에서 숫자 42를 반환하는 함수를 가지고 있습니다. 이 함수의 프록시는 applyconstruct Trap을 사용하여 그 동작을 각각 Reflect.apply()Reflect.construct() 메서드에 위임합니다. 결과적으로 프록시 함수는 typeof가 사용될 때 함수로 자신을 식별하는 것을 포함하여 대상 함수와 똑같이 동작합니다. 프록시는 42를 반환하기 위해 new 없이 호출합니다. 그리고 new를 호출하여 instance라는 객체를 만듭니다. instance 객체는 프로토 타입 체인을 사용하여 이 정보를 결정하기 때문에 proxytarget의 인스턴스로 간주됩니다. 프로토 타입 체인 조회는 이 프록시의 영향을 받지 않으므로 프록시와 대상이 JavaScript 엔진과 동일한 프로토 타입을 사용하는 것으로 보여집니다.

함수 파라미터 유효성 검사

applyconstruct Trap은 함수가 실행되는 방식을 변경하는 많은 가능성을 열어줍니다. 예를 들어 모든 파라미터가 특정 타입인지 확인하려고 한다고 가정해 보겠습니다. apply Trap에서 파라미터를 확인할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 모든 파라미터를 더합니다.
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0);
}
let sumProxy = new Proxy(sum, {
apply: function(trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
throw new TypeError("This function can't be called with new.");
}
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// error를 발생시킵니다.
console.log(sumProxy(1, "2", 3, 4));
// 역시 error를 발생시킵니다.
let result = new sumProxy();

이 예제는 apply Trap을 사용하여 모든 파라미터가 숫자임을 확인합니다. sum() 함수는 전달된 모든 파라미터를 더합니다. 숫자가 아닌 값이 전달되면 이 함수는 계속 작업을 시도하므로 예기치 않은 결과가 발생할 수 있습니다. sum()sumProxy() 프록시 안에 넣음으로써 이 코드는 함수 호출을 가로 채고 호출이 진행되기 전에 각 파라미터가 숫자인지 확인합니다. 안전을 위해, 코드는 construct Trap을 사용하여 new 함수를 호출할 수 없도록합니다.

함수를 new로 호출하고 파라미터가 숫자인지 확인해야합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Numbers(...values) {
this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
apply: function(trapTarget, thisArg, argumentList) {
throw new TypeError("This function must be called with new.");
},
construct: function(trapTarget, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.construct(trapTarget, argumentList);
}
});
let instance = new NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// throws error
NumbersProxy(1, 2, 3, 4);

여기서 construct Trap이 Reflect.construct() 메서드를 사용하여 입력값을 검증하고 새로운 인스턴스를 리턴할 때 apply Trap은 오류를 던집니다. 물론, 대신에 new.target을 사용하여 프록시없이 동일한 것을 수행할 수 있습니다.

new가 없는 생성자 호출

3 장에서 new.target 메타 프로퍼티를 소개했습니다. new.targetnew가 호출되는 함수에 대한 참조입니다. 즉, new.target의 값을 다음과 같이 검사하여 new를 사용하여 함수를 호출했는지 여부를 알 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Numbers(...values) {
if (typeof new.target === "undefined") {
throw new TypeError("This function must be called with new.");
}
this.values = values;
}
let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// throws error
Numbers(1, 2, 3, 4);

이 예제는 “함수 파라미터 유효성 검사” 섹션의 프록시를 사용하지 않는 예제와 유사하며, new를 사용하지 않고 Numbers가 호출될 때 에러를 던집니다. 유일한 목적이 new없이 함수를 호출하는 것을 방지하는 것이라면 이와 같은 코드를 작성하는 것은 프록시를 사용하는 것보다 훨씬 간단하며 바람직합니다. 그러나 때로는 동작을 수정해야하는 함수를 제어하지 못하는 경우가 있습니다. 이 경우 프록시를 사용하는 것이 좋습니다.

Numbers 함수가 수정할 수 없는 코드에 정의되었다고 가정해 보겠습니다. 이 코드는 new.target에 의존하고 있으며 여전히 함수를 호출할 때 체크를 피하기를 원합니다. new를 사용할 때의 동작은 이미 설정되어 있으므로 apply Trap을 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Numbers(...values) {
if (typeof new.target === "undefined") {
throw new TypeError("This function must be called with new.");
}
this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
apply: function(trapTarget, thisArg, argumentsList) {
return Reflect.construct(trapTarget, argumentsList);
}
});
let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]

NumbersProxy 함수는 new를 사용하지 않고 Numbers를 호출하고 new가 사용된 것처럼 행동하게합니다. 그렇게하기 위해, apply Trap은 Reflect.construct()를 호출하고 apply에 전달된 파라미터를 사용합니다. Numbers 내부의 new.targetNumbers 자체와 동일하며 에러는 발생하지 않습니다. 이것은 new.target을 변경하는 간단한 예제이지만, 더 직접적으로 할 수도 있습니다.

추상 기본 클래스 생성자 (Abstract Base Class Constructor) 재정의

한 걸음 더 나아가 new.target에 할당할 특정 값으로 Reflect.construct()의 세 번째 파라미터를 지정할 수 있습니다. 이는 함수가 추상 기본 클래스 생성자를 생성할 때(9 장에서 설명)와 같이 알려진 값에 대해 new.target을 검사할 때 유용합니다. 추상 기본 클래스 생성자에서, new.target은 이 예제에서와 같이 클래스 생성자 그 자체가 아닌 다른 것으로 예상됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
class Numbers extends AbstractNumbers {}
let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// throws error
new AbstractNumbers(1, 2, 3, 4);

new AbstractNumbers()가 호출되면, new.targetAbstractNumbers와 같기 때문에 에러를 발생 시킵니다. new.targetNumbers와 같기 때문에 new Numbers()는 여전히 작동합니다. 수동으로 new.target에 프록시를 할당함으로써 이 제약을 우회할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
let AbstractNumbersProxy = new Proxy(AbstractNumbers, {
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList, function() {});
}
});
let instance = new AbstractNumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]

AbstractNumbersProxyconstruct Trap을 사용하여 new AbstractNumbersProxy() 메서드에 대한 호출을 가로챕니다. 그런 다음, Reflect.construct() 메서드가 Trap의 파라미터와 함께 호출되고 빈 함수를 세 번째 파라미터로 추가합니다. 그 빈 함수는 생성자 내부의 new.target의 값으로 사용됩니다. new.targetAbstractNumbers와 같지 않기 때문에 오류가 발생하지 않고 생성자가 완전히 실행됩니다.

호출가능한 클래스 생성자

9 장에서는 클래스 생성자가 항상 new로 호출되어야 한다고 설명했다. 이는 클래스 생성자에 대한 내부 [[Call]] 메서드가 오류를 throw 하도록 지정 되었기 때문에 발생합니다. 그러나 프록시는 [[Call]] 메서드에 대한 호출을 가로챌 수 있습니다. 즉, 프록시를 사용하여 호출 가능한 클래스 생성자를 효과적으로 만들 수 있습니다. 예를 들어, 클래스 생성자가 new를 사용하지 않고 동작하게 하려면, apply Trap을 사용하여 새로운 인스턴스를 생성 할 수 있습니다. 다음은 몇 가지 샘플 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
constructor(name) {
this.name = name;
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
let me = PersonProxy("Nicholas");
console.log(me.name); // "Nicholas"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true

PersonProxy 객체는 Person 클래스 생성자의 프록시입니다. 클래스 생성자는 단지 함수이므로 프록시에서 사용될 때 함수처럼 작동합니다. apply Trap은 기본 동작을 재정의하고 대신 Person과 같은 trapTarget의 새로운 인스턴스를 반환합니다(이 예제에서 trapTarget을 사용하여 클래스를 수동으로 지정하지 않아도 된다는 것을 보여주기 위해 사용했습니다). argumentListSpread 연산자를 사용하여 각 파라미터가 개별적으로trapTarget에 전달됩니다.new를 사용하지 않고PersonProxy()를 호출하면Person의 인스턴스를 반환합니다(new없이Person()`을 호출하려고 하면 생성자는 여전히 오류를 던질 것입니다). 호출 가능한 클래스 생성자를 만드는 것은 프록시를 사용하는 경우에만 가능합니다.

취소 가능한 프록시

일반적으로 프록시가 생성되면 프록시는 대상에서 재배치할 수 없습니다. 이장의 모든 예제는 재배치할 수 없는 프록시를 사용했습니다. 그러나 더이상 사용할 수 없도록 프록시를 취소하려는 경우가 있을 수 있습니다. 보안을 위해 API를 통해 객체를 제공하고 언제든지 일부 기능에 대한 액세스를 차단할 수 있는 기능을 유지하려는 경우 프록시를 해지하는 것이 유용합니다.

Proxy.revocable() 메서드로 취소 가능한 프록시를 생성할 수 있습니다. 이 메서드는 대상 객체와 프록시 핸들러인 Proxy 생성자와 같은 파라미터를 사용합니다. 반환 값은 다음과 같은 프로퍼티를 가진 객체입니다.

  1. proxy - 취소할 수 있는 프록시 객체
  2. revoke - 프록시를 취소하기 위해서 호출하는 함수

revoke() 함수가 호출될 때, proxy를 통해 더 이상의 연산을 수행할 수 없습니다. 프록시 Trap을 발생시키는 방식으로 프록시 오브젝트와 상호 작용하려는 모든 시도는 오류를 발생시킵니다.

1
2
3
4
5
6
7
8
9
10
11
12
let target = {
name: "target"
};
let { proxy, revoke } = Proxy.revocable(target, {});
console.log(proxy.name); // "target"
revoke();
// error 발생
console.log(proxy.name);

이 예제는 취소 가능한 프록시를 만듭니다. Proxy.revocable() 메서드에 의해 반환된 객체에서 같은 이름의 프로퍼티에 proxyrevoke 변수를 할당하기 위해 Destructuring을 사용합니다. 그 후, proxy 객체는 취소가 불가능한(nonrevocable) 프록시 객체처럼 사용될 수 있습니다. 그래서 proxy.nametarget.name을 그대로 통과하기 때문에 "target"을 리턴합니다. 그러나 일단 revoke() 함수가 호출되면, 프록시는 더이상 함수가 아닙니다. proxy.name에 접근하려고 시도하면 에러가 발생하고, 프록시에서 Trap을 발생시키는 다른 동작도 마찬가지입니다.

Array의 문제점 해결하기

이장의 시작 부분에서 개발자가 ECMAScript 6 이전의 JavaScript에서 Array의 동작을 정확하게 모방할 수 없다는 것을 설명했습니다. 프록시와 리플렉션 API를 사용하면 프로퍼티가 추가되고 제거될 때 Built-in Array 타입과 같은 방식으로 동작하는 객체를 생성할 수 있습니다. 기억을 되새겨 아래 예제는 프록시가 Array를 모방하는데 도움이되는 동작을 보여주는 예입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"

위 예제에서 주의해야할 두가지 중요한 동작이 있습니다.

  1. colors[3]에 값이 할당되면 length 프로퍼티가 4로 증가합니다.
  2. length 프로퍼티가 2로 설정되면 Array의 마지막 두 항목이 삭제됩니다.

이 두가지 동작은 Built-in Array의 작동 방식을 정확하게 재현하기 위해 모방되어야하는 중요한 동작입니다. 다음 몇 섹션에서는 올바르게 Array 객체를 모방하는 방법을 설명합니다.

Array 색인 찾기

정수형 프로퍼티 키에 할당하는 것은 비 정수형 키와 다르게 취급되는 Array의 특별한 경우입니다. ECMAScript 6 사양에서는 프로퍼티 키가 Array 인덱스인지 확인하는 방법에 대한 지침을 제공합니다.

toString(ToUint32(P))P이고 ToUint32(P)2 ^ 32-1이 아니면 String 프로퍼티 이름 PArray 인덱스입니다.

이 연산자는 다음과 같이 JavaScript로 구현될 수 있습니다.

1
2
3
4
5
6
7
8
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}

toUint32() 함수는 사양에 설명된 알고리즘을 사용하여 주어진 값을 부호없는 32비트 정수로 변환합니다. isArrayIndex() 함수는 먼저 키를 uint32로 변환한 다음 비교를 수행하여 키가 Array 인덱스인지 여부를 확인합니다. 이러한 유틸리티 함수를 사용할 수 있으면 Built-in Array를 모방할 객체를 구현할 수 있습니다.

새로운 요소를 추가할 때 length 증가

설명한 두Array의 동작이 프로퍼티 할당에 의존한다는 것을 눈치 챘을 것입니다. 즉, 두가지 동작을 모두 수행하려면 set 프록시 Trap을 사용해야합니다. 먼저 length-1보다 큰 Array 인덱스가 사용되면 length 프로퍼티를 증가시켜 첫 번째 두가지 동작을 구현하는 예제를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
function createMyArray(length=0) {
return new Proxy({ length }, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// the special case
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
}
// always do this regardless of key type
return Reflect.set(trapTarget, key, value);
}
});
}
let colors = createMyArray(3);
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"

이 예제는 set 프록시 Trap을 사용하여 Array 인덱스의 설정을 가로챕니다. 키가 Array 인덱스인 경우 키는 항상 문자열로 전달되기 때문에 숫자로 변환됩니다. 그 숫자 값이 현재 length 프로퍼티보다 크거나 같으면 length 프로퍼티가 숫자 키보다 하나 더 업데이트됩니다 (위치 3의 항목 설정은 length가 4 여야 함을 의미합니다). 그 후에 프로퍼티를 설정하기위한 기본 동작은 Reflect.set()를 통해 사용됩니다. 프로퍼티가 지정된 값을 받기를 원하기 때문입니다.

최초의 커스텀 Arraylength가 3인 createMyArray()를 호출하여 생성되며, 그 세항목의 값은 바로 뒤에 추가됩니다. length 프로퍼티는 3번 위치에 "black"값이 할당될 때까지 정확히 3을 유지합니다. 그 시점에서 length는 4로 설정됩니다.

첫번째 동작이 작동하면 두 번째 동작으로 이동할 시간입니다.

length 줄이기에 대한 요소 삭제

모방을 위한 첫 번째 Array 동작은 Array 인덱스가 length 프로퍼티보다 크거나 같은 경우에만 사용됩니다. 두 번째 동작은 length 프로퍼티가 이전에 포함된 값보다 작은 값으로 설정되면 값을 줄이고 남은 Array 항목을 제거합니다. 이는 length 프로퍼티를 변경하는 것뿐만 아니라 존재하지 않는 모든 항목을 삭제하는 것을 포함합니다. 예를 들어, length가 4인 Arraylength를 2로 설정하면, 2와 3 위치의 항목은 삭제됩니다. 첫 번째 동작과 함께 set 프록시 Trap에서 이것을 수행 할 수 있습니다. 아래 예제는 앞의 예제를 업데이트한createMyArray 메서드 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
function createMyArray(length=0) {
return new Proxy({ length }, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// the special case
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
} else if (key === "length") {
if (value < currentLength) {
for (let index = currentLength - 1; index >= value; index\
--) {
Reflect.deleteProperty(trapTarget, index);
}
}
}
// 항상 키 타입에 관계없이 이 작업을 수행합니다.
return Reflect.set(trapTarget, key, value);
}
});
}
let colors = createMyArray(3);
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"

이 코드의 set 프록시 Trap은 나머지 객체를 정확하게 조정하기 위해 key"length"인지를 검사합니다. 이 경우 현재 length
Reflect.get()을 사용하여 먼저 검색되고 새 값과 비교됩니다. 새로운 값이 현재 length보다 작으면 for 루프는 더이상 사용할 수 없는 대상의 모든 프로퍼티를 삭제합니다. for 루프는 현재 Array의 크기(currentLength)에서 뒤로 이동하고 새로운 Array 크기(value)에 도달할 때까지 각 프로퍼티를 삭제합니다.

이 예제는 네가지 색상을 colors에 추가한 다음 length 프로퍼티를 2로 설정합니다. 이렇게 하면 위치 2와 3의 항목을 효과적으로 제거하므로 액세스하려고할 때 undefined를 반환합니다. length 프로퍼티는 정확하게 2로 설정되고 위치 0과 1의 아이템은 여전히 접근 가능합니다.

두가지 동작을 모두 구현하면 Built-in Array의 동작을 모방한 객체를 쉽게 만들수 있습니다. 그러나 함수를 사용하여 이렇게하는 것은 이 동작을 캡슐화하는 클래스를 만드는 것보다 바람직하지 않습니다. 따라서 다음 단계는 이 함수를 클래스로 구현하는 것입니다.

MyArray 클래스 구현

프록시를 사용하는 클래스를 만드는 가장 간단한 방법은 클래스를 평소대로 정의한 다음 생성자에서 프록시를 반환하는 것입니다. 이렇게하면 클래스가 인스턴스화될 때 반환되는 객체는 인스턴스가 아닌 프록시가됩니다. (인스턴스는 생성자 내의 `this ‘값입니다.) 인스턴스는 프록시의 대상이되고 프록시는 인스턴스인 것처럼 반환됩니다. 인스턴스는 완전히 비공개이며 직접 액세스할 수는 없지만 프록시를 통해 간접적으로 액세스할 수 있습니다.

다음은 클래스 생성자에서 프록시를 반환하는 간단한 예입니다.

1
2
3
4
5
6
7
8
class Thing {
constructor() {
return new Proxy(this, {});
}
}
let myThing = new Thing();
console.log(myThing instanceof Thing); // true

이 예제에서 Thing 클래스는 생성자에서 프록시를 반환합니다. 프록시 대상은 this이며 프록시는 생성자에서 반환됩니다. 즉, Thing 생성자를 호출하여 myThing을 만들었지만 실제로 myThing은 프록시입니다. 프록시는 자신의 동작을 대상에 전달하기 때문에 myThing은 여전히 Thing 클래스로 간주되며 프록시는 Thing 클래스를 사용하는 모든 사람에게 완전히 투명합니다.

이를 염두에두고 상대적으로 간단한 방식으로 프록시를 사용하여 맞춤 Array 클래스를 만듭니다. 코드는 “length 줄이기에 대한 요소 삭제”절의 코드와 거의 같습니다. 동일한 프록시 코드가 사용되지만 이번에는 클래스 생성자 내부에 있습니다. 전체 예제는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
class MyArray {
constructor(length=0) {
this.length = length;
return new Proxy(this, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// the special case
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
} else if (key === "length") {
if (value < currentLength) {
for (let index = currentLength - 1; index >= value; i\
ndex--) {
Reflect.deleteProperty(trapTarget, index);
}
}
}
// always do this regardless of key type
return Reflect.set(trapTarget, key, value);
}
});
}
}
let colors = new MyArray(3);
console.log(colors instanceof MyArray); // true
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"

이 코드는 생성자에서 프록시를 반환하는 MyArray 클래스를 만듭니다. length 프로퍼티는 생성자에 추가되고 (전달된 값이나 기본값 0으로 초기화 됨) 프록시가 생성되어 반환됩니다. 이것은 colors 변수에 MyArray의 인스턴스를 대입하는 모양이 되며 Array의 주요 동작을 구현합니다.

클래스 생성자에서 프록시를 반환하는 것은 쉽지만 모든 인스턴스에 대해 새 프록시가 만들어 짐을 의미합니다. 그러나 모든 인스턴스가 하나의 프록시를 공유하는 방법이 있습니다. 프록시를 프로토 타입으로 사용할 수 있습니다.

프로토 타입으로 프록시 사용

프록시는 프로토 타입으로 사용될 수 있지만, 이장의 앞의 예제보다 약간 복잡합니다. 프록시가 프로토 타입일 때, 프록시 Trap은 기본 동작이 정상적으로 프로토 타입에 계속될 때만 호출되며, 프록시의 기능을 프로토 타입으로 제한합니다. 다음 예제를 살펴 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let target = {};
let newTarget = Object.create(new Proxy(target, {
// never called
defineProperty(trapTarget, name, descriptor) {
// would cause an error if called
return false;
}
}));
Object.defineProperty(newTarget, "name", {
value: "newTarget"
});
console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true

newTarget 객체는 프록시를 프로토 타입으로 하여 생성됩니다. target을 프록시 타겟으로 만들면 프록시가 투명하기 때문에 효과적으로 targetnewTarget의 프로토 타입으로 만듭니다. 이제, 프록시 Trap은 newTarget에 대한 연산이 target에서 일어날 연산을 통과할 때만 호출됩니다.

Object.defineProperty() 메서드는 newTarget에 호출되어 name이라는 자체 프로퍼티를 생성합니다. 객체상에 프로퍼티를 정의하는 것은 일반적으로 객체의 프로토 타입으로 계속되는 연산이 아니기 때문에 프록시상의 defineProperty Trap은 결코 호출되지 않고 name 프로퍼티는 자신의 프로퍼티로 newTarget에 추가됩니다.

프로토 타입으로 사용할 경우 프록시가 심각하게 제한되지만 여전히 유용한 몇가지 Trap이 있습니다.

프로토 타입에서 get Trap 사용하기

내부 [[Get]] 메서드가 호출되어 프로퍼티를 읽으면 연산은 먼저 자신의 프로퍼티를 찾습니다. 지정된 이름을 가진 자체 프로퍼티를 찾을 수 없는 경우 연산은 프로토 타입을 계속 진행하고 거기에서 프로퍼티를 찾습니다. 검사할 프로토 타입이 더이상 없을 때까지 프로세스가 계속됩니다.

이 프로세스 덕분에, get 프록시 Trap을 설정하면 주어진 이름의 자체 프로퍼티가 존재하지 않을 때마다 프로토 타입에서 Trap이 호출됩니다. get Trap을 사용하면 존재한다고 보장할 수 없는 프로퍼티에 액세스할 때 예기치 않은 동작을 방지할 수 있습니다. 단지 존재하지 않는 프로퍼티에 액세스하려고 할 때마다 오류가 발생하는 객체를 만들면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let target = {};
let thing = Object.create(new Proxy(target, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
}));
thing.name = "thing";
console.log(thing.name); // "thing"
// error 발생
let unknown = thing.unknown;

이 코드에서, thing 객체는 프록시를 프로토 타입으로 하여 생성됩니다. get Trap은 주어진 키가 thing 객체에 존재하지 않은 호출에 대해 에러를 발생시킵니다. thing.name을 읽을 때, 프로퍼티가 thing에 존재하기 때문에 연산은 결코 프로토 타입에서 get Trap을 호출하지 않습니다. get Trap은 존재하지 않는 thing.unknown 프로퍼티에 접근할 때에만 호출됩니다.

마지막 줄이 실행될 때 unknownthing의 자체 속성이 아니므로 연산은 프로토 타입으로 계속됩니다. 그리고 get Trap은 오류를 던집니다. 이러한 유형의 동작은 JavaScript에서 매우 유용할 수 있습니다. JavaScript는 알려지지 않은 프로퍼티는 오류를 발생(다른 언어에서처럼)하는 대신 undefined를 자동으로 반환합니다.

이 예제에서 trapTargetreceiver는 다른 객체라는 것을 이해하는 것이 중요합니다. 프록시가 프로토 타입으로 사용될 때, trapTarget은 프로토 타입 객체 자체이고 receiver는 인스턴스 객체입니다. 이 경우 trapTargettarget과 같고 receiverthing과 같습니다. 이렇게하면 원래 프록시 대상과 작업을 수행할 대상에 모두 액세스할 수 있습니다.

프로토 타입에서 set Trap 사용하기

내부 [[Set]] 메서드는 자체 프로퍼티를 확인한 다음 필요에 따라 프로토 타입을 계속 진행합니다. 오브젝트 프로퍼티에 값을 할당하면, 같은 이름의 프로퍼티에 값이 할당됩니다. 지정된 이름의 프로퍼티가 없는 경우 연산은 프로토 타입으로 계속 진행됩니다. 까다로운 부분은 할당 작업이 프로토 타입으로 계속 되더라도 해당 프로퍼티에 값을 할당하면 해당 이름의 프로퍼티가 프로토 타입에 있는지 여부에 관계없이 기본적으로 프로토 타입이 아닌 인스턴스에 대한 프로퍼티가 만들어집니다.

프로토 타입에setTrap이 호출될 때와 그렇지 않을 때를 더 잘 이해하려면, 기본 동작을 보여주는 다음 예제를 살펴보십시오.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let target = {};
let thing = Object.create(new Proxy(target, {
set(trapTarget, key, value, receiver) {
return Reflect.set(trapTarget, key, value, receiver);
}
}));
console.log(thing.hasOwnProperty("name")); // false
// triggers the `set` proxy trap
thing.name = "thing";
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true
// does not trigger the `set` proxy trap
thing.name = "boo";
console.log(thing.name); // "boo"

이 예제에서, target은 자신의 프로퍼티가 없이 시작됩니다. thing 객체는 새로운 프로퍼티의 생성을 위한 set Trap을 정의하는 프록시를 프로토 타입으로 가지고 있습니다. thing.name에 값으로 "thing"이 지정되면 thingname이라는 자체 프로퍼티가 없으므로set 프록시 Trap이 호출됩니다. set Trap 내에서 trapTargettarget과 같고 receiverthing과 같습니다. 연산은 궁극적으로 thing에 새로운 프로퍼티를 만들어야하며, 다행히 Reflect.set()는 네 번째 파라미터로 receiver를 전달하면 이 기본 동작을 구현합니다.

name 프로퍼티가 thing에 생성되면 thing.name을 다른 값으로 설정해도 더이상 set 프록시 Trap을 호출하지 않습니다. 이 시점에서, name은 자체 프로퍼티이므로 [[Set]] 연산은 프로토 타입으로 계속되지 않습니다.

프로토 타입에서 has Trap 사용하기

has Trap은 객체에서 in 연산자의 사용을 가로챈다는 것을 설명했습니다. in 연산자는 먼저 주어진 이름을 가진 객체 자신의 프로퍼티를 검색합니다. 이름이 같은 자체 프로퍼티가 없으면 연산이 프로토 타입으로 계속 진행됩니다. 프로토 타입에 자체 프로퍼티가 없으면 프로토
타입을 찾거나 검색할 프로토 타입이 더 이상 없을 때까지 프로토 타입 체인을 통해 검색이 계속됩니다.

따라서 has Trap은 검색이 프로토 타입 체인에서 프록시 객체에 도달할 때만 호출됩니다. 프로토 타입으로 프록시를 사용하는 경우 지정된 이름의 자체 프로퍼티가 없을 때만 발생합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let target = {};
let thing = Object.create(new Proxy(target, {
has(trapTarget, key) {
return Reflect.has(trapTarget, key);
}
}));
// triggers the `has` proxy trap
console.log("name" in thing); // false
thing.name = "thing";
// does not trigger the `has` proxy trap
console.log("name" in thing); // true

이 코드는 thing 프로토 타입에 has 프록시 Trap을 만듭니다. has Trap은 in 연산자가 사용될 때 프로토 타입을 자동으로 검색하기 때문에 getset Trap처럼 receiver 객체를 통과하지 않습니다. 대신, has Trap은 target과 같은 trapTarget에서만 작동해야합니다. 이 예제에서 처음으로 in 연산자가 사용되면, 프로퍼티 이름이 thing의 자체 프로퍼티로 존재하지 않기 때문에 has Trap이 호출됩니다. thing.name에 값이 주어지고 in 연산자가 다시 사용될 때 has에 자신의 프로퍼티 name을 찾은 후에 연산이 멈추기 때문에 has Trap이 호출되지 않습니다.

이 시점의 프로토 타입 예제는 Object.create() 메서드를 사용하여 생성된 객체를 중심으로 이루어졌습니다. 그러나 프록시를 프로토 타입으로 사용하는 클래스를 만들려면 프로세스가 좀 더 복잡합니다.

클래스의 프로토 타입으로서의 프록시

프로토 타입 프로퍼티가 non-writable이기 때문에 클래스를 프록시 prototype으로 사용하도록 직접 수정할 수 없습니다. 그러나 상속을 사용하여 프록시를 프로토 타입으로 사용하는 클래스를 약간 다르게 사용할 수 있습니다. 시작하려면 생성자 함수를 사용하여 ECMAScript 5 스타일 형식 정의를 만들어야합니다. 그런 다음 프로토 타입을 프록시로 덮어 쓸수 있습니다. 다음은 그 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
let thing = new NoSuchProperty();
// `get` 프록시 Trap으로 인해 오류가 발생합니다.
let result = thing.name;

NoSuchProperty 함수는 클래스가 상속하는 기반을 나타냅니다. 함수의 프로토 타입 프로퍼티에는 제한이 없으므로 프록시로 이를 덮어 쓸 수 있습니다. get Trap은 프로퍼티가 존재하지 않을 때 오류를 던지기 위해 사용됩니다. thing 객체는 NoSuchProperty 인스턴스로 생성되고 존재하지 않는 name 프로퍼티에 접근할 때 오류를 던집니다.

다음 단계는 NoSuchProperty를 상속받은 클래스를 만드는 것입니다. 9장에서 논의된 extends 문법을 사용하여 클래스 프로토 타입 체인에 프록시를 도입 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
// "wdth"가 존재하지 않기 때문에 에러가 발생합니다.
let area2 = shape.length * shape.wdth;

Square 클래스는 NoSuchProperty를 상속받습니다. 그래서 프록시는 Square 클래스의 프로토 타입 체인에 있습니다. shape 객체는Square의 새로운 인스턴스로 생성되고 lengthwidth라는 두개의 프로퍼티를 갖습니다. get 프록시 Trap이 결코 호출되지 않기 때문에 이들 프로퍼티의 값을 읽는 것은 성공합니다. shape에 존재하지 않는 프로퍼티(shape.wdth, 명백한 오타)에 접근할 때만 get 프록시 Trap 트리거가 발생하고 오류가 발생합니다.

이는 프록시가 shape의 프로토 타입 체인에 있음을 증명하지만 프록시가 shape의 직접 프로토 타입이 아니라는 점은 분명하지 않을 수 있습니다. 실제로, 프록시는 shape에서 프로토 타입 체인까지 두단계입니다. 앞의 예를 약간 변경하면 더 명확하게 알 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function NoSuchProperty() {
// empty
}
// 프로토 타입이 될 프록시에 대한 참조를 저장
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
NoSuchProperty.prototype = proxy;
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let shapeProto = Object.getPrototypeOf(shape);
console.log(shapeProto === proxy); // false
let secondLevelProto = Object.getPrototypeOf(shapeProto);
console.log(secondLevelProto === proxy); // true

이 버전의 코드는 프록시를 proxy라는 변수에 저장하므로 나중에 쉽게 식별할 수 있습니다. shape의 프로토 타입은 Shape.prototype이고 프록시가 아닙니다. 그러나 Shape.prototype의 프로토 타입은 NoSuchProperty에서 상속된 프록시입니다.

상속은 프로토 타입 체인에 또 다른 단계를 추가합니다. 프록시에 get Trap을 호출할 때 발생할 수 있는 작업으로 인해 하나의 추가 단계가 필요하기 때문에 중요합니다. Shape.prototype에 프로퍼티가 있으면 다음과 같이 get 프록시 Trap이 호출되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
let area2 = shape.getArea();
console.log(area2); // 12
// "wdth"가 존재하지 않기 때문에 에러가 발생됩니다.
let area3 = shape.length * shape.wdth;

여기에서 Square 클래스는 getArea() 메서드를 가지고 있습니다. getArea() 메서드는 Square.prototype에 자동으로 추가되므로
shape.getArea()가 호출되면 getArea() 메서드에 대한 검색은 shape 인스턴스에서 시작한 후 프로토 타입으로 진행됩니다. 프로토 타입에 getArea ()가 있기 때문에 검색이 멈추고 프록시는 호출되지 않습니다. getArea()가 호출됐을때 오류를 잘못 던지는 것을 원하지 않기 때문에 실제로 이 상황에서 우리가 원하는 동작입니다.

프로토 타입 체인에 프록시가있는 클래스를 만드는 데 약간의 추가 코드가 필요하지만 이러한 기능이 필요한 경우에는 노력할 가치가 있습니다.

요약

ECMAScript 6 이전에는 특정 객체(예 : Array)가 개발자가 복제할 수없는 비표준 동작을 표현했습니다. 프록시가 그것을 바꿉니다. 프록시를 사용하면 여러 저수준 JavaScript 작업에 대한 비표준 동작을 정의할 수 있으므로 프록시 Trap을 통해 Built-in JavaScript 객체의 모든 동작을 가로챌 수 있습니다. 이러한 Trap은 in 연산자를 사용하는 것처럼 다양한 작업이 수행되는 뒤에서 호출됩니다.

개발자가 각 프록시 Trap에 대한 기본 동작을 구현할 수 있도록 ECMAScript 6에 리플렉션 API도 도입되었습니다. 각 프록시 Trap에는 Reflect 객체에 동일한 이름의 해당 메서드가 있으며 ECMAScript 6에 추가되었습니다. 프록시 Trap 및 리플렉션 API 메서드의 조합을 사용하면 일부 동작만 필터링하여 특정 조건에서만 다르게 동작하도록 할 수 있으며 Built-in 동작을 기본값으로 사용할 수 있습니다.

취소 가능한 프록시는 revoke() 함수를 사용하여 효과적으로 비활성화 할 수있는 특수 프록시입니다. revoke() 함수는 프록시의 모든 기능을 종료하므로 revoke()가 호출된 후 프록시의 프로퍼티와 상호 작용하려는 동작은 오류가 발생합니다. 취소 가능한 프록시는 Third-party 개발자가 지정된 시간 동안 특정 객체에 액세스해야하는 응용 프로그램의 보안에 중요합니다.

프록시를 직접 사용하는 것이 가장 강력한 유스 케이스이지만 프록시를 다른 객체의 프로토 타입으로 사용할 수도 있습니다. 이 경우 효과적으로 사용할 수 있는 프록시 Trap 수가 크게 제한됩니다. 프로토 타입으로 사용될 때 프록시에서 get, sethas 프록시 Trap만 호출되어 사용가능 사례를 훨씬 더 좁힙니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://leanpub.com/understandinges6/read#leanpub-auto-proxies-and-the-reflection-api]

참고

공유하기