Published on

nestjs dynamic module 에 대해서

Authors
  • avatar
    Name
    길재훈
    Twitter

Dynamic Module 에 대해서 알아보자

Module 은 말그대로 그룹을 형성해주는 역할을 한다 내부에는 ControllerProviders 를 받아서 자신만의 실행 컨텍스트를 만든다고 보면 된다

만약 Module 내부에서 Service logicexports 하지 않는다면 encapsulate 되기에, 해당 Module 범위에서만 사용되도록 한정시킨다.

이러한 방식을 nestjs 에서는 static module binding 이라고 한다 만약 exportsService 가 포함되어 있으며, Moduleimports 한다면 다음과 같이 작동한다.

가져오는 Module을 사용하는 Module 에서 가져와 모든 종속성을 해결하고 ModuleInstace 화 시킨다

그리고 사용하는 Module 에서 가져온 Module 이 내보낸 Providers 를 구성요소에서 사용할 수 있도록 하며, 사용하는 ModuleInstance 화 한다.

그런다음, 사용해야할 Service 를 주입시키는 방식으로 이루어진다

Docs 에서는 사용하는 ModuleHost Module 이라 칭하고, 사용되는 모듈을 Consuming Module 이라 명한다

Dynamic Module 을 사용하는 이유

Module 을 사용자 입맛대로 동적으로 생성이 가능하다

이전의 Module exportimport 방식은 consuming module 에 의해 항상 정의되고 만들어져야 한다는 점이다

이렇게 정적으로 제공되어지기 보다는, 사용자가 해당 모듈을 가져올때 원하는 방식의 API 를 만들어, Module 로 받아올 수 있다면 정말 편할것이다.

Dynamic Moduleconsuming module 에서 API 를 구성하여 Host Module 로 가져올때, 사용자가 정의한 방법에 따라 동적으로 모듈을 구성하는 방법이다

Docs 에서는 ConfigModule 을 사용할때, 사용자가 정의한 옵션에 맞추어 Module 을 가져오는것을 보여준다

// 정적으로 module 을 가져오는 로직

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 {}
// 동적으로 module 을 가져오는 로직

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 moduleregister static method 를 사용하여 객체로 인자를 전달하고 있다

보통 이렇게 Dynamic module 로 만들어지면, static method 를 통해 forRoot 혹은 register 를 만들어 사용하는것이 Convention 이라고 한다

위의 로직은 흥미롭게도, Modulestatic method 에 인자로 객체를 보낸다. 그러면 이 Module 는 제공하는 Service 에 해당 인자의 객체를 제공하여, 설정한 객체에 맞는 Service 를 주입해준다

일단 register() 가 반환하는 DynamicModuleruntime 에 생성된 모듈에 불과하다.

모듈을 간단하게 아무렇게나 빠르게 선언하면 다음과 같이 선언도 가능하다


@Module({
  imports: [DogsModule],
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})

실제로, 우리가 여태껏 작성한 Module 역시 동일하게 작성해서 처리한다 그냥 객체에 Module 에 사용할 API 에 맞춰서 값을 넣은것 뿐이다.

Dynamic Module 역시 동일한 인터페이스 와 함께 module 이라고 불리는 property 를 추가한 형태를 가진다

module propertyDynamic module 의 이름을 정의해주는 property 로 생각하면 된다

이제, register 가 어떻게 작동하는지 확인하는것이 좋을 듯 하다

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],
    };
  }

위는 Docs 에서 제시해주는 예시이다. 이 예를 보면 ConfigModuleregister 가 어떻게 생겼는지 볼 수 있다 정말 간단하게도, 일반 모듈과 같지만, module 프로퍼티가 존재하는것이 보인다.

이제 이 register 에 인자를 넣으면 다음처럼 만들 수 있다

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],
    }
  }
}

굉장히 흥미로운 상황이다. 여기서 인자값으로 받은 값을 받는데, 위의 Record Utility 는 다음의 type 으로 만든다.


  static register(options: { [key: string]: any }): DynamicModule {

즉, 어떠한 값이라도 받을수 있는 options 객체를 뜻하지만, 실제로 Interface 로 만들어 options 를 지정하는것이 더 현명한 방법이다.

위는 Docs 에서 보여주기식으로 만든 코드라는점 알아두자 즉, 어떠한 값이라도 받을수 있는 options 객체를 뜻하지만, 실제로 Interface 로 만들어 options 를 지정하는것이 더 현명한 방법이다.

위는 Docs 에서 보여주기식으로 만든 코드라는점 알아두자

일단, 위의 로직은 custom Provider 를 사용하여 새로운 Servide Logic 을 만든다.


providers: [
  {
    provide: 'CONFIG_OPTIONS',
    useValue: options,
  },
  ConfigService,
],

위 로직은 간단하게, value 값이 options 이며, Provider tokenCONFIG_OPTIONS 라는것을 뜻한다

이렇게, 주입된 Provider 를 어떻게 커스텀하게 사용할까? 만약 DI 에 대한 이해가 있다면 쉽게 유추가능하다.

바로, Service 로직인 configService 에서 주입받아서 처리하면, 동적으로 설정에 따라 Logic 변경이 가능하다.

ConfigService 를 보도록 하자

import * as dotenv from 'dotenv'
import * as fs from 'fs'
import * as path from 'path'
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]
  }
}

위 로직을 보면, Custom Provider 를 제공하기 위해, @inject 를 사용하여 Constructor 에서 주입받는것을 볼 수 있다.

이제 이 Service 로직은 Host Module 에서 Dynamic module 에 넣은 options 주입받아서 동적으로 처리된다.

굉장히 멋진 방법으로 Module 을 조작한다. Dynamic Module 에 대해서 알아보았다.

Docs 에는 Dynamic Modulebuild 할수 있는 새로운 API 를 제시하지만 이부분은 조금씩 차근차근 공부해 나가볼 예정이다.