동적 모듈(Dynamic Module)은 프로그램 실행 중에 동적으로 로드되는 모듈을 말합니다. 이는 정적으로 컴파일된 모듈과는 달리, 실행 시에 필요한 모듈을 동적으로 결정하고 로드할 수 있는 유연성을 제공합니다.
일반적으로 프로그램은 미리 정의된 모듈을 사용하여 작동합니다. 이러한 정적 모듈은 프로그램의 시작 시점에 미리 로드되고 사용됩니다. 하지만 동적 모듈은 실행 시점에 필요한 모듈을 동적으로 결정하고 로드할 수 있습니다.
NestJS에서 동적 모듈의 대표적인 예로 Config라고 부르는 모듈이 있습니다. Config 모듈은 실행 환경에 따라서 서버에 설정되는 환경 변수를 관리하는 모듈입니다.
개발, 배포 두 개의 환경에 따라 환경 변수가 달라지도록 설정해보겠습니다.
npm i cross-env @nestjs/config
필요한 패키지를 2개 설치해줍니다.
package.json에서 아래와 같이 설정해줍니다.
"scripts": {
"prebuild": "rimraf dist", // 이 부분 빼먹어서 많이 헤맸습니다.
...
"start:dev": "npm run prebuild && cross-env NODE_ENV=development nest start --watch",
...
"start:prod": "cross-env NODE_ENV=production node dist/main",
...
}
개발 환경에 개발 환경 변수 사용할 수 있음
npm run start:dev
배포 환경에서 배포 환경 변수 사용할 수 있음
npm run start:prod
cross-env는 NODE_ENV에서 development와 production을 설정해주어 서버를 켤때 로컬 환경 변수를 사용할지, 배포 환경 변수를 사용할지 정할 수 있습니다. cross-env를 안쓰면 에러가 발생합니다.
.development.env의 환경 변수 먼저 등록을 해줍니다.
PORT=5000
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=0000
DB_DATABASE=postgres
DB_SYNCHRONIZE=true
API_Key=asldfjadsfalskdfjh
app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { TypeOrmModule } from '@nestjs/typeorm'
import { typeORMConfig } from './config/typeorm.config'
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
// validationSchema,
load: [],
cache: true,
envFilePath: [
process.env.NODE_ENV === 'production'
? '.production.env'
: '.development.env',
],
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) =>
await typeORMConfig(configService),
}),
],
})
export class AppModule {}
isGlobal을 true로 하면 ConfigModule
을 전체 모듈에 등록합니다. envFilePath에 삼항 연산자를 활용하여 .development.env를 사용할지 .production.env를 사용할지 정할 수 있습니다.
ConfigModuleOptions에는 여러 가지 옵셥이 있습니다. 아래에 옵션을 활용하시면 됩니다.
export interface ConfigModuleOptions {
/**
* true이면 process.env의 값이 메모리에 캐시됩니다.
* 이것은 전체적으로 애플리케이션의 성능을 향상시킵니다.
*/
cache?: boolean;
/**
* 만약 true면, `ConfigModule`모듈을 전체 모듈에 등록합니다.
*/
isGlobal?: boolean;
/**
* 만약 true면, .env가 무시됩니다.
*/
ignoreEnvFile?: boolean;
/**
* 만약 true면, 미리 정의된 환경 변수의 유효성을 검사할 수 없습니다.
*/
ignoreEnvVars?: boolean;
/**
* 로드할 환경 파일의 경로입니다.
*/
envFilePath?: string | string[];
/**
* 환경 변수의 유효성 검사하는 사용자 지정함수.
* 환경 변수를 포함하는 객체를 입력으로 받아 검증된 환경 변수를 출력합니다.
* 만약 함수에 예외가 있으면 애플리케이션의 bootstrapping을 방지할 수 있습니다.
* 또한 환경 변수는 이 함수를 통해 편집할 수 있으며 변경 사항은 process.env 객체에 반영됩니다.
*/
validate?: (config: Record<string, any>) => Record<string, any>;
/**
* 환경 변수 유효성 검사 스키마(Joi).
*/
validationSchema?: any;
/**
* 스키마 유효성 검사 옵션.
*/
validationOptions?: Record<string, any>;
/**
* 로드할 사용자 지정 구성 파일의 배열입니다.
*/
load?: Array<ConfigFactory>;
/**
* 확장 변수의 사용을 나타내는 boolean 값 또는 dotenv-expand에 전달하는 옵션을 포함하는 개체입니다.
* .env에 확장 변수가 포함된 경우 이 속성이 true로 설정된 경우에만 구문 분석됩니다.
*/
expandVariables?: boolean | DotenvExpandOptions;
}
main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ConfigService } from '@nestjs/config'
import { ValidationPipe, Logger } from '@nestjs/common'
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
skipMissingProperties: true,
}),
)
const configService = app.get(ConfigService)
const port = configService.get('PORT') || 3000
await app.listen(port)
Logger.log(`Application running on port ${port}`)
}
bootstrap()
isGlobal: true
설정을 해주었기 때문에 ConfigService를 따로 export 해주지 않아도 사용할 수 있습니다.
.development.env에 적은 DB의 환경 변수는 어떻게 사용할 수 있을까요? src 폴더 내에 config 폴더를 만들고 그 안에 typeorm.config 파일을 만들어 줍니다. 그리고 위의 AppModule에서 TypeOrmModule을 imports 시켜주고 ConfigService를 주입시켜줍니다.
// src/config/typeorm.config
import { ConfigService } from '@nestjs/config'
import { TypeOrmModuleOptions } from '@nestjs/typeorm'
import { SnakeNamingStrategy } from 'typeorm-naming-strategies'
export const typeORMConfig = async (
configService: ConfigService,
): Promise<TypeOrmModuleOptions> => {
return {
type: 'postgres',
host: configService.get<string>('DB_HOSTNAME') || 'localhost',
port: parseInt(configService.get<string>('DB_PORT')) || 5432,
username: configService.get<string>('DB_USERNAME') || 'postgres',
password: configService.get<string>('DB_PASSWORD') || '0000',
database: configService.get<string>('DB_DATABASE') || 'postgres',
entities: [__dirname + '/../**/*.entity.{js,ts}'],
synchronize: configService.get<boolean>('DB_SYNCHRONIZE') || false,
namingStrategy: new SnakeNamingStrategy(),
// logging: true,
}
}
config 폴더에 다양한 커스텀 Config 파일을 작성하는 이유는 두 가지가 있다고 생각합니다.
첫 번째는 database.config, email.config 등과 같은 파일을 의미 있는 단위로 묶어서 관리할 수 있기 때문에 유지보수가 효율적입니다.
두 번째 Service 마다 똑같은 환경 변수를 여러 모듈에 사용할 수 있습니다.
main.ts에서 보았듯이 const port = configService.get('PORT')
환경 변수를 직접 사용하는 것 보다. const dbHost = configService('database.host')
와 같은 방식으로 사용하는 것이 긴 환경 변수를 간단하게 사용할 수 있습니다. 현재 예시는 짧습니다만 AWS의 secret 키 등 환경 변수의 이름이 길어지는 경우를 많이 봤습니다.
// config/database.config.ts
export default registerAs('database', () => ({
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT || 5432
}));
config 폴더에 커스텀 파일을 작성해보겠습니다.
import { registerAs } from '@nestjs/config'
export default registerAs('schedule', () => ({
apiKey: process.env.API_KEY,
}))
작성한 커스텀 Config파일을 App 모듈에 등록해줍니다.
import scheduleConfig from './config/schedule.config'
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [scheduleConfig], // -> 바로 여기에 load 해주면 사용할 수 있어요
cache: true,
envFilePath: [
process.env.NODE_ENV === 'production'
? '.production.env'
: '.development.env',
],
}),
...
],
})
export class AppModule {}
const key = this.configService.get<string>('schedule.apiKey');
schedule로 등록하여 apiKey라는 변수로 API_KEY 환경 변수를 가져옵니다.
NestJS의 공식문서를 살펴보면 bootstraping이란 단어를 살펴 볼 수 있습니다.
bootstraping은 NestJS 애플리케이션을 시작하는 초기화 과정을 뜻합니다.
NestJS 애플리케이션의 bootstrapping 과정은 다음과 같은 주요 단계를 포함합니다.
애플리케이션 모듈 생성
NestJS는 모듈 기반의 아키텍처를 사용합니다. 따라서 애플리케이션의 모듈을 생성하고 필요한 모듈을 포함시켜야 합니다. 모듈은 애플리케이션의 구성 요소를 정의하고 조직화하는 데 사용됩니다.
의존성 주입(Depandency Injection) 컨테이너 생성
NestJS는 의존성 주입을 사용하여 컴포넌트 간의 의존 관계를 관리합니다. 의존성 주입 컨테이너는 애플리케이션에서 사용되는 서비스, 리포지토리 등의 인스턴스를 생성하고 관리합니다.
프로바이더 등록
프로바이더는 NestJS 애플리케이션에서 사용되는 서비스, 리포지토리, 팩토리 등의 인스턴스를 나타냅니다. 애플리케이션 모듈에서 필요한 프로바이더를 등록하여 의존성 주입 컨테이너에 인스턴스를 생성하고 사용할 수 있도록 합니다.
애플리케이션 실행
애플리케이션 모듈, 의존성 주입 컨테이너, 프로바이더 등 모두 설정되면, 애플리케이션을 실행할 준비가 완료됩니다.
NestJS는 설정된 포트에서 HTTP 서버를 시작하고, 요청을 처리하며, 애플리케이션의 기능을 수행합니다.
이러한 bootstrapping 단계를 통해 NestJS 애플리케이션은 초기화되고 실행됩니다. NestJS는 이러한 구조화된 접근 방식을 통해 개발자에게 유연하고 확장 가능한 애플리케이션 개발을 제공합니다.