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 객체를 모방할 수 없는 방식으로 동작합니다. Array의 length
프로퍼티는 특정 Array 항목에 값을 할당할 때 영향을 받으며 length
프로퍼티를 수정하여 Array 항목을 수정할 수 있습니다.
|
|
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-in
과Object.keys()
가 객체의 속성을 열거하는 방법을 변경하기 위해 고안된enumerate
라는 추가 Trap이 있습니다. 그러나 ECMAScript 7 (ECMAScript 2016이라고도 함)에서는 구현 중에 어려움이 발견되어enumerate
Trap이 제거되었습니다.enumerate
Trap 더 이상 JavaScript 환경에 존재하지 않으므로 이 장에서 다루지 않습니다.
간단한 프록시 생성
Proxy
생성자를 사용하여 프록시를 만들 때, 두 개의 파라미터, 즉 대상과 핸들러를 넘깁니다. 핸들러는 하나 이상의 Trap을 정의하는 객체입니다. 프록시는 해당 작업에 대해 Trap이 정의된 경우를 제외하고 모든 작업에 대해 기본 동작을 사용합니다. 간단한 forwarding 프록시를 만들려면 Trap 없이 핸들러를 사용할 수 있습니다.
|
|
이 예제에서 proxy
는 모든 작업을 target
에 직접 전달합니다. "proxy"
가 proxy.name
프로퍼티에 할당되면 name
이 target
에 생성됩니다. 프록시 자체가 이 프로퍼티를 저장하지 않습니다. 이것은 단순히 작업을 target
으로 전달하는 것입니다. 비슷하게, proxy.name
과 target.name
의 값은 target.name
을 참조하기 때문에 동일합니다. 즉, target.name
을 새로운 값으로 설정하면
proxy.name
도 같은 변경을 반영합니다. 그리고 Trap이 없는 프록시는 별로 흥미롭지 않으므로 Trap을 정의하면 어떻게 될까요?
set
Trap을 사용하여 프로퍼티 검증하기
프로퍼티 값이 숫자여야 하는 객체를 만들고 싶다고 가정 해보겠습니다. 즉, 객체에 추가된 모든 새로운 프로퍼티에 대해 유효성 검사를 해야하며 값이 숫자가 아닌 경우 오류가 발생되어야 합니다. 이것을 달성하기 위해, 값을 설정하는 기본 동작을 무시하는 set
Trap을 정의 할 수 있습니다. set
Trap은 네개의 파라미터를 받습니다.
trapTarget
- 프로퍼티를 수신하는 객체 (프록시의 타겟)key
- 프로퍼티 키 (문자열 또는 Symbol)value
- 프로퍼티 값receiver
- 조작이 발생된 오브젝트 (일반적으로 프록시)
Reflect.set()
은 set
Trap에 대응하는 리플렉션 메서드이며, 이 연산의 기본 동작입니다. Reflect.set()
메서드는 set
프록시 Trap과 동일한 네개의 파라미터를 받아 Trap 내부에서 메서드를 사용하기 쉽게 만듭니다. Trap은 프로퍼티가 설정되면 true
를 반환하고 그렇지 않으면 false
를 반환합니다 (Reflect.set()
메서드는 작업이 성공했는지 여부에 따라 올바른 값을 반환합니다).
프로퍼티의 값을 검증하기 위해서는 set
Trap을 사용하고 입력된 value
를 검사 해야합니다. 다음은 그 예제입니다.
|
|
이 코드는 target
에 추가되는 새로운 프로퍼티의 값을 확인하는 프록시 Trap을 정의합니다. proxy.count=1
이 실행되면 set
Trap이 호출됩니다. trapTarget
값은 target
과 같고 key
는 count
, value
는 1
이며 receiver
(이 예제에서는 사용되지 않음)는proxy
입니다. target
에 count
라는 이름의 기존 프로퍼티가 없으므로 프록시는 isNaN()
에 전달하여 값의 유효성을 검사합니다. 결과가 NaN
의 경우, 숫자값이 아니기 때문에 에러가 발생됩니다. 이 코드는 count
를 1
로 설정하기 때문에, 프록시는 새 프로퍼티를 추가하기 위해 Trap에 전달된 네개의 파라미터를 사용하여 Reflect.set()
을 호출합니다.
proxy.name
에 문자열이 지정되어도 작업은 성공적으로 완료됩니다. target
은 이미 name
프로퍼티를 가지고 있기 때문에, trapTarget.hasOwnProperty()
메서드를 호출함으로써 유효성 체크에서 그 프라퍼티를 생략합니다. 이렇게 하면 기존의 비숫자 프로퍼티 값이 계속 지원됩니다.
그러나 proxy.anotherName
에 문자열이 할당되면 오류가 발생합니다. anotherName
프로퍼티는 target
에 없으므로 해당 값의 유효성을 검사 해야합니다. 유효성 검사에서 "proxy"
가 숫자 값이 아니기 때문에 오류가 발생합니다.
프로퍼티가 쓰여질 때 set
프록시 Trap이 가로챌수 있고, get
프록시 Trap은 프로퍼티가 읽혀질 때 가로 챌 수있습니다.
get Trap을 사용하여 객체 모양 유효성 검사
때때로 JavaScript의 흥미롭고 혼란스러운 부분 중 하나는 존재하지 않는 프로퍼티를 읽는 것이 오류를 발생시키지 않는다는 것입니다. 대신 다음 예와 같이 프로퍼티 값에 undefined
값이 사용됩니다.
|
|
대부분의 다른 언어에서 target.name
을 읽으려고 하면 프로퍼티가 존재하지 않기 때문에 오류가 발생합니다. 그러나 JavaScript는
target.name
프로퍼티 값에 undefined
를 사용합니다. 대규모 코드 기반에서 작업한 적이 있다면, 특히 프로퍼티 이름에 오타가있을 때 이 동작이 어떻게 심각한 문제를 일으킬 수 있는지 보셨을 것입니다. 프록시를 사용하면 객체 모양의 유효성 검사를 통해 이 문제를 방지 할 수 있습니다.
객체 모양은 객체에서 사용할 수있는 프로퍼티 및 메서드의 모음입니다. JavaScript 엔진은 객체 모양을 사용하여 코드를 최적화하고 종종 객체를 나타내는 클래스를 만듭니다. 객체가 항상 동일한 프로퍼티 및 메서드 (Object.preventExtensions()
메서드, Object.seal()
메서드 또는 Object.freeze()
메서드로 적용 할 수있는 동작)를 항상 가지고 있다고 가정할 수 있는 경우, 존재하지 않는 프로퍼티에 액세스하려는 시도에 오류가 발생하면 도움이될 수 있습니다. 프록시는 객체 모양 유효성 검사를 쉽게 만들수 있게 합니다.
프로퍼티 검증은 프로퍼티가 읽혀질 때만 발생해야 하기때문에 get
Trap을 사용합니다. get
Trap은 프로퍼티가 객체 상에 존재하지 않더라도 프로퍼티가 읽힐 때 호출되며 세개의 파라미터를 받습니다.
trapTarget
- 프로퍼티를 읽어내는 객체 (프록시의 타겟)key
- 프로퍼티 키 (문자열 또는 Symbol)receiver
- 조작이 발생된 오브젝트 (일반적으로 프록시)
이 파라미터는 set
Trap의 파라미터와 유사하며, 눈에 띄는 차이점이 하나 있습니다. get
Trap은 값을 쓰지 않기 때문에 value
는 여기서 아무런 가치가 없습니다. Reflect.get()
메서드는 get
Trap과 동일한 세개의 파라미터를 받아들이고 프로퍼티의 기본값을 반환합니다.
다음과 같이 get
Trap과 Reflect.get()
를 사용하여 프로퍼티가 대상에 없을때 에러를 발생시킬 수 있습니다.
|
|
이 예제에서, get
Trap은 프로퍼티 읽기 연산을 가로 챕니다. in
연산자는 프로퍼티가 이미 receiver
에 존재 하는지를 결정하는데 사용됩니다. receiver
는 receiver
가 has
Trap을 가진 프록시인 경우에 trapTarget
대신에 in
과 함께 사용됩니다. 다음 절에서 다루게될 타입입니다. 이 경우 trapTarget
을 사용하면 has
Trap을 회피하고 잠재적으로 잘못된 결과를 줄 수 있습니다. 프로퍼티가 없으면 오류가 발생하고 그렇지 않으면 기본 처리가 사용됩니다.
이 코드를 사용하면 proxy.name
과 같은 새 프로퍼티는 아무 문제없이 추가하고 수정및 읽을 수 있습니다. 마지막 줄에는 오타가 있습니다. proxy.nme
는 proxy.name
이어야합니다. nme
가 프로퍼티로 존재하지 않으므로 오류가 발생합니다.
has Trap을 사용하여 프로퍼티 숨기기
in
연산자는 주어진 객체에 프로퍼티가 존재하는지 여부를 판단하고, 이름이나 Symbol과 일치하는 자체 프로퍼티나 프로토 타입 프로퍼티가 있으면 true
를 반환합니다.
|
|
value
와 toString
모두 object
에 존재하기 때문에 in
연산자는 true
를 리턴합니다. value
프로퍼티는 자신의 프로퍼티이고toString
는 (Object에서 상속받은) 프로토 타입 프로퍼티입니다. 프록시를 사용하면 이 작업을 가로 채고
hasTrap을 사용하여
in`에 다른 값을 반환할 수 있습니다.
has
Trap은 in
연산자가 사용될 때마다 호출됩니다. 호출될 때 두개의 파라미터가 has
Trap에 전달됩니다.
trapTarget
- 프로퍼티의 read 객체 (프록시의 타겟)key
- 체크 대상의 프로퍼티 키 (문자열 또는 Symbol)
Reflect.has()
메서드는 동일한 파라미터를 받아 들여 in
연산자에 대한 기본 응답을 반환합니다. has
Trap과 Reflect.has()
를 사용하면 일부 프로퍼티는 in
의 동작을 변경하고 다른 프로퍼티는 기본 동작으로 되돌릴 수 있습니다. 예를 들어, value
프로퍼티를 숨기고 싶다고 가정 해보십시오. 이렇게 할 수 있습니다.
|
|
proxy
의 has
Trap은 key
가 value
이면 false
를 반환합니다. 그렇지 않으면 Reflect.has()
메서드를 호출하여 기본 동작을 사용합니다. 결과적으로 in
연산자는 value
가 실제로 대상에 존재하더라도 value
프로퍼티에 대해 false
를 반환합니다. name
과
toString
은 in
연산자와 함께 사용될 때 정확하게 true
를 리턴합니다.
deleteProperty Trap을 사용하여 프로퍼티 삭제 방지
delete
연산자는 객체에서 프로퍼티를 제거하고, 성공하면 true
를 실패하면 false
를 반환합니다. strict 모드에서 delete
는nonconfigurable
프로퍼티를 지울때 에러를 던집니다. nonstrict 모드에서 delete
는 단순히 false
를 반환합니다. 다음 예제를 참고하세요.
|
|
value
프로퍼티는 delete
연산자를 사용하여 삭제되고 결과적으로 in
연산자는 세 번째 console.log ()
호출에서 false
를 반환합니다. nonconfigurable name
프로퍼티는 삭제될 수 없으므로 delete
연산자는 단순히 false
를 반환합니다 (이 코드가 strict
모드로 실행되면 대신 에러가 발생합니다). 프록시에서 deleteProperty
Trap을 사용하여 이 동작을 변경할 수 있습니다.
deleteProperty
Trap은 객체 속성에서 delete
연산자가 사용될 때마다 호출됩니다. Trap에는 두개의 파라미터가 전달됩니다.
trapTarget
- 프로퍼티을 삭제해야 할 객체 (프록시의 대상)key
- 삭제하는 프로퍼티 키 (문자열 또는 Symbol)
Reflect.deleteProperty()
메서드는 deleteProperty
Trap의 기본 구현을 제공하고 동일한 두개의 파라미터를 받아들입니다. Reflect.deleteProperty()
와 deleteProperty
Trap을 결합하여 delete
연산자가 어떻게 동작 하는지를 변경할 수 있습니다. 예를 들어, value
프로퍼티를 삭제할수 없도록 할수 있습니다.
|
|
이 코드는 has
Trap 예제와 매우 비슷합니다. deleteProperty
Trap은 key
가 "value"
인지 확인하고, 그렇다면 false
를 리턴합니다. 그렇지 않으면 Reflect.deleteProperty()
메서드를 호출하여 기본 동작을 계속합니다. 연산이 Trap 되었기 때문에 value
프로퍼티는 프록시를 통해 삭제할 수 없지만 name
프로퍼티는 예상대로 삭제됩니다. 이 접근법은 strict 모드에서 오류를 던지지 않고 프로퍼티를 삭제되지 않도록 보호하려는 경우에 특히 유용합니다.
Prototype 프록시 Trap
4 장에서는 ECMAScript 5의 Object.getPrototypeOf()
메서드를 보완하기 위해 ECMAScript 6에 추가한 Object.setPrototypeOf()
메서드를 소개했습니다. 프록시를 사용하면 setPrototypeOf
와 getPrototypeOf
Trap을 통해 두 메서드의 실행을 가로챌 수 있습니다. 두 경우 모두Object
메서드는 프록시에서 해당 이름의 Trap을 호출하여 메서드의 동작을 변경할 수 있습니다.
프로토 타입 프록시와 관련된 두개의 Trap이 있고 각 Trap 유형과 관련된 메서드가 있습니다. setPrototypeOf
Trap은 다음 파라미터를 받습니다.
trapTarget
- 프로토 타입을 설정해야하는 객체 (프록시의 대상)proto
- 프로토 타입으로 사용하는 객체
이들은 Object.setPrototypeOf()
및 Reflect.setPrototypeOf()
메서드에 전달되는 동일한 파라미터입니다. 반면에 getPrototypeOf
Trap은 trapTarget
파라미터만 받습니다. 파라미터는 Object.getPrototypeOf()
및 Reflect.getPrototypeOf()
메서드로 전달됩니다.
프로토 타입 프록시 Trap의 작동 방식
이 Trap들에는 몇가지 제한 사항이 있습니다. 첫째, getPrototypeOf
Trap은 객체 또는 null
을 반환해야하고, 다른 반환 값은 런타임 오류를 발생시킵니다. 반환값 검사는 Object.getPrototypeOf()
가 항상 예상값을 반환하도록 보장합니다. 비슷하게, 연산이 성공하지 못하면
setPrototypeOf
Trap의 반환값은 false
이어야합니다. setPrototypeOf
가 false
를 반환하면, Object.setPrototypeOf()
는 에러를 던집니다. setPrototypeOf
가 false
가 아닌 다른 값을 반환하면 Object.setPrototypeOf()
는 연산이 성공했다고 가정합니다.
다음 예제는 항상 null
을 반환하여 프록시의 프로토 타입을 숨기며 프로토 타입을 변경할 수 없습니다.
|
|
이 코드는 target
과 proxy
의 동작 사이의 차이점을 강조합니다. Object.getPrototypeOf()
은 target
에 대한 값을 반환하는 동안
getPrototypeOf
Trap이 호출되기 때문에 proxy
에 대해 null
을 리턴합니다. 비슷하게 Object.setPrototypeOf()
는 target
에서 사용될 때 성공하지만 setPrototypeOf
Trap으로 인해 proxy
에서 사용될 때 에러를 던집니다.
이 두 Trap의 기본 동작을 사용하려면 Reflect
에서 해당 메서드를 사용해야 합니다. 예를 들어, 이 코드는 getPrototypeOf
및
setPrototypeOf
Trap의 기본 동작을 구현합니다.
|
|
이 예에서, getPrototypeOf
및 setPrototypeOf
Trap이 기본 실행을 사용하기 위해 통과하고 있기 때문에 target
과 proxy
를 교대로 사용할 수 있고 같은 결과를 얻을 수 있습니다. 이 예제는 몇가지 중요한 차이점 때문에 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()
는 먼저 작업을 수행하기 전에 값을 객체에 강제 변환합니다. 각 메서드에 숫자를 전달하면 다른 결과가 나타납니다.
|
|
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의 기본 실행을 구현하는데 적합하지 않습니다. 다음 코드는 이러한 차이점을 보여줍니다.
|
|
이 예제에서 Object.setPrototypeOf()
는 target1
을 값으로 반환하지만 Reflect.setPrototypeOf()
는 true
를 반환합니다. 이 미묘한 차이는 매우 중요합니다. Object
와 Reflect
에 중복된 메서드가 더많이 보일것입니다. 항상 프록시 Trap 내에서 Reflect
메서드를 사용해야합니다.
두 메서드는 프록시에서 사용될 때
getPrototypeOf
및setPrototypeOf
프록시 Trap을 호출합니다.
Object 확장 Trap
ECMAScript 5는 Object.preventExtensions()
및 Object.isExtensible()
메서드를 통해 객체 확장성 수정을 추가했으며 ECMAScript 6을 사용하면 프록시가 preventExtensions
및 isExtensible
Trap을 통해 기본 객체에 대한 메서드 호출을 차단할 수 있습니다. 두 Trap 모두 메서드가 호출된 객체인 trapTarget
이라는 단일 파라미터를 받습니다. isExtensible
Trap은 객체가 확장 가능한지 여부를 나타내는 부울값을 반환해야 하고 preventExtensions
Trap은 작업이 성공했는지를 나타내는 부울값을 반환해야합니다.
또한 기본 동작을 구현하기 위해 Reflect.preventExtensions()
와 Reflect.isExtensible()
메서드가 있습니다. 둘 다 부울값을 반환하므로 해당 Trap에서 직접 사용할 수 있습니다.
기본 예제 두개
작업에서 객체 확장성 Trap을 확인 하기위해 isExtensible
및 preventExtensions
Trap에 대한 기본 동작을 구현하는 다음 코드를 살펴보겠습니다.
|
|
이 예제는 Object.preventExtensions()
와 Object.isExtensible()
모두 proxy
에서 target
으로 정확하게 전달되는 것을 보여줍니다. 물론 동작을 바꿀 수도 있습니다. 예를 들어 Object.preventExtensions()
가 프록시에서 성공하지 못하도록하려는 경우
preventExtensions
Trap에서 false
를 반환할 수 있습니다.
|
|
preventExtensions
Trap이 false
를 반환하기 때문에 Object.preventExtensions(proxy)
에 대한 호출이 무시됩니다. 작업은 기본
target
으로 전달되지 않으므로 Object.isExtensible()
은 true
를 반환합니다.
중복 확장 메서드
다시 한번, Object
와 Reflect
에 중복된 메서드가 있다는 것을 눈치챘을 것입니다. 이 경우에는 더 유사합니다. Object.isExtensible()
및 Reflect.isExtensible()
메서드는 비객체 값이 전달된 경우를 제외하고는 서로 비슷합니다. 이 경우 Reflect.isExtensible()
이 오류를 발생하고 Object.isExtensible()
은 항상 false
를 반환합니다. 다음은 그 동작의 예제입니다.
|
|
이 제한은 Object.getPrototypeOf()
와 Reflect.getPrototypeOf()
메서드의 차이와 유사합니다. 하위 레벨 기능의 메서드는 상위 레벨 기능보다 더 엄격한 오류 확인 기능을 가지고 있기 때문입니다.
Object.preventExtensions()
및 Reflect.preventExtensions()
메서드도 매우 비슷합니다. Object.preventExtensions()
메서드는 값이 객체가 아닌 경우에 파라미터로 전달된 값을 항상 반환합니다. 반대로 Reflect.preventExtensions()
메서드는 파라미터가 객체가 아닌 경우 오류를 발생시킵니다. 파라미터가 객체인 경우 Reflect.preventExtensions()
는 작업이 성공하면 true
를 반환하고 그렇지 않으면 false
를 반환합니다.
|
|
여기서 Object.preventExtensions()
는 2
가 객체가 아니더라도 2
를 통과 시킵니다. Reflect.preventExtensions()
메서드는 객체가 전달될 때 true
를 반환하고 2
를 전달하면 오류를 발생시킵니다.
프로퍼티 Descriptor Trap
ECMAScript 5의 가장 중요한 기능중 하나는 Object.defineProperty()
메서드를 사용하여 프로퍼티 속성을 정의하는 기능이었습니다. 이전 버전의 JavaScript에서는 Accessor 프로퍼티를 정의하거나, 속성을 Read-only로 만들거나, 속성을 Nonenumerable하게 만들 수있는 방법이 없었습니다. 이 모든 작업은 Object.defineProperty()
메서드를 사용하여 수행할 수 있으며 Object.getOwnPropertyDescriptor()
메서드를 사용하여 이러한 속성을 검색할 수 있습니다.
프록시를 사용하면 defineProperty
및 getOwnPropertyDescriptor
Trap을 각각 사용하여 Object.defineProperty()
및 Object.getOwnPropertyDescriptor()
에 대한 호출을 가로 채게할 수 있습니다. defineProperty
Trap은 다음 파라미터를 받습니다.
trapTarget
- 프로퍼티을 정의 할 필요가있는 객체 (프록시의 대상)key
- 프로퍼티의 문자열 또는 Symboldescriptor
- 프로퍼티 설명 객체
defineProperty
Trap은 작업이 성공하면 true
를, 그렇지 않으면 false
를 반환합니다. getOwnPropertyDescriptor
Trap은
trapTarget
과 key
만 받으며, Descriptor를 리턴해야합니다. 상응하는 Reflect.defineProperty()
와
Reflect.getOwnPropertyDescriptor()
메서드는 프록시 Trap과 동일한 파라미터를 받습니다. 다음은 각 Trap의 기본 동작을 구현하는 예제입니다.
|
|
이 코드는 Object.defineProperty()
메서드를 사용하여 프록시에서 "name"
이라는 프로퍼티를 정의합니다. 그런 다음 해당 프로퍼티의 Descriptor가 Object.getOwnPropertyDescriptor()
메서드에 의해 검색됩니다.
Object.defineProperty() 잠그기
defineProperty
Trap은 조작이 성공했는지 여부를 나타내기 위해 부울 값을 리턴하도록 요구합니다. true
가 리턴되면, Object.defineProperty()
는 평소대로 성공합니다; false
가 리턴되면 Object.defineProperty()
는 에러를 발생시킵니다. 이 기능을 사용하여 Object.defineProperty()
메서드가 정의할 수있는 프로퍼티의 종류를 제한할 수 있습니다. 예를 들어 Symbol 프로퍼티가 정의되지 않도록하려면 key
가 문자열인지 확인하고 그렇지 않으면 false
를 반환합니다.
|
|
defineProperty
프록시 Trap은 key
가 Symbol 일 때 false
를 리턴하고 그렇지 않으면 기본 동작을 진행합니다. name
을 key
로
하여 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
, get
및 set
속성들만 defineProperty
Trap로 전달된 Descrptor 객체에 있을수 있습니다.
|
|
여기서, Object.defineProperty()
는 세 번째 파라미터에 비표준 name
프로퍼티를 가지고 호출됩니다. defineProperty
Trap이 호출되면, descriptor
객체는 name
프로퍼티를 갖지 않고 value
프로퍼티는 갖습니다. 왜냐하면 descriptor
는 Object.defineProperty()
메서드에 전달된 실제 세 번째 파라미터에 대한 참조가 아니라 허용 가능한 프로퍼티만을 포함하는 새로운 객체이기 때문입니다. Reflect.defineProperty()
메서드는 또한 Descriptor의 비표준 특성을 무시합니다.
getOwnPropertyDescriptor
Trap은 반환값이 null
, undefined
또는 객체가되도록 약간 다른 제한이 있습니다. 객체가 반환되면 객체의 자체 프로퍼티로 enumerable
, configurable
, value
, writable
, get
및 set
만 허용됩니다. 아래 코드와 같이 허용되지 않는 자체 프로퍼티를 가진 객체를 반환하면 오류가 발생합니다.
|
|
프로퍼티 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
를 반환합니다.
|
|
target
에서 Object.defineProperty()
가 호출되면 반환값은 target
입니다. Reflect.defineProperty()
가 target
에서 호출되면 반환값은 연산이 성공했음을 나타내는 true
입니다. defineProperty
프록시 Trap은 반환될 부울 값을 필요로하기 때문에 필요할 때
Reflect.defineProperty()
를 사용하여 기본 동작을 구현하는 것이 좋습니다.
getOwnPropertyDescriptor() 메서드
Object.getOwnPropertyDescriptor()
메서드는 Primitive 값이 전달될 때 첫 번째 파라미터를 객체로 강제 변환한 다음 작업을 계속합니다. 반면에 첫 번째 파라미터가 Primitive 값이면 Reflect.getOwnPropertyDescriptor()
메서드는 오류를 발생시킵니다. 다음은 이 두 가지를 보여주는 예입니다.
|
|
Object.getOwnPropertyDescriptor()
메서드는 2
를 객체로 강제변환하고 객체에는 name
프로퍼티가 없기 때문에 undefined
를 반환합니다. 주어진 이름을 가진 프로퍼티가 객체에서 발견되지 않을때의 메서드 표준 동작입니다. 그러나 Reflect.getOwnPropertyDescriptor()
가 호출되면 해당 메서드가 첫 번째 파라미터에 대한 Primitive 값을 허용하지 않기 때문에 오류가 즉시 발생합니다.
ownKeys Trap
ownKeys
프록시 Trap은 내부 메서드 [[OwnPropertyKeys]]
를 가로 채고, 여러분이 값의 Array를 반환함으로써 동작을 오버라이드할 수 있도록합니다. 이 Array는 Object.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을 사용하여 다음과 같이 키를 걸러낼 수 있습니다.
|
|
이 예제는 Reflect.ownKeys()
를 먼저 호출하여 대상의 기본 리스트를 얻는 ownKeys
Trap을 사용합니다. 그런 다음 filter()
메서드는 문자열이며 밑줄 문자로 시작하는 키를 필터링하는데 사용됩니다. 그런 다음 proxy
객체에 세가지 프로퍼티가 추가됩니다.(name
, _name
및 nameSymbol
). Object.getOwnPropertyNames()
와 Object.keys()
가 proxy
에서 호출되면 name
프로퍼티만 반환됩니다. 비슷하게 Object.getOwnPropertySymbols()
가 proxy
에서 호출될 때 nameSymbol
만 반환됩니다. _name
프로퍼티는 필터링되어서 어느 결과에도 나타나지 않습니다.
ownKeys
Trap은 또한for-in
루프에 영향을 미칩니다.이 루프는 Trap을 호출하여 루프 내부에서 사용할 키를 결정합니다.
apply와 construct Trap을 이용한 Function 프록시
모든 프록시 Trap 중, apply
와 construct
만이 프록시 대상으로 함수가 필요합니다. 3장에서 함수는 각각 new
연산자를 사용하지 않고 함수를 호출할 때 실행되는 [[Call]]
및 [[Construct]]
라는 두가지 내부 메서드를 가지고 있음을 설명했습니다. apply
및 construct
Trap은 해당 내부 메서드를 재정의 하도록합니다. 함수가 new
없이 호출되면, apply
Trap은 Reflect.apply()
에 다음 파라미터가 필요합니다.
trapTarget
- 실행중인 함수 (프록시의 대상)thisArg
- 호출 중 함수 안에있는this
의 값argumentsList
- 함수에게 건네진 파라미터 Array
new
를 사용하여 함수가 실행될 때 호출되는 construct
Trap은 다음 파라미터를 받습니다.
trapTarget
- 실행중인 함수 (프록시의 대상)argumentsList
- 함수에게 건네진 파라미터 Array
Reflect.construct()
메서드는 또한 이 두개의 파라미터를 받아들이며 newTarget
이라는 선택적인 세 번째 파라미터를 받습니다. 주어진 경우, newTarget
파라미터는 함수 안에 new.target
의 값을 지정합니다.
apply
와construct
Trap은 모든 프록시 대상 함수의 동작을 완벽하게 제어합니다. 함수의 기본 동작을 모방하려면 다음과 같이 하면됩니다.
|
|
이 예제에서 숫자 42를 반환하는 함수를 가지고 있습니다. 이 함수의 프록시는 apply
와 construct
Trap을 사용하여 그 동작을 각각 Reflect.apply()
와 Reflect.construct()
메서드에 위임합니다. 결과적으로 프록시 함수는 typeof
가 사용될 때 함수로 자신을 식별하는 것을 포함하여 대상 함수와 똑같이 동작합니다. 프록시는 42를 반환하기 위해 new
없이 호출합니다. 그리고 new
를 호출하여 instance
라는 객체를 만듭니다. instance
객체는 프로토 타입 체인을 사용하여 이 정보를 결정하기 때문에 proxy
와 target
의 인스턴스로 간주됩니다. 프로토 타입 체인 조회는 이 프록시의 영향을 받지 않으므로 프록시와 대상이 JavaScript 엔진과 동일한 프로토 타입을 사용하는 것으로 보여집니다.
함수 파라미터 유효성 검사
apply
와 construct
Trap은 함수가 실행되는 방식을 변경하는 많은 가능성을 열어줍니다. 예를 들어 모든 파라미터가 특정 타입인지 확인하려고 한다고 가정해 보겠습니다. apply
Trap에서 파라미터를 확인할 수 있습니다.
|
|
이 예제는 apply
Trap을 사용하여 모든 파라미터가 숫자임을 확인합니다. sum()
함수는 전달된 모든 파라미터를 더합니다. 숫자가 아닌 값이 전달되면 이 함수는 계속 작업을 시도하므로 예기치 않은 결과가 발생할 수 있습니다. sum()
을 sumProxy()
프록시 안에 넣음으로써 이 코드는 함수 호출을 가로 채고 호출이 진행되기 전에 각 파라미터가 숫자인지 확인합니다. 안전을 위해, 코드는 construct
Trap을 사용하여 new
함수를 호출할 수 없도록합니다.
함수를 new
로 호출하고 파라미터가 숫자인지 확인해야합니다.
|
|
여기서 construct
Trap이 Reflect.construct()
메서드를 사용하여 입력값을 검증하고 새로운 인스턴스를 리턴할 때 apply
Trap은 오류를 던집니다. 물론, 대신에 new.target
을 사용하여 프록시없이 동일한 것을 수행할 수 있습니다.
new
가 없는 생성자 호출
3 장에서 new.target
메타 프로퍼티를 소개했습니다. new.target
은 new
가 호출되는 함수에 대한 참조입니다. 즉, new.target
의 값을 다음과 같이 검사하여 new
를 사용하여 함수를 호출했는지 여부를 알 수 있습니다.
|
|
이 예제는 “함수 파라미터 유효성 검사” 섹션의 프록시를 사용하지 않는 예제와 유사하며, new
를 사용하지 않고 Numbers
가 호출될 때 에러를 던집니다. 유일한 목적이 new
없이 함수를 호출하는 것을 방지하는 것이라면 이와 같은 코드를 작성하는 것은 프록시를 사용하는 것보다 훨씬 간단하며 바람직합니다. 그러나 때로는 동작을 수정해야하는 함수를 제어하지 못하는 경우가 있습니다. 이 경우 프록시를 사용하는 것이 좋습니다.
Numbers
함수가 수정할 수 없는 코드에 정의되었다고 가정해 보겠습니다. 이 코드는 new.target
에 의존하고 있으며 여전히 함수를 호출할 때 체크를 피하기를 원합니다. new
를 사용할 때의 동작은 이미 설정되어 있으므로 apply
Trap을 사용할 수 있습니다.
|
|
NumbersProxy
함수는 new
를 사용하지 않고 Numbers
를 호출하고 new
가 사용된 것처럼 행동하게합니다. 그렇게하기 위해, apply
Trap은 Reflect.construct()
를 호출하고 apply
에 전달된 파라미터를 사용합니다. Numbers
내부의 new.target
은 Numbers
자체와 동일하며 에러는 발생하지 않습니다. 이것은 new.target
을 변경하는 간단한 예제이지만, 더 직접적으로 할 수도 있습니다.
추상 기본 클래스 생성자 (Abstract Base Class Constructor) 재정의
한 걸음 더 나아가 new.target
에 할당할 특정 값으로 Reflect.construct()
의 세 번째 파라미터를 지정할 수 있습니다. 이는 함수가 추상 기본 클래스 생성자를 생성할 때(9 장에서 설명)와 같이 알려진 값에 대해 new.target
을 검사할 때 유용합니다. 추상 기본 클래스 생성자에서, new.target
은 이 예제에서와 같이 클래스 생성자 그 자체가 아닌 다른 것으로 예상됩니다.
|
|
new AbstractNumbers()
가 호출되면, new.target
은 AbstractNumbers
와 같기 때문에 에러를 발생 시킵니다. new.target
이 Numbers
와 같기 때문에 new Numbers()
는 여전히 작동합니다. 수동으로 new.target
에 프록시를 할당함으로써 이 제약을 우회할 수 있습니다.
|
|
AbstractNumbersProxy
는 construct
Trap을 사용하여 new AbstractNumbersProxy()
메서드에 대한 호출을 가로챕니다. 그런 다음, Reflect.construct()
메서드가 Trap의 파라미터와 함께 호출되고 빈 함수를 세 번째 파라미터로 추가합니다. 그 빈 함수는 생성자 내부의 new.target
의 값으로 사용됩니다. new.target
이 AbstractNumbers
와 같지 않기 때문에 오류가 발생하지 않고 생성자가 완전히 실행됩니다.
호출가능한 클래스 생성자
9 장에서는 클래스 생성자가 항상 new
로 호출되어야 한다고 설명했다. 이는 클래스 생성자에 대한 내부 [[Call]]
메서드가 오류를 throw 하도록 지정 되었기 때문에 발생합니다. 그러나 프록시는 [[Call]]
메서드에 대한 호출을 가로챌 수 있습니다. 즉, 프록시를 사용하여 호출 가능한 클래스 생성자를 효과적으로 만들 수 있습니다. 예를 들어, 클래스 생성자가 new
를 사용하지 않고 동작하게 하려면, apply
Trap을 사용하여 새로운 인스턴스를 생성 할 수 있습니다. 다음은 몇 가지 샘플 코드입니다.
|
|
PersonProxy
객체는 Person
클래스 생성자의 프록시입니다. 클래스 생성자는 단지 함수이므로 프록시에서 사용될 때 함수처럼 작동합니다. apply
Trap은 기본 동작을 재정의하고 대신 Person
과 같은 trapTarget
의 새로운 인스턴스를 반환합니다(이 예제에서 trapTarget
을 사용하여 클래스를 수동으로 지정하지 않아도 된다는 것을 보여주기 위해 사용했습니다). argumentList
는 Spread 연산자를 사용하여 각 파라미터가 개별적으로trapTarget에 전달됩니다.
new를 사용하지 않고
PersonProxy()를 호출하면
Person의 인스턴스를 반환합니다(
new없이
Person()`을 호출하려고 하면 생성자는 여전히 오류를 던질 것입니다). 호출 가능한 클래스 생성자를 만드는 것은 프록시를 사용하는 경우에만 가능합니다.
취소 가능한 프록시
일반적으로 프록시가 생성되면 프록시는 대상에서 재배치할 수 없습니다. 이장의 모든 예제는 재배치할 수 없는 프록시를 사용했습니다. 그러나 더이상 사용할 수 없도록 프록시를 취소하려는 경우가 있을 수 있습니다. 보안을 위해 API를 통해 객체를 제공하고 언제든지 일부 기능에 대한 액세스를 차단할 수 있는 기능을 유지하려는 경우 프록시를 해지하는 것이 유용합니다.
Proxy.revocable()
메서드로 취소 가능한 프록시를 생성할 수 있습니다. 이 메서드는 대상 객체와 프록시 핸들러인 Proxy
생성자와 같은 파라미터를 사용합니다. 반환 값은 다음과 같은 프로퍼티를 가진 객체입니다.
proxy
- 취소할 수 있는 프록시 객체revoke
- 프록시를 취소하기 위해서 호출하는 함수
revoke()
함수가 호출될 때, proxy
를 통해 더 이상의 연산을 수행할 수 없습니다. 프록시 Trap을 발생시키는 방식으로 프록시 오브젝트와 상호 작용하려는 모든 시도는 오류를 발생시킵니다.
|
|
이 예제는 취소 가능한 프록시를 만듭니다. Proxy.revocable()
메서드에 의해 반환된 객체에서 같은 이름의 프로퍼티에 proxy
와 revoke
변수를 할당하기 위해 Destructuring을 사용합니다. 그 후, proxy
객체는 취소가 불가능한(nonrevocable) 프록시 객체처럼 사용될 수 있습니다. 그래서 proxy.name
은 target.name
을 그대로 통과하기 때문에 "target"
을 리턴합니다. 그러나 일단 revoke()
함수가 호출되면, 프록시는 더이상 함수가 아닙니다. proxy.name
에 접근하려고 시도하면 에러가 발생하고, 프록시에서 Trap을 발생시키는 다른 동작도 마찬가지입니다.
Array의 문제점 해결하기
이장의 시작 부분에서 개발자가 ECMAScript 6 이전의 JavaScript에서 Array의 동작을 정확하게 모방할 수 없다는 것을 설명했습니다. 프록시와 리플렉션 API를 사용하면 프로퍼티가 추가되고 제거될 때 Built-in Array
타입과 같은 방식으로 동작하는 객체를 생성할 수 있습니다. 기억을 되새겨 아래 예제는 프록시가 Array를 모방하는데 도움이되는 동작을 보여주는 예입니다.
|
|
위 예제에서 주의해야할 두가지 중요한 동작이 있습니다.
colors[3]
에 값이 할당되면length
프로퍼티가 4로 증가합니다.length
프로퍼티가 2로 설정되면 Array의 마지막 두 항목이 삭제됩니다.
이 두가지 동작은 Built-in Array의 작동 방식을 정확하게 재현하기 위해 모방되어야하는 중요한 동작입니다. 다음 몇 섹션에서는 올바르게 Array 객체를 모방하는 방법을 설명합니다.
Array 색인 찾기
정수형 프로퍼티 키에 할당하는 것은 비 정수형 키와 다르게 취급되는 Array의 특별한 경우입니다. ECMAScript 6 사양에서는 프로퍼티 키가 Array 인덱스인지 확인하는 방법에 대한 지침을 제공합니다.
toString(ToUint32(P))
이P
이고ToUint32(P)
가2 ^ 32-1
이 아니면 String 프로퍼티 이름P
는 Array 인덱스입니다.
이 연산자는 다음과 같이 JavaScript로 구현될 수 있습니다.
|
|
toUint32()
함수는 사양에 설명된 알고리즘을 사용하여 주어진 값을 부호없는 32비트 정수로 변환합니다. isArrayIndex()
함수는 먼저 키를 uint32로 변환한 다음 비교를 수행하여 키가 Array 인덱스인지 여부를 확인합니다. 이러한 유틸리티 함수를 사용할 수 있으면 Built-in Array를 모방할 객체를 구현할 수 있습니다.
새로운 요소를 추가할 때 length
증가
설명한 두Array의 동작이 프로퍼티 할당에 의존한다는 것을 눈치 챘을 것입니다. 즉, 두가지 동작을 모두 수행하려면 set
프록시 Trap을 사용해야합니다. 먼저 length-1
보다 큰 Array 인덱스가 사용되면 length
프로퍼티를 증가시켜 첫 번째 두가지 동작을 구현하는 예제를 보겠습니다.
|
|
이 예제는 set
프록시 Trap을 사용하여 Array 인덱스의 설정을 가로챕니다. 키가 Array 인덱스인 경우 키는 항상 문자열로 전달되기 때문에 숫자로 변환됩니다. 그 숫자 값이 현재 length
프로퍼티보다 크거나 같으면 length
프로퍼티가 숫자 키보다 하나 더 업데이트됩니다 (위치 3의 항목 설정은 length
가 4 여야 함을 의미합니다). 그 후에 프로퍼티를 설정하기위한 기본 동작은 Reflect.set()
를 통해 사용됩니다. 프로퍼티가 지정된 값을 받기를 원하기 때문입니다.
최초의 커스텀 Array는 length
가 3인 createMyArray()
를 호출하여 생성되며, 그 세항목의 값은 바로 뒤에 추가됩니다. length
프로퍼티는 3번 위치에 "black"
값이 할당될 때까지 정확히 3을 유지합니다. 그 시점에서 length
는 4로 설정됩니다.
첫번째 동작이 작동하면 두 번째 동작으로 이동할 시간입니다.
length 줄이기에 대한 요소 삭제
모방을 위한 첫 번째 Array 동작은 Array 인덱스가 length
프로퍼티보다 크거나 같은 경우에만 사용됩니다. 두 번째 동작은 length
프로퍼티가 이전에 포함된 값보다 작은 값으로 설정되면 값을 줄이고 남은 Array 항목을 제거합니다. 이는 length
프로퍼티를 변경하는 것뿐만 아니라 존재하지 않는 모든 항목을 삭제하는 것을 포함합니다. 예를 들어, length
가 4인 Array가 length
를 2로 설정하면, 2와 3 위치의 항목은 삭제됩니다. 첫 번째 동작과 함께 set
프록시 Trap에서 이것을 수행 할 수 있습니다. 아래 예제는 앞의 예제를 업데이트한createMyArray
메서드 입니다.
|
|
이 코드의 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 ‘값입니다.) 인스턴스는 프록시의 대상이되고 프록시는 인스턴스인 것처럼 반환됩니다. 인스턴스는 완전히 비공개이며 직접 액세스할 수는 없지만 프록시를 통해 간접적으로 액세스할 수 있습니다.
다음은 클래스 생성자에서 프록시를 반환하는 간단한 예입니다.
|
|
이 예제에서 Thing
클래스는 생성자에서 프록시를 반환합니다. 프록시 대상은 this
이며 프록시는 생성자에서 반환됩니다. 즉, Thing
생성자를 호출하여 myThing
을 만들었지만 실제로 myThing
은 프록시입니다. 프록시는 자신의 동작을 대상에 전달하기 때문에 myThing
은 여전히 Thing
클래스로 간주되며 프록시는 Thing
클래스를 사용하는 모든 사람에게 완전히 투명합니다.
이를 염두에두고 상대적으로 간단한 방식으로 프록시를 사용하여 맞춤 Array 클래스를 만듭니다. 코드는 “length 줄이기에 대한 요소 삭제”절의 코드와 거의 같습니다. 동일한 프록시 코드가 사용되지만 이번에는 클래스 생성자 내부에 있습니다. 전체 예제는 다음과 같습니다.
|
|
이 코드는 생성자에서 프록시를 반환하는 MyArray
클래스를 만듭니다. length
프로퍼티는 생성자에 추가되고 (전달된 값이나 기본값 0으로 초기화 됨) 프록시가 생성되어 반환됩니다. 이것은 colors
변수에 MyArray
의 인스턴스를 대입하는 모양이 되며 Array의 주요 동작을 구현합니다.
클래스 생성자에서 프록시를 반환하는 것은 쉽지만 모든 인스턴스에 대해 새 프록시가 만들어 짐을 의미합니다. 그러나 모든 인스턴스가 하나의 프록시를 공유하는 방법이 있습니다. 프록시를 프로토 타입으로 사용할 수 있습니다.
프로토 타입으로 프록시 사용
프록시는 프로토 타입으로 사용될 수 있지만, 이장의 앞의 예제보다 약간 복잡합니다. 프록시가 프로토 타입일 때, 프록시 Trap은 기본 동작이 정상적으로 프로토 타입에 계속될 때만 호출되며, 프록시의 기능을 프로토 타입으로 제한합니다. 다음 예제를 살펴 보겠습니다.
|
|
newTarget
객체는 프록시를 프로토 타입으로 하여 생성됩니다. target
을 프록시 타겟으로 만들면 프록시가 투명하기 때문에 효과적으로 target
을newTarget
의 프로토 타입으로 만듭니다. 이제, 프록시 Trap은 newTarget
에 대한 연산이 target
에서 일어날 연산을 통과할 때만 호출됩니다.
Object.defineProperty()
메서드는 newTarget
에 호출되어 name
이라는 자체 프로퍼티를 생성합니다. 객체상에 프로퍼티를 정의하는 것은 일반적으로 객체의 프로토 타입으로 계속되는 연산이 아니기 때문에 프록시상의 defineProperty
Trap은 결코 호출되지 않고 name
프로퍼티는 자신의 프로퍼티로 newTarget
에 추가됩니다.
프로토 타입으로 사용할 경우 프록시가 심각하게 제한되지만 여전히 유용한 몇가지 Trap이 있습니다.
프로토 타입에서 get Trap 사용하기
내부 [[Get]]
메서드가 호출되어 프로퍼티를 읽으면 연산은 먼저 자신의 프로퍼티를 찾습니다. 지정된 이름을 가진 자체 프로퍼티를 찾을 수 없는 경우 연산은 프로토 타입을 계속 진행하고 거기에서 프로퍼티를 찾습니다. 검사할 프로토 타입이 더이상 없을 때까지 프로세스가 계속됩니다.
이 프로세스 덕분에, get
프록시 Trap을 설정하면 주어진 이름의 자체 프로퍼티가 존재하지 않을 때마다 프로토 타입에서 Trap이 호출됩니다. get
Trap을 사용하면 존재한다고 보장할 수 없는 프로퍼티에 액세스할 때 예기치 않은 동작을 방지할 수 있습니다. 단지 존재하지 않는 프로퍼티에 액세스하려고 할 때마다 오류가 발생하는 객체를 만들면 됩니다.
|
|
이 코드에서, thing
객체는 프록시를 프로토 타입으로 하여 생성됩니다. get
Trap은 주어진 키가 thing
객체에 존재하지 않은 호출에 대해 에러를 발생시킵니다. thing.name
을 읽을 때, 프로퍼티가 thing
에 존재하기 때문에 연산은 결코 프로토 타입에서 get
Trap을 호출하지 않습니다. get
Trap은 존재하지 않는 thing.unknown
프로퍼티에 접근할 때에만 호출됩니다.
마지막 줄이 실행될 때 unknown
은 thing
의 자체 속성이 아니므로 연산은 프로토 타입으로 계속됩니다. 그리고 get
Trap은 오류를 던집니다. 이러한 유형의 동작은 JavaScript에서 매우 유용할 수 있습니다. JavaScript는 알려지지 않은 프로퍼티는 오류를 발생(다른 언어에서처럼)하는 대신 undefined
를 자동으로 반환합니다.
이 예제에서 trapTarget
과 receiver
는 다른 객체라는 것을 이해하는 것이 중요합니다. 프록시가 프로토 타입으로 사용될 때, trapTarget
은 프로토 타입 객체 자체이고 receiver
는 인스턴스 객체입니다. 이 경우 trapTarget
은 target
과 같고 receiver
는 thing
과 같습니다. 이렇게하면 원래 프록시 대상과 작업을 수행할 대상에 모두 액세스할 수 있습니다.
프로토 타입에서 set Trap 사용하기
내부 [[Set]]
메서드는 자체 프로퍼티를 확인한 다음 필요에 따라 프로토 타입을 계속 진행합니다. 오브젝트 프로퍼티에 값을 할당하면, 같은 이름의 프로퍼티에 값이 할당됩니다. 지정된 이름의 프로퍼티가 없는 경우 연산은 프로토 타입으로 계속 진행됩니다. 까다로운 부분은 할당 작업이 프로토 타입으로 계속 되더라도 해당 프로퍼티에 값을 할당하면 해당 이름의 프로퍼티가 프로토 타입에 있는지 여부에 관계없이 기본적으로 프로토 타입이 아닌 인스턴스에 대한 프로퍼티가 만들어집니다.
프로토 타입에set
Trap이 호출될 때와 그렇지 않을 때를 더 잘 이해하려면, 기본 동작을 보여주는 다음 예제를 살펴보십시오.
|
|
이 예제에서, target
은 자신의 프로퍼티가 없이 시작됩니다. thing
객체는 새로운 프로퍼티의 생성을 위한 set
Trap을 정의하는 프록시를 프로토 타입으로 가지고 있습니다. thing.name
에 값으로 "thing"
이 지정되면 thing
에 name
이라는 자체 프로퍼티가 없으므로set
프록시 Trap이 호출됩니다. set
Trap 내에서 trapTarget
은 target
과 같고 receiver
는 thing
과 같습니다. 연산은 궁극적으로 thing
에 새로운 프로퍼티를 만들어야하며, 다행히 Reflect.set()
는 네 번째 파라미터로 receiver
를 전달하면 이 기본 동작을 구현합니다.
name
프로퍼티가 thing
에 생성되면 thing.name
을 다른 값으로 설정해도 더이상 set
프록시 Trap을 호출하지 않습니다. 이 시점에서, name
은 자체 프로퍼티이므로 [[Set]]
연산은 프로토 타입으로 계속되지 않습니다.
프로토 타입에서 has Trap 사용하기
has
Trap은 객체에서 in
연산자의 사용을 가로챈다는 것을 설명했습니다. in
연산자는 먼저 주어진 이름을 가진 객체 자신의 프로퍼티를 검색합니다. 이름이 같은 자체 프로퍼티가 없으면 연산이 프로토 타입으로 계속 진행됩니다. 프로토 타입에 자체 프로퍼티가 없으면 프로토
타입을 찾거나 검색할 프로토 타입이 더 이상 없을 때까지 프로토 타입 체인을 통해 검색이 계속됩니다.
따라서 has
Trap은 검색이 프로토 타입 체인에서 프록시 객체에 도달할 때만 호출됩니다. 프로토 타입으로 프록시를 사용하는 경우 지정된 이름의 자체 프로퍼티가 없을 때만 발생합니다.
|
|
이 코드는 thing
프로토 타입에 has
프록시 Trap을 만듭니다. has
Trap은 in
연산자가 사용될 때 프로토 타입을 자동으로 검색하기 때문에 get
및 set
Trap처럼 receiver
객체를 통과하지 않습니다. 대신, has
Trap은 target
과 같은 trapTarget
에서만 작동해야합니다. 이 예제에서 처음으로 in
연산자가 사용되면, 프로퍼티 이름이 thing
의 자체 프로퍼티로 존재하지 않기 때문에 has
Trap이 호출됩니다. thing.name
에 값이 주어지고 in
연산자가 다시 사용될 때 has
에 자신의 프로퍼티 name
을 찾은 후에 연산이 멈추기 때문에 has
Trap이 호출되지 않습니다.
이 시점의 프로토 타입 예제는 Object.create()
메서드를 사용하여 생성된 객체를 중심으로 이루어졌습니다. 그러나 프록시를 프로토 타입으로 사용하는 클래스를 만들려면 프로세스가 좀 더 복잡합니다.
클래스의 프로토 타입으로서의 프록시
프로토 타입 프로퍼티가 non-writable이기 때문에 클래스를 프록시 prototype
으로 사용하도록 직접 수정할 수 없습니다. 그러나 상속을 사용하여 프록시를 프로토 타입으로 사용하는 클래스를 약간 다르게 사용할 수 있습니다. 시작하려면 생성자 함수를 사용하여 ECMAScript 5 스타일 형식 정의를 만들어야합니다. 그런 다음 프로토 타입을 프록시로 덮어 쓸수 있습니다. 다음은 그 예제입니다.
|
|
NoSuchProperty
함수는 클래스가 상속하는 기반을 나타냅니다. 함수의 프로토 타입 프로퍼티에는 제한이 없으므로 프록시로 이를 덮어 쓸 수 있습니다. get
Trap은 프로퍼티가 존재하지 않을 때 오류를 던지기 위해 사용됩니다. thing
객체는 NoSuchProperty
인스턴스로 생성되고 존재하지 않는 name
프로퍼티에 접근할 때 오류를 던집니다.
다음 단계는 NoSuchProperty
를 상속받은 클래스를 만드는 것입니다. 9장에서 논의된 extends
문법을 사용하여 클래스 프로토 타입 체인에 프록시를 도입 할 수 있습니다.
|
|
Square
클래스는 NoSuchProperty
를 상속받습니다. 그래서 프록시는 Square
클래스의 프로토 타입 체인에 있습니다. shape
객체는Square
의 새로운 인스턴스로 생성되고 length
와 width
라는 두개의 프로퍼티를 갖습니다. get
프록시 Trap이 결코 호출되지 않기 때문에 이들 프로퍼티의 값을 읽는 것은 성공합니다. shape
에 존재하지 않는 프로퍼티(shape.wdth
, 명백한 오타)에 접근할 때만 get
프록시 Trap 트리거가 발생하고 오류가 발생합니다.
이는 프록시가 shape
의 프로토 타입 체인에 있음을 증명하지만 프록시가 shape
의 직접 프로토 타입이 아니라는 점은 분명하지 않을 수 있습니다. 실제로, 프록시는 shape
에서 프로토 타입 체인까지 두단계입니다. 앞의 예를 약간 변경하면 더 명확하게 알 수 있습니다.
|
|
이 버전의 코드는 프록시를 proxy
라는 변수에 저장하므로 나중에 쉽게 식별할 수 있습니다. shape
의 프로토 타입은 Shape.prototype
이고 프록시가 아닙니다. 그러나 Shape.prototype
의 프로토 타입은 NoSuchProperty
에서 상속된 프록시입니다.
상속은 프로토 타입 체인에 또 다른 단계를 추가합니다. 프록시에 get
Trap을 호출할 때 발생할 수 있는 작업으로 인해 하나의 추가 단계가 필요하기 때문에 중요합니다. Shape.prototype
에 프로퍼티가 있으면 다음과 같이 get
프록시 Trap이 호출되지 않습니다.
|
|
여기에서 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
, set
및 has
프록시 Trap만 호출되어 사용가능 사례를 훨씬 더 좁힙니다.
이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://leanpub.com/understandinges6/read#leanpub-auto-proxies-and-the-reflection-api]
참고
- ECMAScript 6 Block Binding
- ECMAScript 6 문자열과 정규 표현식
- ECMAScript 6 함수
- ECMAScript 6 객체의 확장된 기능
- ECMAScript 6 쉬운 데이터 액세스를 위한 Destructuring
- ECMAScript 6 Symbol과 Symbol 프로퍼티
- ECMAScript 6 Set과 Map
- ECMAScript 6 Iterator와 Generator
- ECMAScript 6 JavaScript 클래스 소개
- ECMAScript 6 Array 기능 향상
- ECMAScript 6 Promise와 비동기 프로그램밍
- ECMAScript 6 프록시와 리플렉션 API
- ECMAScript 6 Module로 코드 캡슐화하기
- ECMAScript 6 부록 A. 작은 변경 사항
- ECMAScript 6 부록 B. ECMAScript 7(2016) 이해하기