본문 바로가기
javascript

기호 및 프록시를 사용하여 JavaScript 오브젝트 서명

by it-square 2022. 1. 19.
반응형

ECMAScript 6에는 Proxy()와 같은 강력한 새로운 API와 그 유용한 동료 Reflect가 포함되어 있습니다. 여기서는 프록시와 기호를 조합하여 객체에 대한 "비밀" 혼합물을 생성하는 방법에 대해 알아보려고 합니다.

아직 기호를 활용하지 않으셨다면 MDN 문서에서 기호를 읽어보시는 것이 좋습니다. 간단히 말해서:

  • 기호는 원시 요소이다.
  • 그것들은 문자열과 같은 물체의 열쇠로 사용될 수 있다.
  • 기호는 해당 기호가 생성된 참조를 통해서만 사용할 수 있습니다. 즉, 기호는 그 자체와 동일합니다.
  • 이전 버전 때문에 JSON에 직렬화하거나 JSON에서 부활할 수 없습니다.
  • 범위에 있는 기호에 액세스할 수 없으면 읽을 수 없습니다!

기호를 사용하여 기존 개체를 확장하고 개체의 기존 속성과 충돌하지 않는 특성을 추가할 수 있습니다. 즉, 기호는 안전한 "믹스인"을 만들거나 "브랜드화"하는 데 강력한 도구가 될 수 있다.

const entityTypeSym: unique symbol = Symbol();
type WithEntityType<
    T extends object,
        S extends symbol
> = T & {
            readonly [entityTypeSym]: S;
};
const USER: unique symbol = Symbol();
type WithUserType<T extends object> =
    WithEntityType<T, typeof USER>;
function withUserType<T extends object>(
    object: T
  ): WithUserType<T> {
    return {
      ...object,
     [entityTypeSym]: USER,
};
}
function isUserType<T>(value: T): value is WithUserType<T> {
    return value !== null && typeof value === "object" &&
      value[entityTypeSym] === USER;
}
const data = { username: "foo" } as const;
// data: { username: string }
const user = withUserType(data);
// user: { [entityTypeSym]: USER, username: string }
 

이 간단한 구현은 꽤 깔끔하지만 일부 부작용이 있습니다. 즉, 객체 위에서 반복하면 기호가 열거되고 혼합이 더 불투명해지기를 원합니다. Object.defineProperty()를 사용하여 해결할 수 있지만 일반적으로 Object.getOwnPropertyDescriptors()의 내부 구현을 통해 개체에 연결된 모든 속성에 액세스할 수 있습니다.

// :(
for (const prop in user) {
    alert(prop);
    // Uncaught TypeError: Cannot convert a Symbol value to a string
}

만약 우리가 기호에 의해 접근이 가능하도록 만들 수 있다면요? 하지만 실제로 물체에 부착되지 않은 채 말이죠. 그렇다면, 만약 우리의 주체가TypeSym 키는 비공개로 유지되며(모듈로 범위 지정), 모듈의 기능만 해당 값을 읽거나 쓸 수 있습니다.

이것은 Proxy() 덕분에 실제로 가능합니다!

function withUserType<T extends object>(
    object: T
  ): WithUserType<T> {
    return new Proxy(object, {
      get: (target, prop, receiver) => {
              if (prop === entityTypeSym) {
                        return USER;
              }
              return Reflect.get(target, prop, receiver);
      },
}) as WithUserType<T>;
}
 

withUserType의 반환 값은 지정된 개체를 래핑하고 개체의 속성에 액세스하기 위한 기본 제공 동작을 실제로 가로채는 전면 개체입니다! 따라서 다른 프록시 함수가 갇히지 않고 단순히 객체에 포워드 되는 동안, 우리는 개인 엔터티에 대한 액세스를 잡습니다.TypeSym 기호를 입력하고 해당 유형을 반환합니다.

프록시 객체의 다른 메서드는 갇히지 않으므로 Object.GetOwnPropertyDescriptors()와 같은 함수는 기호를 반환하지 않습니다. 즉, 모듈 외부에서는 코드가 프록시 객체를 통과할 수 있고 개인 데이터에 액세스할 수 없습니다!

그러나 기호에 액세스할 수 있는 모듈 내의 모든 함수는 다른 코드가 어떤 값도 조작할 수 없다는 확신을 가지고 기호에 읽고 쓸 수 있다.

function getType<T>(value: T): symbol | undefined {
    return value !== null && typeof value === "object" &&
          value[entityTypeSym];
}
getType(user); // Code outside this module can never read
               // entityTypeSym nor value[entityTypeSym]
               // without our getType() function

이걸 어디에 쓸까요?

 

나는 물건이 있다. 자주 사용하는 키의 수를 참조해야 하지만 일반적인 방법은 Object.keys(개체)입니다.length는 크기를 얻기 위해 열거 가능한 모든 키의 배열을 생성하는 오버헤드를 가지고 있기 때문에 약간 비싼 연산입니다.

프록시를 사용하여 객체의 속성 수를 추적하고 개인 기호를 사용하여 이 값에 액세스할 수 있다면 어떨까요?

function hasOwn<T>(
    object: T,
    key: string | symbol
  ): key is keyof T {
    return Object.prototype.hasOwnProperty.call(object, key);
}
const size: unique symbol = Symbol("size");
type WithSize<T extends object> = T & {
    readonly [size]: number;
};
function withSize<T extends object>(target: T): WithSize<T> {
    let s = Object.keys(target).length;
  return new Proxy(target, {
        get: (target, prop, receiver) => {
                if (prop === size) {
                          return s;
                }
                return Reflect.get(target, prop, receiver);
        },
        defineProperty: (target, prop, descriptor) => {
                const ret = Reflect.defineProperty(target, prop, descriptor);
                if (ret && !hasOwn(target, prop) && descriptor.enumerable) {
                          s++;
                }
                return ret;
        },
        deleteProperty: (target, property) => {
                const ret = Reflect.deleteProperty(target, property);
                if (ret) {
                          s--;
                }
                return ret;
        },
  });
}
function getSize<T extends object>(object: WithSize<T>): number {
    return object[size];
}
const obj = withSize({ foo: 1, bar: 2 });
obj.size; // 2
obj.baz = 3;
obj.size; // 3
delete obj.baz;
obj.size; // 2

꽤 깔끔하죠?

프록시와 심볼에 대한 다른 멋진 응용 프로그램들이 있으면 댓글로 알려주세요!

 

댓글