[NestJS 공식문서 정독하기] Fundamentals - Dynamic modules
2022. 8. 9. 05:12ㆍWeb/NestJS
반응형
🔗 https://docs.nestjs.com/fundamentals/dynamic-modules
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
속성을 추가로 포함하는데 해당 모듈의 이름을 나타내며 모듈의 클래스명과 동일해야 함)DynmaicModule
의module
속성은 필수이며 나머지 속성들은 모두 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
는 기본적으로register
와registerAsync
메소드를 제공함- 다른 명칭을 사용하기 위해서는
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 될 수 있음
반응형