NestJs Config

이우길·2022년 3월 28일
1

NestJs

목록 보기
2/20
post-thumbnail

NestJs에서는 환경을 어떻게 나눌까?

예제코드는 github에 있습니다!

Goal

  • NestJs에서 Configuration를 어떻게 나눌 수 있는지에 대한 이해

  • test, dev, production 환경 구성하기


환경을 나눠야 하는 이유

애플리케이션을 개발할 때 하나의 환경에서 개발하여 배포까지 이뤄지지는 않는다.

Test를 할 때 필요한 환경, 개발을 할 때 필요한 환경, Production에서의 환경 등등.. 여러 환경이 필요하게 된다.

각 환경에서 사용되는 환경변수를 분리하고 상황에 맞게 가져다 쓰기 위해 NestJs에서는 @nestjs/config를 제공해준다.

마치 Spring에서 application.yml, application.properties에 각각 환경을 나누는 것과 비슷하다.


NestJs에서 제공하는 @nestjs/config

NestJs에서는 환경을 구성하기 위해 필요한 dependency를 제공해준다. project의 root에서 아래의 명령어를 터미널에 입력하면 dependency를 추가할 수 있다.

yarn add @nestjs/config

해당 패키지는 내부적으로 Node에서 환경을 나눌 때 주로 사용하는 dotenv를 내부적으로 사용하고 있다고 합니다.

The @nestjs/config package internally uses dotenv.


사용법

환경변수 파일생성

사용법은 간단하다. 먼저 환경변수를 저장할 .env파일을 생성한다. Project Root에서 3개의 .env 파일을 생성하였다.

src
 ㄴ ...
 ㄴ .production.env
 ㄴ .development.env
 ㄴ .test.env
 ㄴ main.ts

각각의 env파일에는 다음과 같은 환경변수들이 담겨있다.

DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
# 각 환경에 맞게 DATABASE_USER명이 다름
DATABASE_USER= 
DATABASE_PASSWORD=password

@nestjs/config를 이용하여 Root모듈에 ConfigModule등록

@nestjs/config에서 제공하는 ConfigModulestatic method인 forRoot()를 이용하여 루트 모듈에 import를 해준다.

@Module({
  imports: [
    ConfigModule.forRoot()
  ],
  controllers: [AppController],
  providers: [AppService],
})

forRoot()의 인자로는 ConfigModuleOptions를 받을 수 있는데 ConfigModuleOptionsinterface는 아래와 같이 정의되어 있다.

export interface ConfigModuleOptions {
    cache?: boolean;
    isGlobal?: boolean;
    ignoreEnvFile?: boolean;
    ignoreEnvVars?: boolean;
    envFilePath?: string | string[];
    encoding?: string;
    validate?: (config: Record<string, any>) => Record<string, any>;
    validationSchema?: any;
    validationOptions?: Record<string, any>;
    load?: Array<ConfigFactory>;
    expandVariables?: boolean | DotenvExpandOptions;
}

여러 옵션들이 있지만 살펴볼 옵션들은 다음과 같다. (나머지는 필요할 때 docs를 참고하여 사용하는 것으로~)

  • isGlobal?: boolean

  • ignoreEnvFile?: boolean

  • envFilePath?: string | string[]

  • validationSchema?: any;


isGlobal?: boolean

해당 Option은 Root Module에 ConfigModule을 등록할 때 key 이름 그래도 Global로 등록을 하는지에 대한 boolean 값이다.

Global로 등록을 하지 않으면 해당 모듈을 사용하는 곳에서 import를 받아야

하지만

Global로 등록을 하게 되면 해당 모듈안에 있는 provider들을 import하지 않고 Injection받아 사용할 수 있다.

참조 Global Module - NestJs


ignoreEnvFile?: boolean

env파일을 사용할 것인지 아닌지에 대한 flag값을 설정한다. 해당 flag값이 true가 되면 env의 값들을 읽어오지 않는다.


envFilePath?: string | string[]

단독으로 지정할 수도 있고 배열로도 지정할 수 있으며 배열로 지정할 경우 순서대로 탐색하여 가장 먼저 발견되는 .env파일을 로드하게 된다.

주로 환경변수를 읽어 드릴 때 key가 되는 환경변수를 NODE_ENV로 사용하게 된다. 조건식이나 NODE_ENV를 이용하여 정의하며 나는 아래와 같이 정의하였다. (src안에 config/env 디렉토리를 만들어 그 안에 .*.env들을 넣어놓았다.)

envFilePath: `${__dirname}/config/env/.${process.env.NODE_ENV}.env`

서버를 실행할 때 NODE_ENV를 정의하고 실행시켜 NODE_ENV와 동일한 파일명의 .env가 읽어지게 되는 것이다. 중요한 점은 NestJs를 빌드할 때 .ts파일을 제외한 assets파일들은 제외가 되여 dist 디렉토리에 포함되지 않는다.

그렇기 때문에 배포할 때 .env파일들을 포함하고 싶다면 nest-cli.json파일에 option을 추가해줘야 한다.

{
  ...
  "compilerOptions": {
    "assets": [
      {
        "include": [config파일의 경로],
        "outDir": "./dist/[필요하다면 추가 경로]"
      }
    ]
  }
}

위의 option을 추가하면 NestJs빌드 결과물에 .env가 포함되개 된다. (꼭 .env말고도 static 파일들을 추가할 때 해당 option을 부여하면 빌드 결과물에 추가할 수 있다.)


validationSchema?: any;

해당 option은 .env의 값들을 validation할 수 있는 option이다. ConfigModuleOptions에 달려있는 주석은 다음과 같다.

/**
 * Environment variables validation schema (Joi).
 */
validationSchema?: any;

validation을 제공하는 Joi 의존성을 추가하여 작성할 수 있다.

  • 의존성 추가

    yarn add joi
  • Joi사용 (NODE_ENV, DataBase정보에 대한 validation)

    @Module({
      imports: [
        ConfigModule.forRoot({
          //...
          validationSchema: Joi.object({
            NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
            DATABASE_HOST: Joi.string().required(),
            DATABASE_PORT: Joi.string().required(),
            DATABASE_USER: Joi.string().required(),
            DATABASE_PASSWORD: Joi.string().required(),
          }),
          //...
        }),
      ]
    })

위의 예제코드처럼 Joi.object({})를 이용하여 .env에 값에 대한 type지정 및 required의 여부를 정의할 수 있다. 만약 위에 정의한 validationSchema에 맞지 않는다면 아래와 같은 error를 볼 수 있을 것이다. (NODE_ENV를 부여하지 않고 실행한 결과이다.)

Error: Config validation error: "NODE_ENV" is required. \
"DATABASE_HOST" is required. \
"DATABASE_PORT" is required. \
"DATABASE_USER" is required. \
"DATABASE_PASSWORD" is required

추가적으로 validationOptions라는 Option을 ConfigModuleOptions에 추가할 수 있는데
그 중에서도 abortEarly가 있다.

flag값에 true를 주게되면 위의 error에서는 validation의 대상이 되는 모든 환경변수를 validate하였지만 validate에서 error가 발생하면 그 시점에 바로 유효성 검사를 중단하고 error를 뱉는다. (기본값은 false)

  • validationOption 추가

    @Module({
      imports: [
        ConfigModule.forRoot({
          //...
          validationOptions: {
          abortEarly: true,
          },
        }),
      ]
    })
  • validationOption추가 후 NODE_ENV 값을 부여하지 않았을 때 error메세지

    Error: Config validation error: "NODE_ENV" is required

추가적인 validationOption을 확인하고 싶다면 Joi validate를 참조


ConfigModule 가져다가 사용하기

처음 forRoot()를 이용하여 ConfigModule를 Root모듈에 등록을 할 때 isGlobal Option을 true로 주었던 것을 기억할 것이다.(isGlobal Option이 false인 경우 사용하고자 하는 Module에서 ConfigModule를 import 받아야한다.)

이 후 환경변수들이 필요한 곳이서는 process.evn.${}로 접근을 하는 것이 아니라 ConfigService를 주입받아 사용하게 된다.

간단하게 AppController의 생성자에서 주입받아 사용해보겠다.

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly configService: ConfigService,
  ) {}

  @Get()
  getHello(): string {
    console.log(process.env.NODE_ENV);
    console.log(this.configService.get('DATABASE_HOST'));
    console.log(this.configService.get('DATABASE_PORT'));
    console.log(this.configService.get('DATABASE_USER'));
    console.log(this.configService.get('DATABASE_PASSWORD'));
    return this.appService.getHello();
  }
}

위와 같이 ConfigService를 주입받고 get() method를 이용하여 환경변수 key값을 인자로 넣어주면 해당하는 값을 받을 수 있다.

NODE_ENVproduction으로 하여 실행하고 HTTP Request를 보낸 후 console에 찍힌 결과이다.

//request
curl -X GET http://localhost:3000s

//output
127.0.0.1
5432
production
password

Reference

profile
leewoooo

0개의 댓글