모듈은 여러 기능들이 합쳐서 하나의 작업이 되는 단위를 뜻한다.
NestJS에서는 하나의 루트모듈이 무조건 존재하게되고 이것은 우리가 새로운 Nest 프로젝트를 만들때 자동으로 생성되는 app.modeul.ts
이 그것이다. 이렇게 모듈을 나누는 이유는 책임을 나누고 응집도를 높이기 위함이다.
모듈은 @Module()
데코레이터를 사용한다. 인자로는 ModuleMetadata
를 받는데 인터페이스는 다음과 같다.
export declare function Module(metadata: ModuleMetadata): ClassDecorator;
export interface ModuleMetadata {
imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
controllers?: Type<any>[];
providers?: Provider[];
exports?: Array<DynamicModule | Promise<DynamicModule> | string | symbol | Provider | ForwardReference | Abstract<any> | Function>;
}
import
: 이 모듈에서 사용하고싶은 다른 모듈을 가져오기 위해 사용한다.controller
, provider
: 모듈 전반에서 컨트롤러와 프로바이더를 사용할 수 있도록 Nest가 객체를 생성하고 주입할 수 있도록 해준다.export
: 이 모듈에서 제공하는 컴포넌트를 다른 모듈에서 import 해서 사용하고자 한다면 export 해야 한다. export로 선언했다는 뜻은 어디에서나 가져다 쓸 수 있으므로 public 인터페이스 또는 API로 간주된다.진짜 별거없고 import한 모듈을 export로 다시 내보내는 작업이다.
@Module({
providers: [CommonService],
exports: [CommonService],
})
export class CommonModule { }
@Injectable()
export class CommonService {
hello(): string {
return 'Hello from CommonService';
}
}
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule { }
@Module({
imports: [CoreModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
@Controller()
export class AppController {
constructor(private readonly commonService: CommonService) { }
@Get('/common-hello')
getCommonHello(): string {
return this.commonService.hello();
}
}
Nest는 모듈 범위내에서 프로바이더를 캡슐화한다. 따라서 어떤 모듈에 있는 프로바이더를 사용하려면 모듈을 먼저 가져와야 한다. 하지만 헬퍼와 같은 공통 기능이나 DB 연결과 같은 전역적으로 쓸 수 있어야 하는 프로바이더가 필요한 경우가 있다. 이런 프로바이더를 모아 전역 모듈로 제공하면 좋다.
@Global() //
@Module({
providers: [CommonService],
exports: [CommonService],
})
export class CommonModule { }
@Global() 데코레이터만 선언하면 전역적인 모듈이 된다.
모듈은 응집도를 높이기 위함이라 했는데 모든 것을 전역으로 만들면 응집도가 떨어지게 된다.
때문에 꼭 필요한 것만 선언해주자.
호스트에 따라 환경은 다르기 마련이며, 모듈 또한 호스트 환경에 따라 동적으로 구성이 가능하다.
NestJS가 내부적으로 제공해주는 @nestjs/config
패키지를 활용해서 동적 모듈구성을 해보자.
$ yarn add @nestjs/config
이 패키지에선 ConfigModule
이름을 가진 모듈이 이미 존재하기 때문에 imports를 통해 가져오면된다.
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports:[ConfigModule.forRoot()],
})
export class AppModule {}
정적으로 모듈을 가져올 때와는 달리 .forRoot()를 호출하는 걸 볼 수 있는데, forRoot 메소드는 동적모듈을 리턴하는 정적 메서드이다.
동적 모듈을 작성할대 forRoot, register의 이름을 붙이는게 관례이다.
- 비동기 경우 forRootAsync, registerAsync 이름을 붙인다.
이제 루트 디렉토리에 .env파일을 만들어 등록해보겠다.
.env파일은 환경변수를 정의하는 파일로 민감한 정보를 다수 포함하기 때문에
.gitignore
에 등록하는 걸 추천한다. ( 예제에는 그냥 올라갈것이다. )
DATABASE_HOST=local
DATABASE_HOST=somecompany.dbdomain.com
후 서버를 실행할때 환경변수를 지정해주자
"start": "NODE_ENV=dev nest start",
"start:dev": "NODE_ENV=dev nest start --watch",
"start:prod": "NODE_ENV=prod node dist/main",
물론 실행 할 때마다
$ NODE_ENV=dev nest start --watch
명령어를 입력해도 되나
너무 길어서 이렇게 매크로등록을 해둔것이다.
이제 환경변수에따라 읽어오는 .env파일을 동적으로 구성해보자.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports:[ConfigModule.forRoot({
envFilePath:(process.env.NODE_ENV === 'prod') ? '.prod.env' : '.dev.env'
})],
controllers:[AppController],
providers:[AppService, ConfigService],
exports:[]
})
export class AppModule {}
ConfigService
.env파일의 내용에 접근 할 수 있는 메소드를 제공하는 프로바이더이다.
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Controller()
export class AppController {
constructor(private readonly configService: ConfigService) {
}
@Get()
getHello(): string {
return this.configService.get('DATABASE_HOST');
}
}
이제 환경을 달리하여 서버를 실행시킨 후 결과를 확인해보자.
지금까지 있는 모듈을 동적으로 사용했는데 이번엔 직접 커스텀모듈을 동적으로 할당해보자.
import { DynamicModule, Injectable, Module } from '@nestjs/common';
import { CustomService } from './custom.service';
export interface CustomOption {
name:string,
email:string
}
@Module({})
export class CustomModule {
static forRoot(options : CustomOption): DynamicModule {
return {
module:CustomModule,
providers: [
CustomService,
{
provide: 'CUSTOM_OPTIONS',
useValue: options,
},
],
exports: [CustomService,{
provide: 'CUSTOM_OPTIONS',
useValue: options,
}],
};
}
}
핵심은 동적으로 값을 넣어주는 메소드(forRoot)의 리턴타입을 DynamicModule을 선언 해주는 것이다.
import { Inject, Injectable } from '@nestjs/common';
import { CustomOption } from './custom.module';
@Injectable()
export class CustomService {
constructor(@Inject('CUSTOM_OPTIONS') private readonly options : CustomOption){}
getHello(): string {
return `이름은 ${this.options.name} 이메일은 ${this.options.email}`;
}
}
모듈에서 사용하는 프로바이더를 쓰는 서비스 하나를 만들었다.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CustomModule } from './custom.module';
import { CustomService } from './custom.service';
@Module({
imports:[ConfigModule.forRoot({
envFilePath:(process.env.NODE_ENV === 'prod') ? '.prod.env' : '.dev.env'
}), CustomModule.forRoot({email:"wkdgusdnr55@gmail.com",name:"artlogy"})],
controllers:[AppController],
providers:[AppService, ConfigService, CustomService],
exports:[]
})
export class AppModule {}