[NestJS 공식문서 정독하기] Fundamentals - Dynamic modules

2022. 8. 9. 05:12Web/NestJS

반응형
🔗  https://docs.nestjs.com/fundamentals/dynamic-modules
 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

Introduction

  • 기존 예제들에서 사용하던 모듈은 regular(static) 모듈이라 함
  • provider와 controller 등의 컴포넌트들의 excecution 또는 scope를 제공함
  • static module binding은 호스트와 모듈들에 연결에 필요한 모든 정보가 이미 존재하는 방식임

Dynamic module use case

  • static module binding 방식은 사용하는 모듈에서 호스트 모듈의 설정에 영향을 끼칠 수가 없음
  • 범용 모듈을 만들어 여러가지 케이스에서 각각에 맞게 다르게 동작하는 모듈을 만들 때 해당 기능이 필요함
  • 예를 들면, NestJS의 configuration module은 애플리케이션 설정을 배포 환경에 따라 동적으로 변경하기 용이하도록 dynamic module 기능을 활용함
  • dynamic module은 모듈을 import하면서 해당 모듈의 속성과 동작을 사용자가 지정할 수 있는 API를 제공함

Config module example

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • static module import의 예시
  • import한 모듈의 동작에 영향을 줄 수 있는 기능이 없음
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule.register({ folder: './config' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • dynamic module import의 예시
  • 모듈에 설정 객체를 전달함
  • ConfigModule 클래스에는 register 이라는 static 메소드가 존재함 (이 메소드는 임의의 명칭을 가질 수 있지만 컨벤션으로 forRoot() 또는 register() 로 짓는게 좋음)
  • register() 메소드는 직접 정의하는 메소드이므로 파라미터로 아무 값이나 받을 수 있으며, 해당 메소드는 DynamicModule 을 반환함
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(): DynamicModule {
    return {
      module: ConfigModule,
      providers: [ConfigService],
      exports: [ConfigService],
    };
  }
}
  • DynamicModule 은 런타임에 생성되는 static module과 동일한 속성들을 갖는 모듈임 (단, module 속성을 추가로 포함하는데 해당 모듈의 이름을 나타내며 모듈의 클래스명과 동일해야 함)
  • DynmaicModulemodule 속성은 필수이며 나머지 속성들은 모두 optional함
  • @Module() decorator의 imports는 모듈 클래스명 뿐만 아니라 dynamic module을 반환하는 함수도 전달받을 수 있음
  • dynamic module도 다른 모듈의 provider를 의존해야 하면 다른 모듈을 import할 수 있음

Module configuration

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule.register({ folder: './config' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor() {
    const options = { folder: './config' };

    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}
  • ConfigModule 에 전달한 option 객체는 사실 ConfigService가 알아야 하는 정보임
  • 따라서 특정 방법으로 ConfigService에 해당 객체가 전달되었다는 가정하에 구현함 (우선 하드코딩)
  • 이제 register() 메소드의 options 객체를 ConfigService 로 전달하는 방법을 알아내면 됨
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(options: Record<string, any>): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor(@Inject('CONFIG_OPTIONS') private options: Record<string, any>) {
    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}
  • options 객체를 provider로 만들어서 DI (Custom providers 챕터의 내용처럼 service가 아닌 어떤 값이던지 provider로 처리할 수 있음)
  • CONFIG_OPTIONS 토큰으로 ConfigService 에 주입할 수 있음 (커스텀 토큰은 별도의 파일에 따로 정리해서 관리하는 것을 권장)

Community guidelines

  • dynamic module을 생성하는 메소드 명칭에 따른 동작 방식 차이에 관한 컨벤션
    • register : import하는 모듈에서만 사용할 용도로 특정 설정을 적용하는 경우 (ex. HttpModule.register({ baseUrl: 'someUrl' }))
    • forRoot : 특정 설정을 모듈에 적용하고 해당 모듈을 여러 곳에서 재사용하는 경우 (ex. TypeOrmModule.forRoot())
    • forFeature : forRoot 에서 구성한 모듈의 설정을 사용하되 import하는 곳에 특화된 세부 설정을 추가로 해야하는 경우
  • 위의 모든 종류는 보통 async 버전도 존재함 (같은 맥락이지만 NestJS의 DI를 사용하기 위한 메소드)

Configurable module builder

export interface ConfigModuleOptions {
  folder: string;
}
import { ConfigModuleOptions } from './interfaces/config-module-options.interface';

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<ConfigModuleOptions>().build();
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigurableModuleClass } from './config.module-definition';

@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {}
@Module({
  imports: [
    ConfigModule.register({ folder: './config' }),
    // or alternatively:
    // ConfigModule.registerAsync({
    //   useFactory: () => {
    //     return {
    //       folder: './config',
    //     }
    //   },
    //   inject: [...any extra dependencies...]
    // }),
  ],
})
export class AppModule {}
@Injectable()
export class ConfigService {
  constructor(@Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions) { ... }
}
  • ConfigurableModuleBuilder 를 활용하면 async 메소드를 포함한 모듈 생성을 쉽게 할 수 있음

Custom method key

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<ConfigModuleOptions>().setClassMethodName('forRoot').build();
@Module({
  imports: [
    ConfigModule.forRoot({ folder: './config' }), // <-- note the use of "forRoot" instead of "register"
    // or alternatively:
    // ConfigModule.forRootAsync({
    //   useFactory: () => {
    //     return {
    //       folder: './config',
    //     }
    //   },
    //   inject: [...any extra dependencies...]
    // }),
  ],
})
export class AppModule {}
  • ConfigurableModuleClass 는 기본적으로 registerregisterAsync 메소드를 제공함
  • 다른 명칭을 사용하기 위해서는 setClassMethodName 메소드를 사용해야 함

Custom options factory class

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<ConfigModuleOptions>().setFactoryMethodName('createConfigOptions').build();
@Module({
  imports: [
    ConfigModule.registerAsync({
      useClass: ConfigModuleOptionsFactory, // <-- this class must provide the "createConfigOptions" method
    }),
  ],
})
export class AppModule {}
  • useClass 에서 사용되는 factory class는 기본적으로 모듈 configuration 객체를 반환하는 create 메소드를 갖고 있어야 함
  • 위에서는 setFactoryMethodName 메소드로 명칭을 createConfigOptions 로 변경함

Extra options

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<ConfigModuleOptions>()
    .setExtras(
      {
        isGlobal: true,
      },
      (definition, extras) => ({
        ...definition,
        global: extras.isGlobal,
      }),
    )
    .build();
@Module({
  imports: [
    ConfigModule.register({
      isGlobal: true,
      folder: './config',
    }),
  ],
})
export class AppModule {}
@Injectable()
export class ConfigService {
  constructor(
    @Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions,
  ) {
    // "options" object will not have the "isGlobal" property
    // ...
  }
}
  • extra option은 모듈이 어떻게 동작해야하는지 정하는데 필요한 정보이면서 MODULE_OPTIONS_TOKEN provider에는 포함되지 않아야 하는 옵션임 (ex. global 여부)
  • setExtras 메소드는 첫번째 인자로 기본값을, 두번째 인자로 수정된 모듈을 반환하는 함수를 받음

Extending auto-generated methods

export const {
  ConfigurableModuleClass,
  MODULE_OPTIONS_TOKEN,
  OPTIONS_TYPE,
  ASYNC_OPTIONS_TYPE,
} = new ConfigurableModuleBuilder<ConfigModuleOptions>().build();
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import {
  ConfigurableModuleClass,
  ASYNC_OPTIONS_TYPE,
  OPTIONS_TYPE,
} from './config.module-definition';

@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {
  static register(options: typeof OPTIONS_TYPE): DynamicModule {
    return {
      // your custom logic here
      ...super.register(options),
    };
  }

  static registerAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
    return {
      // your custom logic here
      ...super.registerAsync(options),
    };
  }
}
  • 빌더로 자동 생성된 메소드들은 extend 될 수 있음
반응형