Dynamic modules

Dynamic modules

모듈 챕터는 Nest 모듈의 기본 사항을 다루고 동적 모듈에 대한 간략한 소개를 포함합니다. 이 장은 다이나믹 모듈의 주제를 확장합니다. 완료되면 자신이 무엇인지, 언제 어떻게 사용하는지 잘 파악해야합니다.

Introduction

설명서의 개요 섹션에있는 대부분의 응용 프로그램 코드 예제는 일반 또는 static 모듈을 사용합니다. 모듈은 프로 바이더컨트롤러와 같이 전체 응용 프로그램의 모듈 식 부분으로 구성되는 구성 요소 그룹을 정의합니다. 이러한 구성 요소에 대한 실행 컨텍스트 또는 범위를 제공합니다. 예를 들어, 모듈에 정의 된 공급자는 내보낼 필요없이 모듈의 다른 구성원에게 표시됩니다. 공급자가 모듈 외부에서 볼 수 있어야하는 경우, 먼저 호스트 모듈에서 _exported_를 사용한 다음 소비 모듈로 _imported_합니다.

익숙한 예를 살펴 보겠습니다.

먼저UsersService를 제공하고 내보내는 UsersModule을 정의합니다. UsersModuleUsersServicehost 모듈입니다.

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

다음으로 UserModule을 가져 와서 UserModule의 내 보낸 공급자를 AuthModule내에서 사용할 수 있도록하는 AuthModule을 정의합니다.

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

이러한 구문을 통해 예를 들어 AuthModule에서 호스팅되는 AuthServiceUserService를 삽입 할 수 있습니다.

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}
  /*
    Implementation that makes use of this.usersService
  */
}

이것을 정적(static) 모듈 바인딩이라고합니다. Nest가 모듈을 서로 연결하는 데 필요한 모든 정보는 이미 호스트 및 소비 모듈에서 선언되었습니다. 이 과정에서 일어나는 일을 풀어보겠습니다. Nest는AuthModule 내에서UsersService를 다음과 같이 사용 가능하게합니다.

  1. UsersModule자체가 소비하는 다른 모듈을 전 이적으로 가져오고 종속성을 전 이적으로 해결하는 것을 포함하여 UsersModule인스턴스화 (Custom provider 참조)

  2. AuthModule을 인스턴스화하고 UsersModule의 내 보낸 공급자를 AuthModule의 구성 요소에서 사용할 수 있도록합니다 ( AuthModule에서 선언 된 것처럼).

  3. AuthServiceUsersService의 인스턴스를 주입합니다.

Dynamic module use case

정적 모듈 바인딩을 사용하면 소비 모듈이 호스트 모듈의 공급자가 구성되는 방식에 영향을 줄 수있는 기회가 없습니다. 이것이 왜 중요한가? 다른 사용 사례에서 다르게 작동해야하는 범용 모듈이있는 경우를 고려하십시오. 이는 많은 시스템에서 "플러그인"개념과 유사하며 일반 기능은 소비자가 사용하기 전에 일부 구성이 필요합니다.

Nest의 좋은 예는 구성 모듈입니다. 많은 응용 프로그램은 구성 모듈을 사용하여 구성 세부 정보를 외부화하는 것이 유용하다는 것을 알고 있습니다. 이를 통해 개발자를위한 개발 데이터베이스, 준비 / 테스트 환경을위한 준비 데이터베이스 등 다양한 배포 환경에서 응용 프로그램 설정을 쉽게 변경할 수 있습니다. 구성 매개 변수 관리를 구성 모듈, 응용 프로그램 소스 코드에 위임함으로써 구성 매개 변수와 독립적으로 유지됩니다.

문제는 구성 모듈 자체가 일반 ( "플러그인"과 유사하기 때문에) 소비 모듈에 따라 사용자 정의해야한다는 것입니다. 여기에서 dynamic modules이 시작됩니다. 동적 모듈 기능을 사용하여 소비 모듈이 API를 사용하여 구성 모듈을 가져올 때 사용자 정의하는 방법을 제어 할 수 있도록 구성 모듈을 동적으로 만들 수 있습니다.

다시 말해, 동적 모듈은 한 모듈을 다른 모듈로 가져오고, 지금까지 본 정적 바인딩을 사용하는 것과 반대로 모듈을 가져올 때 해당 모듈의 속성과 동작을 사용자 정의하기위한 API를 제공합니다.

Config module example

이 섹션에서는 configuration chapter의 예제 코드 기본 버전을 사용합니다. 이 장의 끝에서 완성 된 버전은 예제로 사용할 수 있습니다.

우리의 요구 사항은 ConfigModuleoptions 객체를 받아 들여 커스터마이즈 하도록 하는 것입니다. 지원하려는 기능은 다음과 같습니다. 기본 샘플은 .env 파일의 위치를 프로젝트 루트 폴더에 하드 코딩합니다. 선택한 폴더에서 .env 파일을 관리 할 수 있도록 구성 가능하게 만들고 싶다고 가정 해 봅시다. 예를 들어, 다양한 .env 파일을 config라는 프로젝트 루트 아래의 폴더 (예: src의 형제 폴더)에 저장한다고 가정 해보십시오. 다른 프로젝트에서 ConfigModule을 사용할 때 다른 폴더를 선택할 수 있습니다.

동적 모듈은 가져 오는 모듈에 매개 변수를 전달하여 해당 동작을 변경할 수있는 기능을 제공합니다. 이것이 어떻게 작동하는지 봅시다. 소비 모듈의 관점에서 이것이 어떻게 보일지에 대한 최종 목표에서 시작하여 거꾸로 작업하면 도움이됩니다. 먼저, ConfigModule을 가져 오는 _statically_ 가져 오기 (즉, 가져온 모듈의 동작에 영향을 줄 수있는 접근 방법)를 빠르게 검토해 보겠습니다. @Module()데코레이터의 imports 배열에주의를 기울이십시오 :

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 {}

구성 객체를 전달하는 _dynamic module_ import가 어떻게 보일지 생각해 봅시다. 이 두 예제의 imports 배열의 차이점을 비교하십시오.

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 {}

위의 동적 예에서 무슨 일이 일어나는지 봅시다. 움직이는 부분은 무엇입니까?

  1. ConfigModule은 일반 클래스이므로 register()라는 정적 메소드가 있어야 함을 유추할 수 있습니다. 우리는 클래스의 인스턴스가 아니라 ConfigModule 클래스에서 호출하기 때문에 정적임을 알고 있습니다. 참고: 곧 만들 이 메소드는 임의의 이름을 가질 수 있지만, 관례 적으로 forRoot()또는 register()라고 부릅니다.

  2. register() 메소드는 우리에 의해 정의되므로 원하는 입력 인수를 받아 들일 수 있습니다. 이 경우 적절한 속성을 가진 간단한 options 객체를 사용합니다. 이것이 일반적인 경우입니다.

  3. register() 메소드는 리턴 값이 익숙한 imports리스트에 나타나기 때문에 module과 같은 것을 반환해야한다고 유추 할 수 있습니다. 여기에는 모듈리스트가 포함되어 있습니다.

사실, register() 메소드가 리턴 할 것은 DynamicModule입니다. 동적 모듈은 정적 모듈과 동일한 정확한 속성과 module이라는 추가 속성을 가진 런타임에 생성된 모듈에 지나지 않습니다. 데코레이터에 전달된 모듈 옵션에주의를 기울이면서 샘플 정적 모듈 선언을 빠르게 검토해 보겠습니다.

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

다이나믹 모듈은 정확히 같은 인터페이스를 가진 객체와 module이라는 추가 속성을 반환해야합니다. module 속성은 모듈 이름으로 사용되며 아래 예제와 같이 모듈의 클래스 이름과 동일해야합니다.

info 힌트 동적 모듈의 경우 모듈 옵션 객체의 모든 속성은 선택 사항입니다 exceptmodule.

정적 register()메소드는 어떻습니까? 이제 그 역할은 DynamicModule 인터페이스를 가진 객체를 반환하는 것임을 알 수 있습니다. 우리가 그것을 호출 할 때, 우리는 정적 클래스에서 모듈 클래스 이름을 나열함으로써 그렇게하는 것과 유사한 방식으로 모듈을 imports 목록에 효과적으로 제공합니다. 다시 말해, 동적 모듈 API는 단순히 모듈을 반환하지만 @Modules 데코레이터의 속성을 수정하는 대신 프로그래밍 방식으로 지정합니다.

사진을 완성하는 데 도움이되는 몇 가지 세부 정보가 여전히 있습니다.

  1. 이제 @Module()의 데코레이터의 imports 속성은 모듈 클래스 이름 (예: imports: [UsersModule])뿐만 아니라 동적 함수 returning 함수를 사용할 수 있음을 알 수 있습니다 모듈 (예: imports: [ConfigModule.register(...)]).

  2. 동적 모듈 자체는 다른 모듈을 가져올 수 있습니다. 이 예제에서는 그렇게하지 않지만 동적 모듈이 다른 모듈의 공급자에 의존하는 경우 선택적 imports속성을 사용하여 가져옵니다. 다시, 이것은 @Module()데코레이터를 사용하여 정적 모듈에 대한 메타 데이터를 선언하는 방식과 정확히 유사합니다.

이러한 이해를 바탕으로 이제 동적 ConfigModule선언의 모습을 볼 수 있습니다. 그것에 균열을 보자.

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

이제 조각들이 어떻게 묶여 있는지 분명해야합니다. ConfigModule.register(...)를 호출하면 지금까지 @Module() 데코레이터를 통해 메타 데이터로 제공 한 것과 본질적으로 동일한 속성을 가진 DynamicModule 객체를 반환합니다.

info 힌트 @nestjs/common에서 DynamicModule을 가져옵니다.

그러나 동적 모듈은 그다지 흥미롭지 않지만, 우리가 원하는대로 구성하는 기능을 도입하지 않았습니다. 다음에 그 내용을 다루겠습니다.

Module configuration

ConfigModule의 동작을 커스터마이즈하기위한 확실한 해결책은 위에서 생각한 것처럼 정적 register()메소드에서 options 객체를 전달하는 것입니다. 소비 모듈의 imports 속성을 다시 한번 살펴 보자.

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 {}

그것은 '옵션'객체를 동적 모듈에 전달하는 것을 잘 처리합니다. 그러면ConfigModule에서options 객체를 어떻게 사용합니까? 잠시 생각해 봅시다. 우리는ConfigModule이 기본적으로 다른 제공자가 사용하기 위해 주사 가능한 서비스 인ConfigService를 제공하고 내보내는 호스트라는 것을 알고 있습니다. 실제로 동작을 사용자 정의하기 위해options 객체를 읽어야하는 것은 'ConfigService'입니다. register ()메소드에서ConfigServiceoptions '를 얻는 방법을 알고 있다고 가정하자. 이 가정을 통해 서비스의 일부를 변경하여options` 객체의 속성에 따라 동작을 사용자 지정할 수 있습니다. ( 참고 : 당분간은 실제로 전달하는 방법을 결정 했으므로 '옵션'을 하드 코딩 만하면됩니다. 잠시 후에 수정하겠습니다.)

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

이제 ConfigServiceoptions에서 지정한 폴더에서 .env 파일을 찾는 방법을 알고 있습니다.

우리의 나머지 작업은 register()단계의 options 객체를 ConfigService에 삽입하는 것입니다. 물론 _dependency injection_을 사용하여 수행합니다. 이것이 핵심 사항이므로 이해해야합니다. 우리의 ConfigModuleConfigService를 제공하고 있습니다. ConfigService는 런타임에만 제공되는 options 객체에 의존합니다. 따라서 런타임에 먼저 options 객체를 Nest IoC 컨테이너에 바인딩 한 다음 Nest가 ConfigService에 삽입해야합니다. 맞춤 제공자 장에서 제공자는 서비스뿐만 아니라 모든 가치를 포함 할 수 있다는 점을 기억하십시오. 의존성 주입을 사용하여 간단한 options 객체를 처리하는 것이 좋습니다.

옵션 객체를 IoC 컨테이너에 바인딩하는 방법을 먼저 살펴 보겠습니다. 정적 register()메소드에서 이를 수행합니다. 우리는 동적으로 모듈을 구성하고 있으며 모듈의 속성 중 하나는 공급자 목록입니다. 따라서 우리가해야 할 일은 옵션 객체를 공급자로 정의하는 것입니다. 이를 통해 ConfigService에 주입 할 수있게 되며 다음 단계에서 활용할 것입니다. 아래 코드에서providers 배열에주의하십시오 :

import { DynamicModule, Module } from '@nestjs/common';

import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(options): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

이제 CONFIG_OPTIONS제공자를 ConfigService에 삽입하여 프로세스를 완료 할 수 있습니다. 비 클래스 토큰을 사용하여 공급자를 정의 할 때는 여기에 설명된대로 @Inject() 데코레이터를 사용해야합니다.

import { Injectable, Inject } 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(@Inject('CONFIG_OPTIONS') private options) {
    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];
  }
}

마지막 참고 사항: 단순화를 위해 위의 문자열 기반 주입 토큰 (CONFIG_OPTIONS)을 사용했지만 모범 사례는이를 별도의 파일에서 상수 (또는 심볼)로 정의하고 해당 파일을 가져 오는 것입니다. 예를 들면 다음과 같습니다.

export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';

Example

이 장의 코드에 대한 전체 예는 여기에서 확인할 수 있습니다.

Last updated