Proxy, Reflect 그리고 Reflect-metadata 라이브러리를 알아보자

toto9602·2023년 12월 16일
2

Javascript, Typescript

목록 보기
1/2

Typescript 기반의 Nest JS 프레임워크는 데코레이터의 활용 등을 위해,
프레임워크 차원에서 reflect-metadata 라이브러리를 내장하고 있습니다.

Nest JS 프레임워크와 데코레이터 등을 사용해 본 경험은 있지만,
Nest JS의 Reflector, Reflect, metadata 등에 대해 이해하고 있었던 것 같지는 않아,
본 포스팅을 통해 공부해 보고자 합니다!

학습을 위해 작성하는 글이므로, 잘못된 내용이 포함되어 있을 수 있습니다.
잘못된 내용에 대한 지적은 항상 감사드립니다!! :)

참고 자료

Javascript Info : Proxy와 Reflect
MDN 문서 : Reflect
김정환 님 블로그 : 리플렉트 메타데이터
Javascript Proxy. 근데 이제 Reflect를 곁들인
프로퍼티 어트리뷰트
Metadata Proposal - ECMAScript
자바스크립트에서 globalThis의 소름끼치는 폴리필
reflect-metadata 소스 코드

들어가기 전에 : Proxy

MDN에 따르면, 본 포스팅의 주요 주제인 ReflectProxy와 동일한 메서드 구성을 가지고 있고,
Proxy를 쉽게 만들기 위한 built-in object라고 설명되기도 합니다.

왠지, Reflect 를 이해하기 위한 중요한 개념인 것 같으니, Reflect로 들어가기 전에 간단히 살펴보려 합니다!

Proxy에 대한 내용은 주로 아래 글에서 참고하였습니다.

[ Javascript Info : Proxy와 Reflect ]

기본 개념 : Proxy란?

특정 객체를 감싸 프로퍼티 읽기, 쓰기와 같이 객체에 가해지는 작업을 중간에 가로채는 객체

→ 가로채진 작업은 Proxy에서 처리될 수도 있지만, 원래 객체가 처리하도록 그대로 전달될 수도 있다.

기본 문법

let proxy = new Proxy(target, handler)
  • target : Proxy가 감쌀 객체
  • handler : 동작을 가로채는 메서드를 담은 객체
    → 여기서 동작을 가로채는 메서드를 트랩(trap)이라고 합니다.

사용 예시

'프로퍼티 읽기' 가로채기 : get 트랩

'프로퍼티 읽기' 동작을 가로채기 위해서는, handler가 아래 get 메서드를 갖고 있어야 합니다.

function get(target, property, receiver)
  • target : 동작을 전달할 객체
  • property : 프로퍼티 이름
  • receiver : 최초로 작업 요청을 받은 객체
    → 타깃 프로퍼티가 getter라면, receiver는 getter가 호출될 때 this이고,
    대개는 Proxy 객체 자신이 this가 된다.

cf. receiver에 대한 정의는 아래 글에서 참고하였습니다.
본 포스팅의 이하 예제 코드에서는 receiver가 사용되지 않아 추가로 정리하지 않지만, JS의 프로토타입 체이닝과 관련된 흥미로운 개념이어서 참고하시면 좋을 것 같습니다!

[ Javascript Proxy. 근데 이제 Reflect를 곁들인 ]

[ get 트랩 사용 예시 ]

let christmas = {
	timestamp:`12/25`,
	santa:true
};

christmas = new Proxy(christmas, {
  get(target, prop) { // 프로퍼티 읽기 동작을 가로채는 트랩
  	if (prop in target) { // 존재하는 property일 경우
    	return target[prop]; // 해당 property를 반환
    }
    return prop; // 존재하지 않는 property라면 해당 값을 그대로 반환
})
  
  
console.log(christmas['santa']) // true
console.log(christmas['rudolph']) // 'rudolph'

'프로퍼티 쓰기' 가로채기 : set 트랩

'프로퍼티 쓰기' 동작을 가로채기 위해서는, handler가 아래 set 메서드를 갖고 있어야 합니다.

function set(target, property, value, receiver)
  • target : 동작을 전달할 객체 (get과 동일합니다)
  • property : 프로퍼티 이름 (get과 동일합니다)
  • value : 프로퍼티 값
  • receiver : get 트랩과 유사하게 동작하는 객체

[ set 트랩 사용 예시 ]

let names = [];

names = new Proxy(names, {
	set(target, prop, value) {
      	if (prop === "length") return true; // Array.push 사용시 에러 방지
      
    	if (typeof value === 'string') { // 문자열 타입이면
        	target[prop] = value; // 값을 target에 할당하고
          	return true; // true를 반환합니다 
        }
    	return false; // 문자열 타입이 아니라면 false를 반환합니다. 
    }
})

// push, unshift 등의 메서드들은 내부적으로 [[Set]]을 사용
names.push("산타") // OK
names.push("루돌프") // OK
names.push(12.25) // TypeError

[ cf. set 트랩의 반환값 ]

  • set 트랩을 사용할 때, 값을 쓰는 게 성공했다면 true를 반환해야 합니다!
  • falsy한 값을 반환한다면 TypeError가 발생!

[ cf 2. Array push 메서드 ]

참고한 글에서는, set 트랩 예제에 number의 배열을 사용합니다.

저는 string 배열로 해 보고 싶어서 배열에 넣는 값만 바꾸었는데,
.length 호출 관련 에러가 발생했습니다..!

알고 보니 push 메서드가 아래와 유사하게 구현되어 있어서, string이 아닐 때 다 false를 던지면 에러가 나는 거였네요...;

[ 출처 : 스택 오버플로우, How does the Javascript Array Push code work internally ]

function push(value) {
  var len = this.length;
  this[len] = value;
  len++;
  this.length = len;
  return len;
}

Array.push 구현 방식 오늘 처음 안 사람

Proxy까지 오는 데만도 많은 난관이 있었던 느낌이네요..ㅠ

그래도 이제 드디어 Reflect로 넘어가 보겠습니다!!

Reflect

  • Reflect는 Proxy와 같이 JavaScript 명령을 가로챌 수 있는 메서드를 제공하는 내장 객체이다.
  • Proxy handler의 모든 트랩을 Reflect의 내장 메서드가 동일한 인터페이스로 지원한다.

→ 메서드 구성 등, Proxy와 유사한
자바스크립트에서 리플렉션(Reflection)을 쉽게 구현할 수 있도록 하기 위한 API! 정도로 이해하였습니다!

리플렉션(Reflection)이란?

→ 스스로 메타언어가 되어 자기 자신을 프로그래밍(메타 프로그래밍)할 수 있는 언어가 되는 것

→ Reflect를 활용한 예시를 보며, 조금 더 이해해 보겠습니다!

Reflect를 활용한 메타 프로그래밍

Reflect는 Proxy의 handler와 동일한 메서드 구성을 갖고 있기 때문에,
앞서 살펴 본 get, set 메서드 등을 동일한 인터페이스로 사용할 수 있습니다!

이하 예제에서는, 위의 두 메서드 외에,
Reflect를 사용한 메타 프로그래밍의 예시 하나를 보려 합니다!

function foo() {}

console.log(foo.name); // "foo"

foo.name = 'Bar';

console.log(foo.name) // "foo"

위 코드에서는, foo 함수의 이름을 bar로 할당했지만, 다시 출력해 보면 그대로 foo가 출력됩니다.

이는 함수의 name 필드가 읽기 전용으로, 수정할 수 없기 때문입니다.
그리고 이는, 곧 함수의 name 필드는 변경할 수 없다는 정보가 "어딘가"에 기록되어 있다는 뜻이기도 합니다.
→ 내부 슬롯의 Writable

console.log(Reflect.getOwnPropertyDescriptor(foo, "name"));

/*
{
	configurable: true,
    enumerable: false,
    value: "foo",
    writable: false
*/

내부 슬롯과, 프로퍼티 어트리뷰트의 자세한 내용은 아래 글에서 참고하였습니다!

cf. 프로퍼티 어트리뷰트

그렇다면, name 필드는 변경할 수 없다-는 프로그램 자체를 수정한다면?!

Reflect.defineProperty(foo, "name", {
	writable: true,
});

foo.name = "bar";

console.log(foo.name) // 'bar' ==> 성공!

프로그램을 통해, 데이터를 변경하듯
함수의 name은 변경할 수 없다는 "프로그램 자체를 변경"하고,

함수의 name을 바꾸는 데도 성공하였습니다!

→ 프로그램을 통해, 자기 자신(프로그램)을 프로그래밍한
메타 프로그래밍!

Reflect의 한계

자바스크립트 내장 객체인 Reflect를 통해, 자바스크립트에 이미 정의된 속성을 다룰 수 있게 되었습니다!
그런 반면, 한계점도 있습니다.
Reflect만으로는 특정 어플리케이션에서만 다루는 도메인 데이터를, 프로그램 수준에 저장할 수 없기에
자유로운 메타 프로그래밍을 하기에 어려움이 있습니다.

그리고, 이런 한계를 극복하기 위해 나온 라이브러리 (구현체)가,
NestJS에서도 사용되는 reflect-metadata입니다!

cf. Metadata Proposal - ECMAScript
사실 선후관계로는, reflect-metadata 라이브러리가 나오기 전,
메타데이터를 저장할 내부 슬롯을 추가하고, 이에 접근하는 Reflect API를 추가하는 제안이 있었다고 합니다.

그러나 제안 상태에 머물러 있고, 이에 위 제안이 수락되기 전에 reflect-metadata 라이브러리 구현체가 만들어졌다고 하네요!

Reflect-metadata

해당 라이브러리에서 가장 대표적으로(?) 많이 사용되는 메서드인 defineMetadata, getMetadata를 중심으로, 라이브러리의 동작을 살펴볼 예정입니다!

기초 메서드 문법

[ 메타데이터의 등록 ]

Reflect.defineMetadata(metadataKey, metadataValue, target);

[ 메타데이터의 조회 ]

Reflect.getMetadata(metadataKey, target)

[ 사용 예시 ]

import "reflect-metadata"

function foo() {}


Reflect.defineMetadata("version", 1, foo); // version이라는 키에, 1이라는 값을, foo라는 함수에 등록

console.log(Reflect.getMetadata("version", foo)) // version이라는 키로, foo에 등록된 메타데이터를 조회

Reflect-metadata 코드 읽기

위에 예시로 작성한 함수를 호출한다고 가정하고, 살펴 보려고 합니다! :)

들어가기 전에 : 살펴 볼 함수 구조

(function (exporter, root) {
  ...
  ...
  const metadataRegistry = GetOrCreateMetadataRegistry();
  const metadataProvider = CreateMetadataProvider(metadataRegistry);
  ...
  ...
  function defineMetadata(metadataKey:any, metadataValue:any, target:any, propertyKey?:string |symbol):void;
  
  exporter("defineMetadata", defineMetadata);
  
  function getMetadata(metadataKey: any, target: any, propertyKey: string | symbol): any;
  

  exporter("getMetadata", getMetadata);
  
  function GetOrCreateMetadataRegistry():MetadataRegistry;
  
  function CreateMetadataRegistry(): MetadataRegistry;
  
  function CreateMetadataProvider(registry:MetadataRegistry):MetadataProvider {
    function OrdinaryDefineOwnMetadata(MetadataKey: any, MetadataValue: any, O: object, P: string | symbol | undefined): void {
      const metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ true);
      metadataMap.set(MetadataKey, MetadataValue);
     }
   }
     
  function OrdinaryGetMetadata(MetadataKey: any, O: any, P: string | symbol | undefined): any {
  
  function OrdinaryDefineOwnMetadata(MetadataKey: any, MetadataValue: any, O: any, P: string | symbol | undefined): void {
     const provider = GetMetadataProvider(O, P, /*Create*/ true);
     provider.OrdinaryDefineOwnMetadata(MetadataKey, MetadataValue, O, P);
   }
 })

Reflect.ts 파일을 보면, 함수 안에 metadatRegistrymetadataProvider 변수가 존재하고, 같은 계층에 살펴보고자 하는 함수 두 개가 정의되어 있습니다!
그리고 각각의 함수들을 exporter를 통해, 외부로 노출시키는 구조인 것 같네요..!

metadataRegistrymetadataProvider를 스코프 내의 함수들에서 사용하고, 필요한 함수를 exporter를 통해 내보내기 위한 방식인 것 같네요..!

한 가지 더, OrdinaryDefineOwnMetadata 함수가 스코프에 따라 2개 존재하는 것도 볼 수 있네요!
필요한 함수들에 대해서는 아래에서 로직을 볼 예정이기에, 여기서는 함수 선언만 떼어서 작성하였지만, 이 함수만은 구분을 위해 우선 구현 부분을 포함해 두었습니다.

우선은 이렇게 간단한 구조만 본 상태로,
metadataRegistrymetadataProvider를 시작으로
defineMetadatagetMetadata의 동작을 알아보겠습니다!

GetOrCreateMetadataRegistry

/**
  * Gets or creates the shared registry of metadata providers.
*/
function GetOrCreateMetadataRegistry(): MetadataRegistry {
  let metadataRegistry: MetadataRegistry | undefined;
  // const registrySymbol = typeof Symbol === "function" ? Symbol.for("@reflect-metadata:registry") : undefined;
  // Symbol이 support되는지 확인 후, Symbol.for("@reflect-metadata:registry")을 사용!
  if (!IsUndefined(registrySymbol)
      && IsObject(root.Reflect) // root는 전역 스코프의 this..?
      && Object.isExtensible(root.Reflect)) { //  객체가 새로운 property를 가질 수 있는가?
            // 전역 객체의 Reflect에 해당 Symbol로 접근합니다
            metadataRegistry = (root.Reflect as any)[registrySymbol] as MetadataRegistry | undefined;
     }
  
  
  if (IsUndefined(metadataRegistry)) {
           // undefined라면, 새로운 MetadataRegistry를 등록합니다.
           // 아래에서 조금 더 자세히 보려고 합니다!
           metadataRegistry = CreateMetadataRegistry();
     }
  
   if (!IsUndefined(registrySymbol) 
       && IsObject(root.Reflect) 
       && Object.isExtensible(root.Reflect)) {
            // 전역 Reflect의 symbol키에 등록된 registry의 property를 정의합니다!
            Object.defineProperty(root.Reflect, registrySymbol, {
                    enumerable: false,
                    configurable: false,
                    writable: false,
                    value: metadataRegistry
                });
            }
            return metadataRegistry;
}

개인적으로 낯선 코드가 많아, 조금 헷갈리지만ㅠ
기본적으로는 root.Reflect에 해당 Symbol을 통해 접근할 수 있는metadataRegistry가 없다면 새로 만들고, 이미 있다면 기존 값을 사용 후 registry에 property를 정의하고 반환하는! 느낌인 것 같습니다.

[ cf. root ]

위 코드에서 사용하는 root는 아래 코드와 같았습니다!

const root =
     typeof globalThis === "object" ? globalThis :
     typeof global === "object" ? global :
     typeof self === "object" ? self :
     typeof this === "object" ? this :
     sloppyModeThis();

아래 글에, globalThis의 폴리필에 대해 굉장히 설명이 잘 되어 있었는데,
로직이 상당히 비슷하여, 우선 globalThis의 폴리필 정도로 이해하고 넘어가려 합니다!

사실 정확히 이해 못했습니다..ㅠㅠ 다음 기회에 공부해 보겠습니다...

자바스크립트에서 globalThis의 소름끼치는 폴리필

[ cf 2. Symbol.for("@reflect-metadata:registry") ]

reflect-metadata 라이브러리의 소스 코드를 보면, Reflect, ReflectLite, ReflectNoConflict가 구분되어 있는데,
파일마다 다른 Registry를 root.Reflect에 두기 위한 부분인 것 같습니다..!

하지만 막상 ReflectNoConflict의 의미는 잘 모르겠는...ㅠㅠ

이제 조금 더 자세히 보기로 한, CreateMetadataRegistry 함수를 보겠습니다!

< CreateMetadataRegistry >

// Global metadata registry
// - Allows `import "reflect-metadata"` and `import "reflect-metadata/no-conflict"` to interoperate.
// - Uses isolated metadata if `Reflect` is frozen before the registry can be installed.

/**
 * Creates a registry used to allow multiple `reflect-metadata` providers.
*/
function CreateMetadataRegistry(): MetadataRegistry {
    let fallback: MetadataProvider | undefined;
    if (!IsUndefined(registrySymbol) &&
        typeof root.Reflect !== "undefined" &&
        !(registrySymbol in root.Reflect) &&
        typeof root.Reflect.defineMetadata === "function") {
                // interoperate with older version of `reflect-metadata` that did not support a registry.
                fallback = CreateFallbackProvider(root.Reflect);
       }

    let first: MetadataProvider | undefined;
    let second: MetadataProvider | undefined;
    let rest: Set<MetadataProvider> | undefined;
  	// WeakMap을... 사용하고 있네요..!!!
    const targetProviderMap = new _WeakMap<object, Map<string | symbol | undefined, MetadataProvider>>();
    
    // 이하에서 등장하는, 중첩 함수들을 객체의 형태로 반환하고 있네요!
    const registry: MetadataRegistry = {
        registerProvider,
        getProvider,
        setProvider,
    };
    return registry;
  	
  	// registerProvider, getProvider, setProvider 등의 중첩 함수가 이어지네요!
  	function registerProvider(provider:MetadataProvider) {
     	...
        // first, second 변수 순으로 매개변수의 provider를 할당합니다.
        // 둘 다 할당되었다면, rest의 Set에 provider를 더합니다. 
    }
    
    // 
    function getProvider(
      O: object, // function foo
      P: string | symbol | undefined // undefined
    ) {
      // targetProviderMap에서 O(object)로 providerMap을 조회합니다. 
      // providerMap에서 P(propertyKey)로 provider를 조회하여 반환합니다. 
       ...
    }
       
    function setProvider(
      O: object, // function foo
      P: string | symbol | undefined, // undefined
      provider: MetadataProvider
    ) {
      // targetProviderMap에 O(object)로 매핑된 providerMap에,
      // propertyKey로 provider를 등록합니다. 
      ...
    }

분량상, CreateMetadataRegistry에서 반환하는 registerProvider, getProvider, setProvider 함수는 간단한 내용만 적었습니다!

[ 소스 코드 ]

새로운 WeakMap을 생성하고,
해당 WeakMap을 사용하는 중첩 함수들을 호출할 수 있도록 registry로 반환하는 부분이 주요 로직인 것 같습니다!

WeakMap을 사용하는 코드도,
중첩 함수를 활용하는 코드도 처음 봐서 신기한 것 투성이네요...!

[ cf. _WeakMap ]

const _WeakMap: typeof WeakMap = typeof WeakMap === "function" ? WeakMap : CreateWeakMapPolyfill();

→ WeakMap 타입이 function으로 잡히면, WeakMap을 사용하고
아니면 폴리필을 작성해서 사용하고 있네요!

** 중첩 함수에 대한 내용도, 전 해당 코드로 처음 접한 셈이라...ㅎㅎ
아래 글에서 잘 정리해 주신 내용을 참고하였습니다!

> 자바스크립트의 중첩 함수(Nested Functions)는 언제 사용해야 할까?

CreateMetadataProvider

위에서 살펴본 GetOrCreateMetadataRegistry 함수에서 반환되는 값을 매개변수로 받아, metadataProvider를 할당해 주는 함수를 볼 차례네요!

function CreateMetadataProvider(registry: MetadataRegistry): MetadataProvider {
// [[Metadata]] internal slot
// https://rbuckton.github.io/reflect-metadata/#ordinary-object-internal-methods-and-internal-slots
  
    // 여기도 WeakMap이 쓰이고 있네요!
    // 새로운 WeakMap을 만들어 주고
    const metadata = new _WeakMap<any, Map<string | symbol | undefined, Map<any, any>>>();
    // 해당 WeakMap을 사용하는 provider (isProviderFor 함수를 가진 객체)를 만들어주네요!
    const provider: MetadataProvider = {
        isProviderFor(O, P) {
            const targetMetadata = metadata.get(O);
            if (IsUndefined(targetMetadata)) return false;
          
            return targetMetadata.has(P);
        }
		...	
      	...
    };
  
    metadataRegistry.registerProvider(provider);
  
    return provider;
  	
  	// 이 함수에서도 중첩 함수가 이어지네요!
    function GetOrCreateMetadataMap(O: object, P: string | symbol | undefined, Create: false): Map<any, any> | undefined;
  
    function OrdinaryGetOwnMetadata(MetadataKey: any, O: object, P: string | symbol | undefined): any;
                
    function OrdinaryDefineOwnMetadata(MetadataKey: any, MetadataValue: any, O: object, P: string | symbol | undefined): void;
       ...
       ...

이 함수에서 매개변수로 registry를 받은 것은,
이하의 중첩 함수들에서 registry에 접근하기 위함이었던 것 같네요!

여기까지, metadataRegistry 변수와 metadataProvider에 대한 간단한 느낌? 정도는 잡은 것 같습니다!
나머지 동작들은 드디어! 보고 싶었던 함수들을 살펴 보며 알아보겠습니다!!

defineMetadata

function defineMetadata(
  metadataKey: any, // "version"
  metadataValue: any, // 1
  target: any, // function foo
  propertyKey?: string | symbol // undefined
): void {
  if (!IsObject(target)) throw new TypeError();
  if (!IsUndefined(propertyKey)) propertyKey = ToPropertyKey(propertyKey);
  
  // 같은 계층에 있는 OrdinaryDefineOwnMetadata 함수를 호출!
  return OrdinaryDefineOwnMetadata(
    metadataKey,
    metadataValue,
    target,
    propertyKey
  )
}

이 함수 자체에는 크게 복잡한 내용은 없는 것 같습니다!
OrdinaryDefineOwnMetadata를 호출하는 부분이 사실상 주요 로직이네요..!

OrdinaryDefineOwnMetadata 함수로 가 보겠습니다!

OrdinaryDefineOwnMetadata

 function OrdinaryDefineOwnMetadata(
	MetadataKey: any, // "version"
    MetadataValue: any, // 1
    O: any, // target - function foo()
    P: string | symbol | undefined // undefined
): void {
  // 같은 계층에 있는 GetMetadataProvider 함수를 호출합니다!
  const provider = GetMetadataProvider(O, P, /*Create*/ true);
  // "version", 1, foo, undefined    
  provider.OrdinaryDefineOwnMetadata(MetadataKey, MetadataValue, O, P);
}

이 함수에서는 O (target)과, P (propertyKey)를 가지고 GetMetadataProvider를 호출하네요!

로직을 알기 위해서는 GetMetadataProvider까지 가 봐야 할 것 같네요..ㅎㅎ

GetMetadataProvider 까지 가 보겠습니다!

GetMetadataProvider

function GetMetadataProvider(
  O: object, // target - function foo()
  P: string | symbol | undefined, // undefined
  Create: boolean
): MetadataProvider | undefined {
  // 위에서 살펴본 metadataRegistry를 드디어 사용하네요!
  // 앞서 살펴 본 CreateMetadataRegistry에서 getProvider를 반환했으므로,
  // 여기서 사용할 수 있습니다! :)
  const registeredProvider = metadataRegistry.getProvider(O, P);
  
  if (!IsUndefined(registeredProvider)) { // 등록된 provider가 있다면
      return registeredProvider; // 그대로 반환!
      // => 반환된 provider를 통해, `CreateMetadataRegistry` 함수에서 할당한 targetProviderMap에 접근할 수 있겠네요!
  }

// 등록된 provider는 없고, 
  if (Create) { // Create가 true라면
    // 역시, CreateMetadataRegistry에서 반환된 setProvider에 접근!
    if (metadataRegistry.setProvider(O, P, metadataProvider)) {  
        // 위에서 본 metadataProvider의 set이 성공해서 true가 반환된다면
    	// metadataProvider를 반환합니다.
        return metadataProvider;
        }
    // setProvider가 falsy라면 에러
    throw new Error("Illegal state.");
  }
 
 // Create가 true가 아니라면, undefined 반환
 return undefined;
 }

→ 요 함수에서는, MetadataProvider를 반환하네요!
이 반환된 metadataProvider를 통해, metadataRegistry에 값을 할당할 때, 내부적으로 할당된 targetProviderMap에 접근할 수 있을 것 같습니다!

이제, OrdinaryDefineOwnMetadata에서 호출했던,provider.OrdinaryDefineOwnMetadata(MetadataKey, MetadataValue, O, P); 코드로 돌아가 보겠습니다! :)

CreateMetadataProvider - OrdinaryDefineOwnMetadata

function OrdinaryDefineOwnMetadata(
  MetadataKey: any, // "version"
  MetadataValue: any, // 1
  O: object, // function foo
  P: string | symbol | undefined // undefined
): void {
  // 같은 CreateMetadataProvider 스코프의 GetOrCreateMetadataMap에 접근합니다. 
  const metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ true);
  // "version"을 키로, 1이라는 value를 등록합니다! 끝!!
  metadataMap.set(MetadataKey, MetadataValue);
}

CreateMetadataProvider - GetOrCreateMetadataMap

function GetOrCreateMetadataMap(
  O: object, // function foo
  P: string | symbol | undefined, // undefined
  Create: boolean // true
) {
  // CreateMetadataProvider에서 만든 metadata
  // _WeakMap<any, Map<string | symbol | undefined, Map<any, any>>>() 에 접근합니다!
  let targetMetadata = metadata.get(O); // foo 함수의 메타데이터를 조회
  let createdTargetMetadata = false;
  if (IsUndefined(targetMetadata)) { // 데이터가 없고,
      if (!Create) return undefined; // Create가 false라면 undefined
      // 아니라면, target의 메타데이터 저장을 위한 Map을 만들고
      targetMetadata = new _Map<string | symbol | undefined, Map<any, any>>();
       // metadata WeakMap에, foo 함수를 key로, 만든 Map을 등록!
      metadata.set(O, targetMetadata);
      createdTargetMetadata = true;
  }
  let metadataMap = targetMetadata.get(P); // foo 함수의 메타데이터 중,  P 키로 등록된 메타데이터를 조회
  if (IsUndefined(metadataMap)) {
    // 없다면, undefined를 반환하거나 
      if (!Create) return undefined;
    // property의 메타데이터를 저장할 Map을 만들고, foo의 메타데이터 Map에 set합니다. 
      metadataMap = new _Map<any, any>();
      targetMetadata.set(P, metadataMap);
      if (!registry.setProvider(O, P, provider)) {
         targetMetadata.delete(P);
         if (createdTargetMetadata) {
              metadata.delete(O);
         }
      throw new Error("Wrong provider for target.");
      }
   }
 // 최종적으로는, foo함수의 메타데이터를 저장하는 Map을 반환
   return metadataMap;
}

휴, 여기까지 얼추 defineMetadata를 호출했을 때 일어나는 일들을 살펴 보았네요!
getMetadata까지 마저 빠르게 보겠습니다! :)
(이쪽은 좀 더 짧았으면...)

getMetadata

function getMetadata(
  metadataKey: any, // "version"
  target: any, // function foo
  propertyKey?: string | symbol // undefined
): any {
    if (!IsObject(target)) throw new TypeError();
    if (!IsUndefined(propertyKey)) propertyKey = ToPropertyKey(propertyKey);
  
    return OrdinaryGetMetadata(metadataKey, target, propertyKey);
        }

target과 key에 대한 검사 후, OrdinaryGetMetadata를 호출하네요!

앞서 살펴 본, defineMetadata와 구조가 거의 같으니, 빠르게 넘어가겠습니다! :)

OrdinaryGetMetadata

// 3.1.3.1 OrdinaryGetMetadata(MetadataKey, O, P)
// https://rbuckton.github.io/reflect-metadata/#ordinarygetmetadata
function OrdinaryGetMetadata(
  MetadataKey: any, // "version"
  O: any, // function foo
  P: string | symbol | undefined // undefined
): any {
    const hasOwn = OrdinaryHasOwnMetadata(MetadataKey, O, P);
    if (hasOwn) return OrdinaryGetOwnMetadata(MetadataKey, O, P);
  	
    // 객체의 프로토타입을 조회해서
    const parent = OrdinaryGetPrototypeOf(O);
    
    // 재귀 호출을 하고 있는 것 같네요...!
  if (!IsNull(parent)) return OrdinaryGetMetadata(MetadataKey, parent, P);
    
  return undefined;
}

여기서는 foo 함수인, O가 메타데이터를 갖고 있는지 확인하고,
없다면 parent인 프로토타입을 따라가면서 재귀 호출을 하고 있는 것 같네요!

OrdinaryHasOwnMetadata, OrdinaryGetOwnMetadata 까지 이어서 보겠습니다!

OrdinaryHasOwnMetadata

// 3.1.2.1 OrdinaryHasOwnMetadata(MetadataKey, O, P)
// https://rbuckton.github.io/reflect-metadata/#ordinaryhasownmetadata
function OrdinaryHasOwnMetadata(
  MetadataKey: any, // "version"
  O: object, // target // function foo
  P: string | symbol | undefined // undefined
): boolean {
  // 여기서는 Create가 false이므로, 
  // undefined, 혹은 유효한 Map이 반환되겠네요!
  // 앞서 살펴 본 함수이니, 넘어가겠습니다! 
  const metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ false);
    
  if (IsUndefined(metadataMap)) return false;
  // 있다면, Map에 key로 값이 들어있는지 확인!
  return ToBoolean(metadataMap.has(MetadataKey));
}

OrdinaryGetOwnMetadata

// 3.1.4.1 OrdinaryGetOwnMetadata(MetadataKey, O, P)
// https://rbuckton.github.io/reflect-metadata/#ordinarygetownmetadata
function OrdinaryGetOwnMetadata(
  MetadataKey: any, // "version"
  O: object, // function foo
  P: string | symbol | undefined // undefined
): any {
  const metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ false);
  if (IsUndefined(metadataMap)) return undefined;
  return metadataMap.get(MetadataKey);
}

조회한 Map에서 key로 값을 조회하는, 비교적 간단한 함수인 것 같네요! :)

메타데이터를 찾거나, 더 이상 parent가 없을 때까지 재귀 호출을 하는 구조인 것 같습니다!

결론

자바스크립트의 Proxy 객체, Proxy와 많은 부분이 닿아있는 Reflectreflect-metadata 라이브러리에 대해 간단히 공부해 보았습니다!

Proxy와 Reflect는 동일한 메서드 구성을 갖고,
Reflect는 리플렉션, 메타 프로그래밍을 위한 자바스크립트의 API로 볼 수 있었습니다.

그리고, Reflect의 한계를 넘기 위해 제안된, reflect-metadata 라이브러리는, WeakMap과 Map을 통해 target에 메타데이터를 저장한다는 점까지 함께 볼 수 있었습니다!

Reflect와 메타데이터가 궁금해서 시작한 글이지만,
정작 WeakMap, 자바스크립트의 중첩 함수 등 이것저것 낯선 것들을 많이 봐서 좋네요 :)
다음 기회에는, 제가 이 호기심을 갖게 해 준 NestJS에서
이 라이브러리를 사용하는 예시를 조금 살펴보며, 메타데이터의 흐름을 조금 더 이해해 보고 싶습니다 :)

길고 어지러운 글, 관심 가져주셔서 감사합니다!

profile
주니어 백엔드 개발자입니다! 조용한 시간에 읽고 쓰는 것을 좋아합니다 :)

0개의 댓글