NestJS - 동적 모듈로 환경변수 구성
이 포스트는 NestJS 의 동적 모듈에 대해 알아본다.
- 1. 동적 모듈
- 2. dotenv 를 이용한 Config 설정 (dotenv 를 직접 이용하여 동적 모듈 생성)
- 3. NestJS 의 Config Package (
@nestjs/config
로 ConfigModule 을 동적으로 생성) - 4. 유저 서비스 환경 변수 구성
- 참고 사이트 & 함께 보면 좋은 사이트
소스는 example, user-service 에 있습니다.
1. 동적 모듈
동적 모듈 구성은 호스트 모듈 (=Provider 나 Controller 와 같은 컴포넌트를 제공하는 모듈) 을 가져다 쓰는 소비 모듈에서 호스트 모듈 생성 시 동적으로 값을 설정하는 방식이다.
동적 모듈의 예로 ConfigModule 이 있는데 ConfigModule 은 실행 환경에 따라 서버에 설정되는 환경 변수를 관리하는 모듈이다.
동적 모듈을 구성하는 방법은 @nestjs/config
로 ConfigModule 을 동적으로 생성하는 방법과 dotenv 를 직접 이용하여 동적 모듈을 생성하는 방법이 있다.
2. dotenv 를 이용한 Config 설정 (dotenv 를 직접 이용하여 동적 모듈 생성)
Node.js 에서는 dotenv 라이브러리를 이용해 각 환경 변수를 .env 파일에 저장 후 서버 구동 시 해당 파일을 읽어 환경 변수로 설정할 수 있다.
$ nest new ch06
$ npm i dotenv
$ npm i -D @types/dotenv
dotenv 는 기본적으로 루트 디렉터리에 있는 .env 파일을 읽는다.
환경 변수 파일은 민감한 정보가 저장되는 경우가 많기 때문에 repository 에 배포되지 않도록 한다. git 을 사용한다면 .gitignore 파일에 추가해야 한다.
하지만 repository 에 배포하지 않으면 배포 시 마다 직접 .env 파일을 생성해주어야 하므로 민감한 정보를 서버가 구동될 때 환경 변수로 설정하는 것이 좋다. 예를 들면 AWS 의 Secret Manager 에서 값을 읽어서 프로비저닝 과정에서 환경 변수에 넣어줄 수 있다.
NestJS 가 구동되기 전에 서버가 프로비저닝 되는 과정에서 AWS Secret Manager 에서 읽어와서 소스 코드 내의 .env 파일을 수정하도록 하는 방법이 있다.
Node.js 는 NODE_ENV
환경 변수를 통해 서버의 환경을 구성하는데 NODE_ENV
는 아래 명령어로 설정하거나 OS 구동 시 변수를 설정해야 한다.
- Windows:
set NODE_ENV=dev
- Linux:
export NODE_ENV=dev
서버 구동 시마다 새로 설정하기 번거로우니 package.json 의 npm run start:dev
를 아래와 같이 수정한다.
package.json
"prebuild": "rimraf dist", // 파일 및 폴더 삭제
"start:dev": "npm run prebuild && NODE_ENV=local nest start --watch",
// .dev.env
DATABASE_HOST=devhost
// .local.env
DATABASE_HOST=localhost
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as dotenv from 'dotenv';
import * as path from 'path';
// dotenv 패키지 직접 사용
dotenv.config({
path: path.resolve(
process.env.NODE_ENV === 'dev'
? '.dev.env'
: process.env.NODE_ENV === 'stage'
? '.stage.env'
: '.local.env',
),
});
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
app.controller.ts
@Get('env')
getEnv(): string {
return process.env.DATABASE_HOST;
}
$ curl --location 'http://localhost:3000/env'
localhost%
3. NestJS 의 Config Package (@nestjs/config
로 ConfigModule 을 동적으로 생성)
앞에선 dotenv 패키지를 직접 사용했는데 NestJS 는 dotenv 를 내부적으로 활용하는 @nestjs/config
패키지를 제공한다. 해당 패키지를 이용하여 ConfigModule 을 동적으로 생성할 수 있다.
$ npm i @nestjs/config
@nestjs/config
에는 ConfigModule 모듈이 이미 존재하므로 AppModule 에서 이 모듈을 동적으로 가져온다.
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
정적 모듈을 가져올 때와 다르게 ConfigModule.forRoot() 메서드를 호출하는데 forRoot() 는 DynamicModule 을 리턴하는 정적 메서드이다.
forRoot() 시그니처
/**
* Loads process environment variables depending on the "ignoreEnvFile" flag and "envFilePath" value.
* Also, registers custom configurations globally.
* @param options
*/
static forRoot(options?: ConfigModuleOptions): DynamicModule;
/**
* Registers configuration object (partial registration).
* @param config
*/
static forFeature(config: ConfigFactory): DynamicModule;
관례상 동적 모듈 작성 시 forRoot 나 register 를 붙임 (비동기는 forRootAsync, registerAsync)
ConfigModuleOptions
을 인수로 받기 때문에 ConfigModule 은 소비 모듈이 원하는 옵션값을 전달하여 원하는 대로 동적으로 ConfigModule 을 생성한다.
ConfigModuleOptions 시그니처
export interface ConfigModuleOptions {
/**
* If "true", values from the process.env object will be cached in the memory.
* This improves the overall application performance.
* See: https://github.com/nodejs/node/issues/3104
*/
cache?: boolean;
/**
* If "true", registers `ConfigModule` as a global module.
* See: https://docs.nestjs.com/modules#global-modules
*/
isGlobal?: boolean;
/**
* If "true", environment files (`.env`) will be ignored.
*/
ignoreEnvFile?: boolean;
/**
* If "true", predefined environment variables will not be validated.
*/
ignoreEnvVars?: boolean;
/**
* Path to the environment file(s) to be loaded.
*/
envFilePath?: string | string[];
/**
* Environment file encoding.
*/
encoding?: string;
/**
* Custom function to validate environment variables. It takes an object containing environment
* variables as input and outputs validated environment variables.
* If exception is thrown in the function it would prevent the application from bootstrapping.
* Also, environment variables can be edited through this function, changes
* will be reflected in the process.env object.
*/
validate?: (config: Record<string, any>) => Record<string, any>;
/**
* Environment variables validation schema (Joi).
*/
validationSchema?: any;
/**
* Schema validation options.
* See: https://joi.dev/api/?v=17.3.0#anyvalidatevalue-options
*/
validationOptions?: Record<string, any>;
/**
* Array of custom configuration files to be loaded.
* See: https://docs.nestjs.com/techniques/configuration
*/
load?: Array<ConfigFactory>;
/**
* A boolean value indicating the use of expanded variables, or object
* containing options to pass to dotenv-expand.
* If .env contains expanded variables, they'll only be parsed if
* this property is set to true.
*/
expandVariables?: boolean | DotenvExpandOptions;
}
이제 루트 디렉터리에 있는 .env 파일을 환경 변수로 등록한다.
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath:
process.env.NODE_ENV === 'dev'
? '.dev.env'
: process.env.NODE_ENV === 'stage'
? '.stage.env'
: '.local.env',
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
다시 curl 을 날려보면 환경 변수가 잘 출력되는 것을 확인할 수 있다.
$ curl --location 'http://localhost:3000/env'
localhost%
NestJS 가 제공하는 ConfigModule 은 .env 파일에서 읽어온 환경 변수 값을 가져오는 Provider 인 ConfigService
가 있는데 이를 컴포넌트에 주입하여 사용하면 된다.
app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Controller()
export class AppController {
constructor(
private readonly configService: ConfigService,
) {}
@Get('config')
getConfig(): string {
return this.configService.get('DATABASE_HOST');
}
}
$ curl --location 'http://localhost:3000/config'
localhost%
NestJS - Custom Provider 의 2. ClassProvider 와 비교해서 보세요.
4. 유저 서비스 환경 변수 구성
@nestjs/config
패키지와 joi
라이브러리(validation 수행) 를 설치한다.
$ npm i @nestjs/config joi
루트 디렉터리 아래 .local.env, .dev.env 파일 생성 후 아래와 같이 구성한다.
EMAIL_SERVICE=Gmail
EMAIL_AUTH_USER=YOUR-GAMIL
EMAIL_AUTH_PASSWORD=YOUR-GMAIL-PASSWORD
EMAIL_BASE_URL=http://localhost:3000
4.1. 커스텀 Config 파일 생성
@nestjs/config
패키지에서 제공하는 ConfigModule
을 이용하여 모든 환경 변수가 들어있는 .env 파일의 내용을 가져다 쓸 때엔 DatabaseConfig 등으로 의미 있는 단위로 묶어서 처리해본다.
src/config/emailConfig.ts
import { registerAs } from '@nestjs/config';
// 'email' 이라는 토큰으로 ConfigFactory 를 등록할 수 있는 함수
export default registerAs('email', () => ({
service: process.env.EMAIL_SERVICE,
auth: {
user: process.env.EMAIL_AUTH_USER,
pass: process.env.EMAIL_AUTH_PASSWORD,
},
baseUrl: process.env.EMAIL_BASE_URL,
}));
src/config/validationSchema.ts
import * as Joi from 'joi';
export const validationSchema = Joi.object({
EMAIL_SERVICE: Joi.string().required(),
EMAIL_AUIH_USER: Joi.string().required(),
EMAIL_AUTH_PASSWORD: Joi.string().required(),
EMAIL_BASE_URL: Joi.string().required().uri(),
});
4.2. 동적 ConfigModule 등록
위에서 만든 .env 파일을 루트 경로가 아닌 src/config/env 디렉터리로 옮겨서 관리하도록 설정한다.
Nestjs 의 기본 옵션은 .ts 파일 외 asset 은 제외하도록 되어있으므로 .env 파일을 out 디렉터리(dist) 에 복사할 수 있도록 nest-cli.json
옵션을 수정한다.
nest-cli.json
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [{
"include": "./config/env/*.env",
"outDir": "./dist"
}]
}
}
이제 AppModule 에 ConfigModule 을 동적 모듈로 등록한다.
app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { ConfigModule } from '@nestjs/config';
import emailConfig from './config/emailConfig';
import { validationSchema } from './config/validationSchema';
@Module({
imports: [
UsersModule,
ConfigModule.forRoot({
envFilePath: [`${__dirname}/config/env/.${process.env.NODE_ENV}.env`],
load: [emailConfig], // ConfigFactory 지정
isGlobal: true, // 전역으로 등록해서 어느 모듈에서나 사용 가능
validationSchema, // 환경 변수 값에 대해 유효성 검사 수행
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
이제 emailConfig 를 주입받아서 사용해본다.
/src/email/email.service.ts
import { Inject, Injectable } from '@nestjs/common';
import Mail from 'nodemailer/lib/mailer';
import * as nodemailer from 'nodemailer';
import emailConfig from '../config/emailConfig';
import { ConfigType } from '@nestjs/config';
interface EmailOptions {
to: string;
subject: string;
html: string;
}
@Injectable()
export class EmailService {
private transporter: Mail;
// constructor() {
// this.transporter = nodemailer.createTransport({
// service: 'Gmail',
// auth: {
// user: 'test@gmail.com',
// pass: 'aaaaa',
// },
// });
// }
constructor(
// 주입받을 때 @Inject 데커레이터의 토큰을 앞에서 만든 ConfigFactory 의 KEY 인 `email` 문자열로 넣어준다.
@Inject(emailConfig.KEY) private config: ConfigType<typeof emailConfig>,
) {
this.transporter = nodemailer.createTransport({
service: config.service, // env 파일에 있는 값들
auth: {
user: config.auth.user,
pass: config.auth.pass,
},
});
}
// 가입 인증 메일 발송
async sendMemberJoinVerification(email: string, signupVerifyToken: string) {
const baseUrl = this.config.baseUrl;
const url = `${baseUrl}/users/email-verify?signupVerifyToken=${signupVerifyToken}`;
const mailOptions: EmailOptions = {
to: email,
subject: '가입 인증 메일',
html: `가입확인 버튼를 누르시면 가입 인증이 완료됩니다.<br />
<form action="${url}" method="POST">
<button>가입확인</button>
</form>`,
};
return await this.transporter.sendMail(mailOptions);
}
}
$ curl --location 'http://localhost:3000/users' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "assu",
"email": "test@test.com",
"password": "test"
}'
정상적으로 메일이 오는 것을 확인할 수 있다.
$ tree -L 4 -N -I "node_modules"
.
├── app.module.ts
├── config
│ ├── emailConfig.ts
│ ├── env
│ │ ├── .dev.env
│ │ ├── .local.env
│ └── validationSchema.ts
├── email
│ ├── email.module.ts
│ └── email.service.ts
├── main.ts
└── users
├── UserInfo.ts
├── dto
│ ├── create-user.dto.ts
│ ├── user-login.dto.ts
│ └── verify-email.dto.ts
├── users.controller.ts
├── users.module.ts
└── users.service.ts
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 한용재 저자의 NestJS로 배우는 백엔드 프로그래밍을 기반으로 스터디하며 정리한 내용들입니다.
- NestJS로 배우는 백엔드 프로그래밍
- NestJS로 배우는 백엔드 프로그래밍 - Github
- NestJS 공식문서
- NestJS docs
- Nest.js Github
- NestJS 공식 예제 Starter 프로젝트 Github
- dotenv - Github