모듈은 어플리케이션의 모듈식 부분으로 연관된 프로바이더 및 컨트롤러와 같은 컴포넌트를 그룹화고, 이러한 구성 요소에 대한 실행 컨텍스트 또는 범위를 제공한다.
예를 들어, 모듈에 정의된 프로바이더는 export할 필요없이 같은 모듈의 다른 멤버에서 사용할 수 있다.
프로바이더가 모듈 외부에서 사용되어야 하는 경우 먼저 호스트 모듈에서 export하고 사용하려는 모듈에서 import하면 된다.
다음은 UsersService
를 포함하는 UsersModule
을 정의하는 것이다.
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
이 모듈을 import하는 AuthModule
을 정의하여 UsersModule
에서 export한 프로바이더를 사용할 수 있게 한다.
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
에서 호스팅되는 AuthService
에 UsersService
를 삽입할 수 있다.
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
/*
Implementation that makes use of this.usersService
*/
}
위와 같은 바인딩 과정을 정적 모듈 바인딩이라 한다.
Nest가 모듈을 함께 연결하는 데 필요한 모든 정보는 호스트 및 이를 사용하려는 모듈에서 선언이 된다.
Nest는 다음과 같은 과정을 통해 AuthMdule
내에서 UsersService
를 사용할 수 있다.
UsersModule
과 UsersModule
이 import한 다른 모듈들을 인스턴스화 한다.AuthModule
을 인스턴스화하고, UsersModule
에서 export한 프로바이더를 AuthModule
에서 사용할 수 있게 한다.AuthService
에 UsersService
의 인스턴스를 삽입한다.정적 모듈 바인딩을 사용하면, 호스트 모듈의 프로바이더가 구성되는 방식에 이를 사용하는 모듈(이하 소비 모듈)이 관여할 수 없다.
만약, 상황마다 다르게 동작해야 하는 범용 모듈의 경우, 사용전에 조작이 필요한 경우가 있을 것이다.
Nest에서 이를 위해 Configuration Module(이하 구성 모듈)을 제공한다. 많은 어플리케이션은 구성 모듈을 사용해 구성 세부 정보를 구체화할 수 있다.
이를 통해 개발자를 위한 개발 데이터베이스, staging/test 환경을 위한 staging 데이터베이스 등 다양한 배포에서 어플리케이션 설정을 동적으로 쉽게 변경할 수 있다.
문제는 구성 모듈을 소비 모듈에서 Custom하게 정의해야한다는 것인데, 동적 모듈이 이를 해결해 준다.
동적 모듈 기능을 사용해 구성 모듈을 동적으로 만들 수 있으므로 소비 모듈이 API를 사용해 가져올 때 구성 모듈을 Custom하게 정의하는 방법을 제어할 수 있다.
즉, 동적 모듈은 정적 바인딩을 사용하는 것과 반대로 한 모듈을 다른 모듈로 가져오고 해당 모듈을 가져올 때 해당 모듈의 속성과 동작을 Custom하게 정의하는 API를 제공한다.
ConfigModule
이 options
객체를 받아 사용자 정의하도록 한다.
동적 모듈을 이용해 매개변수로 넘어오는 options
으로 넘어온 경로에서 .env
파일을 찾도록 한다.
다음은 구성 객체를 전달하는 동적 모듈을 import하는 예이다. 아래 예는 config
디렉터리 아래 .env
파일 생성해 config
파일에서 이를 찾아올 수 있게 한다.
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 {}
이때, register()
메서드는 DynamicModule
이다. 동적 모듈은 런타임에 생성되며, 정적 모듈과 동일한 속성과 module
(동적 모듈의 이름을 정의하는 필드)이라는 하나의 추가 속성을 포함한다.
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(...)
을 호출하면 DynamicModule
객체가 반환된다.
위 예에서 ConfigModule
은 ConfigService
를 export하는 호스트 모듈이다.
동작을 사용자 정의하기 위해 options
객체를 읽어내는 것은 실제로 ConfigService
이다.
register()
메서드에서 ConfigService
로 options
를 가져오는 방법을 알고 있다고 가정해보자
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];
}
}
이제 ConfigService
는 options
에서 지정한 폴더해서 .env
파일을 찾을 수 있다.
남은 작업은 register()
메서드에서 options
객체를 ConfigService
에 주입하는 것이다.
ConfigModule
은 ConfigService
를 제공한다. ConfigService
는 런타임에만 제공되는 options
객체에 의존한다.
따라서 런타임에 먼저 options
객체를 Nest IoC 컨테이너에 바인딩한 다음 Nest가 이를 ConfigService
에 주입하도록 해야한다.
options
객체를 IoC 컨테이너에 바인딩 하는 작업은 register()
메서드에서 수행한다.
다음과 같이 options
객체를 프로바이더로 정의하면 된다.
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
에 주입하면 된다.
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) {
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' 객체를 문자열 그대로 사용했지만, 이 부분은 지양한다.
이렇게 하는 것이 아닌, 아래와 같이 이를 별도에 파일에 상수 or symbol
로 정의하고 해당 파일을 가져오는 방식을 사용하는 것이 좋다.
export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';