14장 리플렉션-리플렉트과 프록시

2023.06.16

  • 객체를 구성하고 상호 작용하는 데 유용한 유틸리티 함수를 제공하는 Reflect 객체 클라이언트 코드에 API를 제공하는 데 중요할 수 있는 패턴이고 자바스크립트에 대한 궁극적인 파사드 패턴을 제공하는 Proxy에 대해 배운다.
  • Reflect와 Proxy는 함께 사용하도록 설계되었지만 각각은 다른 하나 없이 사용할 수 있다.

14.1 리플렉트

  • Reflect는 ES2015에서 객체에 대해 수행되는 기본 작업에 해당하는 다양한 메서드(속성 값 임포트와 설정, 객체 프로토타입 임포트와 설정. 객체에서 속성 삭제 등)에 추가되었다.
  • Reflect는 다음과 같은 몇 가지 사항을 제공한다.
    • 일부는 구문이 되고 나머지는 Object 함수 또는 Object 함수의 조합이 되는 대신 모든 기본 객체 작업에 대한 얇은 래퍼 함수를 제공한다. 즉, in이나 delete 연산자와 같은 엔트리에 대해 자체 래퍼를 작성하지 않고도 기본 객체 작업을 전달할 수 있다.
    • 해당 함수는 동등한 Object 함수가 대신 오류를 발생시키는 일부 경우에 성공/실에 대한 반환값을 제공한다.
    • 프록시절에서 배우게 될 것처럼 프록시 객체가 연결할 수 있는 각 트랩과 완벽하게 일치하는 함수를 제공하여 트랩에 대한 기본 동작을 구현하므로 동작만 수정하는 트랩을 행동을 완전히 바꾸는 것보다 간단하게 올바르게 구현할 수 있다.
    • newTarget 인수를 사용하여 Reflect.construct 클래스 구문 외부에서 사용할 수 없는 작업을 제공한다.
  • Reflect Object에는 동일한 이름과 대부분 동일한 목적을 가진 여러 함수가 있다.
    • defineProperty
    • getOwnPropertyDescriptor
    • getPrototypeOf
    • setPrototypeOf
    • preventExtensions
  • 기본적으로 동일한 작업을 수행하자 약간의 차이점이 있다.
    • 일반적으로 Reflect 버전은 객체가 예상되는 곳에 비 객체를 전달하면 오류가 발생하지만 객체 버전은 프리미티브를 객체로 강제 변환하고 그 결과에 대해 작동한다. 또는 그냥 호출을 무시한다.
    • 일반적으로 수정 함수의 Reflect 버전은 성공/실패 플래그를 반환하는 반면 Object 버전은 수정하기 위해 전달한 객체를 반환한다. Object 버전은 일반적으로 실패 시 오류를 발생시킨다.
  • 대부분의 Reflect 함수는 주로 프록시를 구현하는 데 유용하지만, 자체적으로 유용한 함수도 살펴보겠다.

14.1.1 Reflect.apply

  • Reflect.apply는 함수의 apply 메서드가 하는 일을 하는 유틸리티 함수이다. 특정 this 값과 배열(또는 배열과 유사한 객체)로 제공되는 인수 목록을 사용하여 함수를 호출한다.
1function example(a, b, c) {
2  console.log(`this.name = ${this.name}, a = ${a}, b = ${b}, c = ${c}`);
3}
4const thisArg = { name: 'test' };
5const args = [1, 2, 3];
6Reflect.apply(example, thisArg, args); // this.name = test, a = 1, b, = 2, c = 3
7
8// Reflect.apply에 대한 호출을 example.apply에 대한 호출로 바꿀 수 있다.
9example.apply(thisArg, args); // this.name = test, a = 1, b, = 2, c = 3
  • Reflect.apply에는 몇 가지 장점이 있다.
    • example apply 속성이 Function.prototype.apply 이외의 다른 것으로 재정의되거나 덮어써도 작동한다.
    • 약간의 변형: 함수의 프로토타입이 변경되어(예를 들어 Object.setPrototypeof로) 함수가 더 이상 Function.prototype에서 상속되지 않도록 변경되어 apply가 없고 완전히 모호한 경우에도 작동한다.
    • 진정한 자바스크립트 함수가 아니더라도 호출 가능한 모든 것([[Call]] 내부 작업이 있는 모든 객체)에서 작동한다. 예전보다 이러한 함수가 더 적지만 호스트가 제공한 "함수"가 예전만 해도 실제 자바스크립트 함수가 아니거나 apply가 없는 경우가 많았다.

14.1.2 Reflect.consturct

  • Reflect.construct는 new 연산자와 마찬가지로 생성자 함수를 통해 새 인스턴스를 만든다. 그런데 Reflect.construct는 new가 제공하지 않는 다음 두 가지 함수를 제공한다. - 생성자에 대한 인수를 배열(또는 배열과 유사한 객체)로 받아들인다. - new.target을 호출하는 함수가 아닌 다른 것으로 설정할 수 있다.
  • 배열에 생성자(Thing)와 함께 사용하려는 인수가 있다고 가정해 보자. ES5에서는 배열의 인수로 생성자를 호출하는 것이 어색했다. 가장 간단한 방법은 객체를 직접 생성한 다음 apply를 통해 생성자를 일반 함수로 호출하는 것이었다.
  • ES2015+에서는 스프레드 구문을 사용하는 방법과 Reflect.construct를 수행하는 두 가지 방법 이 있다.
1// ES5에서
2var o = Thing.apply(Object.create(Thing.prototype), theArguments);
3// ES2015+에서
4let o = new Thing(theArguments);
5// 또는
6let o = Reflect.construct(Thing, theArguments);
  • 스프레드 구문은 이터레이터를 거치기 때문에 Reflect.construct보다 더 많은 작업을 수행한다
  • Reflect.construct가 제공하는 두 번째 사항은 new.target을 제어하는 new가 제공하지 않는 것인데, 프록시와 함께 Reflect를 사용할 때 주로 유용하지만 내장의 하위 유형인 인스턴스를 만드는 데 사용할 수도 있다.
  • class와 new를 사용하지 않으려면 Reflect.consturct를 사용하는 것이 좋다.
1// Defining the function that builds custom errors
2function buildCustomError(...args) {
3  return Reflect.construct(Error, args, buildCustomError);
4}
5buildCustomError.prototype = Object.assign(Object.create(Error.prototype), {
6  constructor: buildCustomError,
7  report() {
8    console.log(`this.message = ${this.message}`);
9  },
10});
11
12// Using it
13const e = buildCustomError('error message here');
14console.log('instanceof Error', e instanceof Error);
15e.report();
16console.log(e);

14.1.3 Reflect.ownKeys

  • Reflect.ownkeys 함수는 표면적으로 Object.keys와 유사하지만 열거할 수 없는 키와 문자열이 아닌 심볼로 이름이 지정된 키를 포함하여 객체의 모든 자체 속성 키의 배열을 반환한다
  • 이름의 유사성에도 불구하고 Object keys보다는 Object.getOwnPropertyNames와 Object. getOwnProperty Symbols의 조합에 가깝다.

14.1.4 Reflect.get, Reflect.set

  • 이러한 함수는 객체의 속성을 가져오고 설정하는 편리한 기능이 있다. 접근 중인 속성이 접근자 속성인 경우 접근자 호출 내에서 이것이 무엇인지 제어할 수 있다. 사실상 그것들은 Reflect. apply의 접근자 속성 버전이다.
1class Product {
2  constructor(x, y) {
3    this.x = x;
4    this.y = y;
5  }
6  get result() {
7    return this.x * this.y;
8  }
9}
10class DoubleProduct extends Product {
11  get result() {
12    return super.result * 2;
13  }
14}
15const d = new DoubleProduct(10, 2);
16console.log(d.result); // 40
  • DoubleProduct의 결과 접근자는 Super.result를 사용하여 DoubleProduct가 아닌 Product의 결과 접근자를 실행해야 한다. DoubleProduct의 버전을 사용하는 경우 재귀적으로 호출되어 스택오버플로가 발생하기 때문이다.
  • super 없이 그렇게 해야 하는 경우 Reflect.get을 사용한다. Reflect.get이 없으면 결과에 대한 속성 설명자를 가져와야 하고 get, call/apply 또는 Reflect.apply를 통해 get 메서드를 호출해 야 하는데, 다음과 같이 어색하다.
1get result() {
2  const proto = Object.getPrototypeOf(Object.getPrototypeOf(this));
3  const descriptor = Object.getOwnPropertyDescriptor(proto, "result");
4  const superResult = descriptor.get.call(this);
5  return superResult * 2;
6}
7
8// Reflect.get을 사용하면 다음 코드처럼 훨씬 더 간단해진다.
9get result() {
10  const proto = Object.getPrototypeOf(Object.getPrototypeOf(this));
11  return Reflect.get(proto, "result", this) * 2
12}
  • Reflect.get을 사용하면 super가 없는 상황 또는 super가 적용되지 않는 상황을 처리할 수 있다. Reflect.get의 시그니처는 다음과 같다.
1value = Reflect.get(target, propertyNamel, receiver]);
  • target은 속성을 가져올 객체다.
  • propertyName은 가져올 속성의 이름이다.
  • receiver는 속성이 접근자 속성인 경우 접근자 호출 중에 이것으로 사용할 선택적 객체다.
  • Reflect.get은 target에서 propertyName에 대한 속성 설명자를 가져오고 설명자가 데이터 속성에 대한 경우 해당 속성의 값을 반환한다.
  • 서술자가 접근자를 위한 것이라면 Reflect.get은 this로 receiver를 사용하여 접근자 함수를 호출한다.
  • Reflect.set은 Reflect.get과 동일한 방식으로 작동하며 속성을 가져오는 대신 설정하면 된다. 다음은 Reflect.set의 시그니처이다.
1result = Reflect.set(target, propertyName, valuel, receiver]);
  • target, propertyName, receiver는 모두 동일하다. value는 설정할 값이다. 이 함수는 값이 설정 되어 있으면 true를 반환하고 그렇지 않으면 false를 반환한다.

14.1.5 기타 리플렉트 함수

1| 함수                     | 설명                                                                                                                                                                                     |
2| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3| defineProperty           | Object.defineProperty와 비슷하지만 오류가 발생하는 대신 성공하면 true, 실패하면 false를 반환한다.                                                                                      |
4| deleteProperty           | delete 연산자의 함수 버전이다.  엄격 모드에서도 항상 성공의 경우 true를 반 삭제 속성 환하고 실패의 경우 false를 반환한다.                                                              |
5| getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor 와 유사하지만 객체가 아닌 것을 전달하는 경우(강제 적용이 아닌) 예외가 발생한다는 점이 다르다.                                                            |
6| getPrototypeof           | Object.getPrototypeOf와 유사하지만, 객체가 아닌 것을 전달하는 경우 강제적 용이 아닌 예외가 발생한다는 점이 다르다.                                                                       |
7| has                      | in 연산자의 함수 버전(hasOwnProperty와 다음, has는 프로토타입도 검사함)이다.                                                                                                             |
8| isExtensible             | Object.isExtensible과 유사하지만 false를 반환하는 대신 객체가 아닌 것을 전달하면 예외가 발생한다.                                                                                        |
9| preventExtentions        | Object.preventExtensions와 유사하지만 1) 객체가 아닌 것을 전달하면 아무것도 하지 않고 해당 값을 반환하는 대신 오류가 발생한다. 2) 작업이 실패하면 (오류를 던지지 않고) false를 반환한다. |
10| setPrototypeOf           | Object.setPrototypeOf와 유사하지만 1) 객체가 아닌 것을 전달하면 아무것도 하지 않고 해당 값을 반환하는 대신 오류가 발생한다. 2) 작업이 실패하면 (오류를 던지지 않고) false를 반환한다.    |

14.2 프록시

  • 프록시 객체는 대상 객체로 가는 도중에 기본적인 객체 작업을 가로채는 데 사용할 수 있는 객체다.
  • 프록시를 생성할 때 속성 값 임포트나 새 속성 정의와 같이 처리할 작업에 대해 하나 이상의 트랩에 대한 핸들러를 정의할 수 있다.
  • 프록시에 대한 많은 사용 사례가 있다.
    • 객체에서 발생하는 작업 기록 존재하지 않는 속성 읽기/쓰기를 오류로 만들기(undefined를 반환하거나 속성을 생성하는 대신)
    • 두 코드 영역 사이에 경계 제공(예: API와 컨슈머)
    • 변경 가능한 객체의 읽기 전용 뷰 만들기
    • 객체에 정보를 숨기거나 객체가 실제보다 더 많은 정보를 갖고 있는 것처럼 보이게 하기
  • 프록시를 사용하면 기본 작업에 연결하고 수정할 수 있으므로 프록시로 수행할 수 있는 작업에는 제한이 거의 없다.
1const obj = {
2  testing: "abc",
3};
4const p = new Proxy(obj, {
5  get(target, name, receiver) {
6    console.log(`(getting property '${name}')`);
7    return Reflect.get(target, name, receiver);
8  },
9});
10
11console.log("Getting 'testing' directly...");
12console.log(`Got ${obj.testing}`);
13console.log("Getting 'testing' via proxy...");
14console.log(`Got ${p.testing}`);
15console.log("Getting non-existent property 'foo' via proxy...");
16console.log(`Got ${p.foo}`);
  • 위의 코드는 get 트랩에 대한 핸들러를 정의하는 프록시를 생성한다.
  • 주의사항 몇가지가 있다.
    • 프록시를 생성하려면 Proxy 생성자에 대상 객체와 트랩 핸들러가 있는 객체를 전달한다.
    • 대상 객체에서 직접 수행되는 작업은 프록시를 트리거하지 않고 프록시를 통행 수행되는 작업만 수행한다.
    • get 트랩은 하나의 단일 속성에만 국한되지 않는다.
  • 프록시가 기본 작업을 수신하는 것 이상을 할 수 없다고 해도 매우 유용하지만 작업 결과를 변경하거나 완전히 억제할 수도 있다.
  • 모든 기본 객체 작업에는 프록시 트랩이 있다. 트랩에 대한 핸들러는 거의 모든 작업을 수행할 수 있다.
1| 트랩 이름                | 작업 이름             | 발생 시점                                                                                |
2| ------------------------ | --------------------- | ---------------------------------------------------------------------------------------- |
3| apply                    | [[Call]]              | 프록시가 함수로 함수 호출될 (프록싱 함수일 때만 사용 가능)                             |
4| construct                | [[Construct]]         | 프록시가 생성자로 사용될 (생성자를 프록시할 때만 사용 가능)                            |
5| defineProperty           | [[DefineOwnProperty]] | 속성이 프록시에서 정의 또는 재정의될 (데이터 속성인 경우 해당 값이 설정되는 경우 포함) |
6| deleteProperty           | [[Delete]]            | 프룩시에서 속성이 삭제될 때                                                              |
7| get                      | [[Get]]               | 속성 값을 프록시에서 읽어질 때                                                           |
8| getOwnPropertyDescriptor | [[GetOwnProperty]]    | 속성의 설명자가 프록시에서 읽어질 (예상보다 할씬 더 자주 발생함)                       |
9| getPrototype0f           | [[GetPrototypeOt]]    | 프록시의 프로토타입을 읽어질 때                                                          |
10| has                      | [[HasProperty]]       | 프록시를 통해 속성의 존재 여부를 확인될 (. in 연산자 또는 이와 유사한 사용)          |
11| isExtensible             | [[IsExtensible]]      | 프로시가 확장 가능한지 확인하기 위해 검사될 (, 아직 확장이 방지되지 않았음)          |
12| ownKeys                  | [[OwnPropertyKeys]]   | 프록시의 고유한 속성 이름을 읽어질 때                                                    |
13| preventExtensions        | [[PreventExtensions]] | 프룩시에서 확장이 방지될 때                                                              |
14| set                      | [[Set]]               | 속성 값이 프록시에서 설정될 때                                                           |
15| setPrototype0f           | [[SetPrototypeOf]]    | 프록시의 프로토타입이 설정될 때                                                          |

14.2.1 예: 로깅프록시

14.2.2 프록시 트랩

  • 일반적인 특징
    • 일반적으로 프록시 트랩에 대한 핸들러는 일부 제한이 있기는 하지만 원하는 거의 모든 작업을 수행할 수 있다.
      • 대상 객체/함수를 건드리지 않고 작업 자체를 처리하기
      • 작업 조정하기
      • 작업 거부, 오류 플래그 반환 또는 오류 발생
      • 트랩 핸들러는 임의의 코드이므로 원하는 부작용 수행하기
    • 트랩 핸들러에 제한이 있는 경우 프록시와 같은 이국적인 객체를 포함하여 모든 객체에 대해 예상되는 필수 불변 동작을 적용하기 위해 존재한다.
  • apply 트랩
    • apply 트랩은 호출 가능한 객체에 대한 [[Call]] 내부 작업을 위한 것이다
    • 프록시는 그 프록시하는 대상 객체에 하나가 있는 경우에만 [[Call]] 작업을 가진다.
    • apply 트 핸들러는 세 개의 인수를 받는다.
      • target: 프록시의 타깃
      • thisValue: 호출 시 this로 사용되는 값
      • args: 호출에 대한 인수의 배열
    • 트랩 핸들러는 기본 호출을 수행하거나 수행하지 않을 수 있으며 원하는 값을 반환하거나 오류를 발생시킬 수 있다.
    • 일부 다른 트랩 핸들러와 달리 apply 핸들러가 수행할 수 있는 작업에는 제한이 없다.
  • construct 트랩
    • construct 트랩은 생성자의 [[Construct]] 내부 연산을 위한 것이다.
    • 프록시는 프록시가 대상 객 체에 하나가 있는 경우에만 [[Construct] 작업을 가진다.함수)다.
    • construct 트랩은 세 개의 인수를 받는다.
      • target: 프록시의 타깃
      • args: 호출에 대한 인수의 배열
      • newTarget: new.target의 값
    • construct 트랩 핸들러에 대한 한 가지 제한 사항은 오류를 던지지 않고 무언가를 반환하는 경우 해당 무언가가 객체여야 한다는 것이다. null이나 프리미티브 타입을 반환하면 프록시에서 오류가 발생한다.
  • defineProperty 트랩
    • defineProperty 트랩은 [[DefineOwnProperty]] 내부 객체 작업을 위한 것이다.
    • [[Define OwnProperty]]는 객체에서 Object.defineProperty(또는 Reflect.defineProperty)를 호출할 때만 사용되는 것이 아니라 데이터 속성의 값이 설정되거나 속성이 할당을 통해 생성된다.
    • defineProperty 트랩은 세 개의 인수를 받는다.
      • target: 프록시의 타깃
      • propName: 정의/재정의할 속성의 이름
      • descriptor: 적용할 서술자
    • 성공 시 true를 반환하고 오류가 발생하면 false를 반환할 것으로 예상된다.
    • 트랩 핸들러는 변경 사항을 거부하거나 적용하기 전에 속성 설명자를 조정하거나, 기타 모든 트랩이 수행할 수 있는 작업을 거부할 수 있다.
    • 다음 코드는 대상의 기존 속성을 쓰기 불가능으로 만드는 것을 금지하는 간단한 트랩을 보여준다.
      1const obj = {};
      2const p = new Proxy(obj, {
      3  defineProperty(target, propName, descriptor) {
      4    if ('writable' in descriptor && !descriptor.writable) {
      5      const currentDescriptor = Reflect.getOwnPropertyDescriptor(target, propName);
      6      if (currentDescriptor && currentDescriptor.writable) {
      7        return false;
      8      }
      9    }
      10    return Reflect.defineProperty(target, propName, descriptor);
      11  },
      12});
      13p.a = 1;
      14console.log(`p.a = ${p.a}`);
      15console.log('Trying to make p.a non-writable...');
      16console.log(
      17  `Result of defineProperty: ${Reflect.defineProperty(p, 'a', {
      18    writable: false,
      19  })}`,
      20);
      21console.log('Setting pa.a to 2...');
      22p.a = 2;
      23console.log(`p.a = ${p.a}`);
  • deleteProperty 트랩
    • deleteProperty 트랩은 객체에서 속성을 제거하는 [[Delete]] 내부 객체 작업을 위한 것이다.
    • deleteProperty 트랩 핸들러는 두 개의 인수를 받는다.
      • target: 프록시의 타깃
      • propName: 삭제할 속성의 이름
    • 삭제 연산자가 느슨한 모드에서 하는 것처럼(엄격 모드에서 실패한 삭제는 오류를 먼짐) 성공시 true를 반환하고 오류 시 false를 반환할 것으로 예상된다.
    • 속성이 타깃에 있고 구성할 수 없는 경우 필수 불변 중 하나를 위반하므로 true를 반환할 수 없다.
    • 다음 예제는 value 속성 삭제를 거부하는 deleteProperty 트랩을 보여준다.
    1const obj = {};
    2const p = new Proxy(obj, {
    3  defineProperty(target, propName, descriptor) {
    4    if ('writable' in descriptor && !descriptor.writable) {
    5      const currentDescriptor = Reflect.getOwnPropertyDescriptor(target, propName);
    6      if (currentDescriptor && currentDescriptor.writable) {
    7        return false;
    8      }
    9    }
    10    return Reflect.defineProperty(target, propName, descriptor);
    11  },
    12});
    13p.a = 1;
    14console.log(`p.a = ${p.a}`);
    15console.log('Trying to make p.a non-writable...');
    16console.log(
    17  `Result of defineProperty: ${Reflect.defineProperty(p, 'a', {
    18    writable: false,
    19  })}`,
    20);
    21console.log('Setting pa.a to 2...');
    22p.a = 2;
    23console.log(`p.a = ${p.a}`);
  • get 트랩
    • get 트랩은 속성 값을 가져오는 [[Get]] 내부 객체 작업을 위한 것이다.
    • get 트랩 핸들러는 세 개의 인수를 받는다.
      • target: 프록시의 타깃
      • propName: 속성 이름
      • receiver: [[Get]] 호출을 받은 객체
    • get 트랩은 값 수정 등 원하는 거의 모든 작업을 수행할 수 있다. 단, 다른 트랩과 마찬가지로 특정 필수 불변량을 위반할 수는 없다.
  • getOwnPropertyDescriptor 트랩
    • getOwnPropertyDescriptor 트랩은 객체에서 속성에 대한 설명자 객체를 가져오는 [[GetOwn Property]] 내부 객체 작업을 위한 것이다.
    • [[GetOwnProperty]]는 코드가 Object.getOwnPropertyDescriptor 또는Reflect.getOwnPropertyDescriptor를 사용할 때뿐만 아니라 다른 내부 객체 작업을 처리하는 동안 여러 위치에서 호출된다.
    • getOwnPropertyDescriptor 트랩 핸들러는 두 개의 인수를 받는다.
      • target: 프록시의 타깃
      • propName: 속성의 이름
    • 속성 설명자 객체를 반환하거나 속성이 존재하지 않는 경우 undefined를 반환해야 한다. 다른 값을 반환하면 오류가 발생한다.
    • 일반적으로 Reflect.getOwnPropertyDescriptor에서 설명자 객체를 가져오지만 설명자 객체를 손으로 만들거나 해당 호출에서 얻은 것을 수정할 수도 있다.
    • getownPropertyDescriptor 트랩의 주요 사용 사례는 아마도 프록시를 사용하는 코드에서 타깃이 가진 속성을 숨기는 것이다.
  • getPrototypeOf 트랩
    • getPrototypeof 트랩은 [GetPrototypeOf]] 내부 객체 작업을 위한 것이다.
    • getPrototypeof 트랩은 Object 또는 Reflect 객체의 getPrototypeof 함수가 사용되거나 내부 작업이 프록시의 프로 토타입을 가져와야 할 때 트리거된다.
    • getPrototypeof 트랩은 하나의 인수만 받는다.
      • target: 프록시의 타깃
    • 아래 코드는 프로토타입이 속성 확인에 사용되더라도 타깃의 프로토타입을 숨기는 프록시를 보여준다.
    1const proto = {
    2  testing: 'one two three',
    3};
    4const obj = Object.create(proto);
    5const p = new Proxy(obj, {
    6  getPrototypeOf(target) {
    7    return null;
    8  },
    9});
    10console.log(p.testing); // one two three
    11console.log(Object.getPrototypeOf(p)); // null
  • has 트랩
    • has 트랩은 [[HasProperty]] 내부 객체 작업을 위한 것으로, 객체에 주어진 속성이 있는지 결정된다.
    • has 트랩은 두 개의 인수를 받는다.
      • target: 프록시의 타깃
      • propName: 속성의 이름
    • 속성이 있으면 true를 반환하고, 없으면 false를 반환할 것으로 예상된다.
    • has 트랩으로 분명히 할 수 있는 것은 객체가 가지고 있는 속성을 숨기거나 객체에 없는 속성이 있다고 주장하는 것이다.
  • isExtensible 트랩
    • isExtensible 트랩은 [[IsExtensible]] 내부 객체 작업을 위한 것으로, 객체가 확장 가능한지 확인한다.
    • isExtensible 트랩 핸들러는 단 하나의 인수를 받는다.
      • target: 프록시의 타깃
    • 객체가 확장 가능하면 true를 반환하고 그렇지 않으면 false를 반환할 것으로 예상된다.
    • isExtensible 트랩은 가장 제한된 트랩이다. 타깃 객체 자체가 반환했을 것과 동일한 값을 반환해야 한다.
  • ownKeys 트랩
    • ownKeys 트랩은 [[OwnPropertyKeys]] 내부 객체 작업을 위한 것으로, 열거할 수 없는 키와 문자열이 아닌 심볼로 이름이 지정된 키를 포함하여 객체 자체 속성 키의 배열을 생성한다.
    • ownKeys 트랩 핸들러는 단 하나의 인수를 받는다.
      • target: 프록시의 타깃
    • ownKeys 트랩 핸들러가 다음과 같은 배열을 반환하면 오류가 발생한다.
      • 중복이 있다.
      • 문자열이 아니고 심볼이 아닌 엔트리가 있다.
      • 타깃 객체가 확장할 수 없는 경우 누락되거나 추가 엔트리가 있다.
      • 타깃에 존재하는 구성할 수 없는 속성에 대한 엔트리가 누락되었다.
    • ownKeys의 일반적인 사용 사례 중 하나는성을 숨기는 것이다.
  • preventExtensions 트랩
    • preventExtensions 트랩은 객체를 확장할 수 없는 것으로 표시하는 [[PreventExtensions]] 내부 객체 작업을 위한 것이다.
    • preventExtensions 트랩 핸들러는 단 하나의 인수를 받는다.
      • target: 프록시의 타깃
    • 성공 시 true, 오류 시 false를 반환 핸들러는 아래와 같이 타깃이 확장 불가능해지는 것을 방지할 수 있다.
    1const obj = {};
    2const p = new Proxy(obj, {
    3  preventExtensions(target) {
    4    return false;
    5  },
    6});
    7console.log(Reflect.isExtensible(p)); // true
    8console.log(Reflect.preventExtensions(p)); // false
    9console.log(Reflect.isExtensible(p)); // true
  • set 트랩
    • set 트랩은 속성 값을 설정하는 [[Set]] 내부 객체 작업을 위한 것으로, 데이터 속성이나 접근가 속성 값이 설정될 때 트리거된다.
    • 트랩 핸들러가 작업을 허용하고 설정 중인 속성이 데이터 속성이면 데이터 속성 value를 설정하기 위해 defineProperty 트랩도 트리거된다.
    • set 트랩 핸들러는 4개의 인수를 받는다.
      • target: 프록시의 타깃
      • propName: 속성의 이름
      • value: 설정할 값
      • receiver: 작업을 받는 객체
    • 성공 시 true, 오류 시 false를 반환할 것으로 예상된다.
    • set은 구성할 수 없는 속성과 확장할 수 없는 대상 객체에서도 false를 반환하여 설정 값을 방지할 수 있다.
  • setPrototypeOf 트랩
    • setPrototypeof 트랩은 [[SetPrototypeOf]] 내부 객체 작업을 위한 것으로, 객체의 프로토타입을 설정한다.
    • setPrototypeof 트랩 핸들러는 두 개의 인수를 받는다.
      • target: 프록시의 타깃
      • newProto: 설정할 프로토타입
    • 성공 시 true, 오류 시 false를 반환할 것으로 예상된다.
    • 아래와 같이 새 프로토타입 설정을 거부할 수 있다.
    1const obj = { foo: 42 };
    2const p = new Proxy(obj, {
    3  setPrototypeOf(target, newProto) {
    4    // Return false unless `newProto` is already `target`'s prototype
    5    return Reflect.getPrototypeOf(target) === newProto;
    6  },
    7});
    8console.log(Reflect.getPrototypeOf(p) === Object.prototype); // true
    9console.log(Reflect.setPrototypeOf(p, Object.prototype)); // true
    10console.log(Reflect.setPrototypeOf(p, Array.prototype)); // false

14.2.3 예: 속성 숨기기

  • 불변 객체에 속성을 숨기는 것은 매우 쉽다. 객체에 대한 작업이 숨겨진 속성을 변경할 수 있을 때 속성을 숨기는 것은 다소 복잡하다.
  • 속성을 숨길 필요가 거의 없다는 점에 유의할 필요도 있다.
  • 대부분의 언어에는 작성자가 정말로 비공개 정보를 얻고자 할 때 사용할 수 있는 백도어(리플렉션을 통해)가 있다.
  • 대부분의 사용 사례에서 “이 속성에 접근하지 말라”라는 말과 간단한 명명 규칙으로 충분하다.
  • 위의 규칙으로 충분하지 않다면 다음과 같은 방법이 있다.
    • 프로토타입에 두지 않고 생성자에서 클로저로 메서드를 만들고 생성자 호출 범위 내의 변수/매개변수에 속성을 저장한다.
    • 위크맵을 사용하여 숨기려는 속성이 객체에 전혀 없도록 한다.
    • 프라이빗 필드를 사용한다.
    • 프록시를 사용하여 속성을 숨긴다.
  • 프록시를 사용하는 경우는 객체의 구현을 수정하지 않기 때문에 다른 세 가지를 사용할 수 없는 경우에 유용하다.
1class Counter {
2  constructor(name) {
3    this._value = 0;
4    this.name = name;
5  }
6  increment() {
7    return ++this._value;
8  }
9  get value() {
10    return this._value;
11  }
12}
13
14function getCounter(name) {
15  const proxies = new WeakMap();
16  const p = new Proxy(new Counter(name), {
17    get(target, name, receiver) {
18      if (name === '_value') {
19        return undefined;
20      }
21      let value = Reflect.get(target, name);
22      if (typeof value === 'function') {
23        let funcProxy = proxies.get(value);
24        if (!funcProxy) {
25          funcProxy = new Proxy(value, {
26            apply(funcTarget, thisValue, args) {
27              const t = thisValue === p ? target : thisValue;
28              return Reflect.apply(funcTarget, t, args);
29            },
30          });
31          proxies.set(value, funcProxy);
32          value = funcProxy;
33        }
34      }
35      return value;
36    },
37    getOwnPropertyDescriptor(target, propName) {
38      if (name === '_value') {
39        return undefined;
40      }
41      return Reflect.getOwnPropertyDescriptor(target, propName);
42    },
43    defineProperty(target, name, descriptor) {
44      if (name === '_value') {
45        return false;
46      }
47      return Reflect.defineProperty(target, name, descriptor);
48    },
49    has(target, name) {
50      if (name === '_value') {
51        return false;
52      }
53      return Reflect.has(target, name);
54    },
55    ownKeys(target) {
56      return Reflect.ownKeys(target).filter((key) => key !== '_value');
57    },
58  });
59  return p;
60}
61
62const p = getCounter('p');
63console.log('p.value before increment:');
64console.log(p.value); // 0
65console.log('p._value before increment:');
66console.log(p._value); // undefined
67const { increment } = Object.getPrototypeOf(p);
68increment.call(p);
69// => Throws TypeError: 'defineProperty' on proxy: trap returned falsish...
70console.log('p.value after increment:');
71console.log(p.value);
72console.log('p._value after increment:');
73console.log(p._value);
74console.log("'_value' in p:");
75console.log('_value' in p);
76console.log('Object.keys(p):');
77console.log(Object.keys(p));
78p._value = 42;
79console.log('p.value after changing _value:');
80console.log(p.value);
  • 프록시는 강력하지만 실제 사용에서는 잠재적으로 복잡하다는 것이다.

14.2.4 취소 가능한 프록시

  • 취소 가능한 프록시(revocable proxy)는 시간이 되면 제공한 객체(프록시)를 취소할 수 있기 때문에 특히 유용하다.
  • 프록시를 취소하면 다음 두 가지 중요한 작업이 수행된다.
    • 해지된 프록시에 대한 모든 작업이 오류와 함께 실패한다.
    • 프록시가 타깃 객체에 대해 가지고 있던 링크를 해제한다. 타깃에 대한 링크를 해제하는 프록시는 소비 코드가 여전히 프록시를 참조할 수 있지만 타깃 객체는 가비지 컬렉션될 수 있음을 의미한다.
  • 취소 가능한 프록시를 만들려면 new Proxy를 사용하는 대신 Proxy.revocable 메서드를 호출한다.
  • Proxy.revocable 메서드는 proxy 속성과 revoke 메서드가 있는 객체를 반환한다.
1const obj = { answer: 42 };
2const { proxy, revoke } = Proxy.revocable(obj, {});
3console.log(proxy.answer); // 42
4revoke();
5console.log(proxy.answer); // TypeError: Cannot perform 'get' on
6// a proxy that has been revoked
  • 프록시가 취소되면 사용 시도가 실패한다.

14.3 과거 습관을 새롭게

  • Reflect와 Proxy가 제공하는 기능은 근본적으로 자바스크립트의 새로운 기능이며 일반적으로 오래된 문제에 대한 새로운 해결책을 제공하기 보다는 새로운 문제를 해결한다.

14.3.1 API 객체를 수정하지 않기 위해 컨슈머 코드에 의존하는 대신 프록시 사용

  • 코드를 소비하기 위해 API 객체를 직접 제공하는 대신에 프록시를 제공하자. 이렇게 하면 코드에서 적절할 때 객체에 대한 모든 접근을 취소하는 것을 포함하여(취소 가능한 프록시를 제공하는 경우) 소비 코드가 객체에 대해 갖는 접근을 제어할 수 있다.

14.3.2 프록시를 사용하여 구현 코드와 계측 코드 분리

  • 프록시를 사용하여 계측 계층(객체 사용, 성능 등의 패턴을 결정하는 데 도움이 되도록 설계되니 코드)을 추가하고 객체 자체에 코드를 깔끔하게 유지하자.