@Module

김동현·2024년 2월 14일
0

Nestjs

목록 보기
6/6
post-thumbnail

Module Decorator

모듈 데코레이터 함수를 분석해보자!

export function Module(metadata: ModuleMetadata): ClassDecorator {
  const propsKeys = Object.keys(metadata); // 1
  validateModuleKeys(propsKeys); // 2

  return (target: Function) => {
    for (const property in metadata) {
      if (metadata.hasOwnProperty(property)) { // 3
        // 1. property -> inport or controllers or providers or exports
        // 2. metadata의 import내의 값들
        // 3. target은 class 자체가 들어간다.
        Reflect.defineMetadata(property, (metadata as any)[property], target); // 3
      }
    }
  };
}
  1. metadata의 프로퍼티를 뽑아 낸다.
  2. metadata의 프로퍼티를 유효성 검사한다.
  3. target의 Metadata를 등록한다.
@Module({
  imports: [LogModule.forRoot(), EnvModule.forRoot()],
  controllers: [AppController],
  providers: [AppService, ...interceptors, ...filters, ...pipes],
})
export class AppModule {}
console.log(Reflect.getMetadataKeys(AppModule));
console.log(Reflect.getMetadata('imports', AppModule));
console.log(Reflect.getMetadata('providers', AppModule));
console.log(Reflect.getMetadata('controllers', AppModule));

등록된 메타에이터를 콘솔 찍어보면 아래와 같이 나온다.

하나씩 뜯어보자

ModuleMetadata

모듈 메타데이터에 대한 인터페이스 정보이다.
imports, controllers, providers, exports에 어떤 타입들이 들어 올 수 있는 지 정의되어 있다!

export interface ModuleMetadata {
  /**
   * Optional list of imported modules that export the providers which are
   * required in this module.
   */
  imports?: Array<
    Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference
  >;
  /**
   * Optional list of controllers defined in this module which have to be
   * instantiated.
   */
  controllers?: Type<any>[];
  /**
   * Optional list of providers that will be instantiated by the Nest injector
   * and that may be shared at least across this module.
   */
  providers?: Provider[];
  /**
   * Optional list of the subset of providers that are provided by this module
   * and should be available in other modules which import this module.
   */
  exports?: Array<
    | DynamicModule
    | Promise<DynamicModule>
    | string
    | symbol
    | Provider
    | ForwardReference
    | Abstract<any>
    | Function
  >;
}

validateModuleKeys

모듈의 메타데이터를 등록하기 전에 MetadataInterface에 정의된 프로퍼티들이 올바르게 들어왔는 지 유효성 검사를 하는 함수이다.

export const MODULE_METADATA = {
  IMPORTS: 'imports',
  PROVIDERS: 'providers',
  CONTROLLERS: 'controllers',
  EXPORTS: 'exports',
};
import { MODULE_METADATA as metadataConstants } from '../constants';

const metadataKeys = [
  metadataConstants.IMPORTS,		// imports
  metadataConstants.EXPORTS,		// exports
  metadataConstants.CONTROLLERS,	// controllers
  metadataConstants.PROVIDERS,		// providers
];

export function validateModuleKeys(keys: string[]) {
  const validateKey = (key: string) => 
  	// 💡 imports, controllers, exports, providers 만 들어올 수 있음
    if (metadataKeys.includes(key)) {
      return;
    }
  	// ⛔️ 다른 프로퍼티가 들어오면 에러를 던진다.
    throw new Error(INVALID_MODULE_CONFIG_MESSAGE`${key}`);
  };
  keys.forEach(validateKey);
}

Reflect.defineMetadata

  return (target: Function) => {
    for (const property in metadata) {
      if (metadata.hasOwnProperty(property)) {
        // 1. property -> inport or controllers or providers or exports
        // 2. metadata의 import내의 값들
        // 3. target은 class 자체가 들어간다.
        Reflect.defineMetadata(property, (metadata as any)[property], target);
      }
    }

🤔 Reflect에 메타데이터를 등록하기 전에 Decorator에 대해 한 번 살펴보자

데코레이터

Decorators
A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.

Decorator Typescript Docs

데코레이터란 클래스, 메서드 접근자, 프로퍼티 또는 파라미터에 추가적인 선언을 할 수 있는 방법이다.
@표현식 형식과 함께 사용하며 데코레이션된 선언은 런타임시에 호출되어 표현식이 평가 된다.

function Decorator(value: string) {
 // 데코레이터 함수
 return function (target: Function) {
   console.log('전달받은 value 값', value);
   console.log('target 정보: ', target);
 };
}

// 데코레이터 팩토리를 사용하면 인자 값을 전달할 수 있다.
@Decorator('value value value')
@Module({
 imports: [LogModule.forRoot(), EnvModule.forRoot()],
 controllers: [AppController],
 providers: [AppService, ...interceptors, ...filters, ...pipes],
})
export class AppModule {}

위 코드는 간략하게 선언한 데코레이터가 있다.
데코레이터의 target 에는 @데코레이션된 클래스가 들어 오게 된다.

💡 즉, 런타임 환경에서 클래스, 메서드 등이 선언될 때 데코레이션 된 표현식이 추가로 작동 되어 보다 더 다양하게 각 요소들을 선언할 수 있는 것이다.

Decorater Factory

If we want to customize how a decorator is applied to a declaration, we can write a decorator factory. A Decorator Factory is simply a function that returns the expression that will be called by the decorator at runtime.

사용자 정의 데코레이터를 만들기 위해서는 타입스크립트에서 약속해 놓은 데코레이터 팩토리 형식으로 구현하면 된다!
데코레이터 팩토리의 형식은 런타임 시 데코레이터 표현식이 호출할 함수를 반환하는 함수를 작성하면 된다.

🤔 데코레이터 표현식이 호출할 함수를 반환하는 함수?
말이 어렵지만, 타입스크립트에서 알려주는 아래의 예시를 보자

We can write a decorator factory in the following fashion:
Decorator Factories Typescript Docs

function color(value: string) {
  // 데코레이터 팩토리 형식으로, 데코레이터 표현식이 사용할 함수를 반환합니다.
  return function (target) {
    // 데코레이터는 이 함수를 활용한다. 'target'과 'value'를 활용하여 해당 블록 내에서 어떠한 일을 할 수 있게 된다.
    
  }; // << 반환되는 이 함수가 데코레이터 표현식이 사용하는 함수이다.
}

@Module

✅ 어떤 모듈 클래스에 @Module을 데코레이션 하게 되면 해당 클래스에 imports, controllers, exports, providers의 메타데이터가 생성되는 것이다!

🤔 이를 통해, 우리가 Service에 생성자 주입을 하거나, 모듈을 다른 모듈에 imports 해주는 등의 행위를 할 때, Nestjs 가 해당 모듈에 추가되어 있는 Metadata를 찾아가 적절한 프레임워크로서 동작해 주는 것 아닐까?

dist 내에 있는 __decorator를 먼저 분석해보고 NestFactory를 차근차근 뜯어보는 시간을 가져야겠다 😤

참고 자료

profile
달려보자

0개의 댓글