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 소스 코드
MDN에 따르면, 본 포스팅의 주요 주제인 Reflect
는 Proxy
와 동일한 메서드 구성을 가지고 있고,
Proxy
를 쉽게 만들기 위한 built-in object라고 설명되기도 합니다.
왠지, Reflect
를 이해하기 위한 중요한 개념인 것 같으니, Reflect
로 들어가기 전에 간단히 살펴보려 합니다!
Proxy에 대한 내용은 주로 아래 글에서 참고하였습니다.
[ Javascript Info : Proxy와 Reflect ]
→ 특정 객체를 감싸 프로퍼티 읽기, 쓰기와 같이 객체에 가해지는 작업을 중간에 가로채는 객체
→ 가로채진 작업은 Proxy
에서 처리될 수도 있지만, 원래 객체가 처리하도록 그대로 전달될 수도 있다.
let proxy = new Proxy(target, handler)
트랩(trap)
이라고 합니다. '프로퍼티 읽기' 동작을 가로채기 위해서는, handler
가 아래 get
메서드를 갖고 있어야 합니다.
function get(target, property, receiver)
this
이고,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'
'프로퍼티 쓰기' 동작을 가로채기 위해서는, handler
가 아래 set
메서드를 갖고 있어야 합니다.
function set(target, property, value, receiver)
get
과 동일합니다)get
과 동일합니다)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
를 반환해야 합니다!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로 넘어가 보겠습니다!!
→ 메서드 구성 등, Proxy
와 유사한
자바스크립트에서 리플렉션(Reflection)을 쉽게 구현할 수 있도록 하기 위한 API! 정도로 이해하였습니다!
→ 스스로 메타언어가 되어 자기 자신을 프로그래밍(메타 프로그래밍)할 수 있는 언어가 되는 것
→ 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
*/
내부 슬롯과, 프로퍼티 어트리뷰트의 자세한 내용은 아래 글에서 참고하였습니다!
그렇다면, name 필드는 변경할 수 없다-는 프로그램 자체를 수정한다면?!
Reflect.defineProperty(foo, "name", {
writable: true,
});
foo.name = "bar";
console.log(foo.name) // 'bar' ==> 성공!
프로그램을 통해, 데이터를 변경하듯
함수의 name은 변경할 수 없다는 "프로그램 자체를 변경"하고,
함수의 name을 바꾸는 데도 성공하였습니다!
→ 프로그램을 통해, 자기 자신(프로그램)을 프로그래밍한
메타 프로그래밍!
자바스크립트 내장 객체인 Reflect를 통해, 자바스크립트에 이미 정의된 속성을 다룰 수 있게 되었습니다!
그런 반면, 한계점도 있습니다.
Reflect만으로는 특정 어플리케이션에서만 다루는 도메인 데이터를, 프로그램 수준에 저장할 수 없기에
자유로운 메타 프로그래밍을 하기에 어려움이 있습니다.
그리고, 이런 한계를 극복하기 위해 나온 라이브러리 (구현체)가,
NestJS에서도 사용되는 reflect-metadata
입니다!
cf. Metadata Proposal - ECMAScript
사실 선후관계로는, reflect-metadata
라이브러리가 나오기 전,
메타데이터를 저장할 내부 슬롯을 추가하고, 이에 접근하는 Reflect API를 추가하는 제안이 있었다고 합니다.
그러나 제안 상태에 머물러 있고, 이에 위 제안이 수락되기 전에 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에 등록된 메타데이터를 조회
위에 예시로 작성한 함수를 호출한다고 가정하고, 살펴 보려고 합니다! :)
(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
파일을 보면, 함수 안에 metadatRegistry
와 metadataProvider
변수가 존재하고, 같은 계층에 살펴보고자 하는 함수 두 개가 정의되어 있습니다!
그리고 각각의 함수들을 exporter
를 통해, 외부로 노출시키는 구조인 것 같네요..!
metadataRegistry
와 metadataProvider
를 스코프 내의 함수들에서 사용하고, 필요한 함수를 exporter
를 통해 내보내기 위한 방식인 것 같네요..!
한 가지 더, OrdinaryDefineOwnMetadata
함수가 스코프에 따라 2개 존재하는 것도 볼 수 있네요!
필요한 함수들에 대해서는 아래에서 로직을 볼 예정이기에, 여기서는 함수 선언만 떼어서 작성하였지만, 이 함수만은 구분을 위해 우선 구현 부분을 포함해 두었습니다.
우선은 이렇게 간단한 구조만 본 상태로,
metadataRegistry
와 metadataProvider
를 시작으로
defineMetadata
와 getMetadata
의 동작을 알아보겠습니다!
/**
* 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
의 폴리필 정도로 이해하고 넘어가려 합니다!
사실 정확히 이해 못했습니다..ㅠㅠ 다음 기회에 공부해 보겠습니다...
[ cf 2. Symbol.for("@reflect-metadata:registry") ]
reflect-metadata
라이브러리의 소스 코드를 보면, Reflect, ReflectLite, ReflectNoConflict가 구분되어 있는데,
파일마다 다른 Registry를 root.Reflect
에 두기 위한 부분인 것 같습니다..!
하지만 막상 ReflectNoConflict의 의미는 잘 모르겠는...ㅠㅠ
이제 조금 더 자세히 보기로 한, 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)는 언제 사용해야 할까?
위에서 살펴본 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
에 대한 간단한 느낌? 정도는 잡은 것 같습니다!
나머지 동작들은 드디어! 보고 싶었던 함수들을 살펴 보며 알아보겠습니다!!
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
함수로 가 보겠습니다!
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
까지 가 보겠습니다!
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);
코드로 돌아가 보겠습니다! :)
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);
}
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
까지 마저 빠르게 보겠습니다! :)
(이쪽은 좀 더 짧았으면...)
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
와 구조가 거의 같으니, 빠르게 넘어가겠습니다! :)
// 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
까지 이어서 보겠습니다!
// 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));
}
// 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와 많은 부분이 닿아있는 Reflect
와reflect-metadata
라이브러리에 대해 간단히 공부해 보았습니다!
Proxy와 Reflect는 동일한 메서드 구성을 갖고,
Reflect는 리플렉션, 메타 프로그래밍을 위한 자바스크립트의 API로 볼 수 있었습니다.
그리고, Reflect의 한계를 넘기 위해 제안된, reflect-metadata
라이브러리는, WeakMap과 Map을 통해 target에 메타데이터를 저장한다는 점까지 함께 볼 수 있었습니다!
Reflect와 메타데이터가 궁금해서 시작한 글이지만,
정작 WeakMap, 자바스크립트의 중첩 함수 등 이것저것 낯선 것들을 많이 봐서 좋네요 :)
다음 기회에는, 제가 이 호기심을 갖게 해 준 NestJS에서
이 라이브러리를 사용하는 예시를 조금 살펴보며, 메타데이터의 흐름을 조금 더 이해해 보고 싶습니다 :)
길고 어지러운 글, 관심 가져주셔서 감사합니다!