NestJS 帶你飛! 시리즈 번역 16# Configuration

2023. 6. 10. 16:04개발 문서 번역/NestJS

728x90
이 포스팅은 「NestJS 기초실무 가이드 : 강력하고 쉬운 Node.js 웹 프레임워크로 웹사이트 만들기」
(서명: NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式)
책이 출간되었습니다.

 

전의 포스팅에서 Dynamic Module와 dotenv를 사용하여 간단하게 환경변수를 관리하는 모듈을 만들었습니다.

하지만 환경변수란 무엇일까요? 왜 환경변수를 만들어 관리해야 하는걸까요?

 

환경 변수

일반적으로 시스템은 다양한 환경에서 실행되곤 합니다.
간단하게 나누어보면 개발 환경, 프로덕션 환경으로 나눌 수 있을겁니다.
이렇게 나누는 이유는 개발/테스트 환경이 프로덕션 환경의 데이터에 영향을 주지 않기 위해서입니다.

또한 데이터베이스도 두개로 나뉘는데 이때 두개의 데이터베이스 연결 정보 또한 관리해주어야 하는데 이때 어떻게 하면 빠르게 환경을 바꿀 수 있을까요? 연결 정보를 담은 변수들을 그대로 코드에 넣어버리는 것은 그리 좋은 방법은 아닙니다.

이를 해결하기 위해 나온것이 바로 "환경변수 (Environment Variable, 環境變數)"입니다.

 

환경 변수와 일반 변수의 다른점은 

환경변수는 코드 외부에 작성되며 이런 변수들은 운영 체제에서 바로 설정이 가능하며 커맨드의 형식으로 설정할 수도 있습니다.

아래는 node.js의 예제이며 먼저 커맨드를 사용해 설정해보겠습니다.

$ NODE_ENV=production node index.js

 

이렇게 작성하면 process.env를 통해 해당 환경 변수를 불러올 수 있습니다. 

그러나 이렇게 매번 입력하고 호출 하는것은 관리도 어려울 뿐더러 그리 좋은 방법은 아닙니다.

이때 등장한것이 바로 환경변수를 담아놓은 파일입니다. node.js에서 가장 자주 사용되는건 .env 파일입니다.

작성 방식도 매우 간단합니다. =의 왼쪽엔 key값, 오른쪽엔 value를 작성해주면 됩니다. 간단하죠?

USERNAME=WOO

Nest에서 ConfigModule을 이미 제공하고 있습니다. 이 모듈을 통해 환경변수를 읽고, 관리할 수 있습니다.

물론 Dynamic Module을 통해 자기가 설계해도 됩니다.

 

ConfigModule 설치

자기가 설계하는것도 좋은 방법이지만, 이미 좋은 라이브러리가 만들어져 있으며 전 포스팅에서 간단하게 Dynamic Module 구현하는 방법에 대해 설명했었습니다.
이번 포스팅에서는 어떻게 공식 사이트에서 제공하는 라이브러리를 사용해야 하는지에 대해 배워보겠습니다. 이 라이브러리는 내장 라이브러리가 아니어서 따로 설치해주어야 합니다. npm으로 설치하면 됩니다.

주의: 만약 이미 dotenv를 설치했다면 삭제해주세요.
$ npm install @nestjs/config --save

 

ConfigModule 사용하기

ConfigModule은 Dynamic Module의 개념으로 설계되었습니다.
AppModuleforRoot 메서드와 같이 추가해주면 사용할 수 있습니다.

아래는 app.module.ts의 예제입니다.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';

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

이어서 프로젝트 경로 아래에 .env 파일을 생성하여 아래와 같이 입력해봅시다.

USERNAME=WOO
주의: 프로젝트의 경로 아래에 생성해야 합니다. src 폴더가 아닌 package.json와 같은 경로입니다.

 

app.controller.ts를 수정해보겠습니다.

AppcontrollerconstructorConfigService를 주입해주고, getHelloConfigServiceget 메서드를 통해 USERNAME을 반환하도록 해보겠습니다.

 

http://127.0.0.1:3000 에 접속해보면..

원하는 결과를 얻을 수 있습니다.

 

사용자 정의 환경변수 파일 사용하기

기본 설정에서는 ConfigModule은 프로젝트 경로의 .env 파일을 환경 변수 파일을 사용합니다.

하지만 종종 다른 환경에 대해 여러 파일을 구성해야 할 때가 있습니다.

이때 사용자 정의 환경 변수 파일을 통해 처리할 수 있습니다.

ConfigModuleforRoot 정적 메서드는 envFilePath 파라미터를 제공하여

.env로써 사용할 파일의 이름을 지정할 수있습니다.

아래는 예제이며, 여기서는 development.env를 사용하도록 해보겠습니다.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: 'development.env'
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

방금 .env파일의 이름을 development.env로 설정한 뒤 서버를 다시 껐다 키고

http://127.0.0.1:3000 으로 접속해봅시다.

 

또 다른 경우로는 로컬 테스트용 환경 변수와 다른 환경에서의 테스트용 환경 변수가 다른 경우입니다.

이 경우에는 우선순위를 두어 해결이 가능합니다.

예를 들어 로컬 환경에서 사용하는 환경 변수의 이름이 development.local.env, 다른 환경에서 쓰는 파일의 이름이 development.env인 경우에 envFilePath를 배열로 선언하여 설정할 수 있습니다.

배열의 내용은 파일의 이름이며, 앞에 작성할 수록 우선순위가 높습니다.

여기서는 먼저 development.local.env 파일을 생성하고 아래 내용을 추가해보겠습니다.

USERNAME=local_tester

 app.module.ts를 수정하여 development.local.env가 먼저 우선순위를 갖게 해보겠습니다.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['development.local.env', 'development.env']
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

http://127.0.0.1:3000에 접속하면 usernamelocal_tester로 변한걸 확인할 수 있습니다.

팩토리 함수 사용하기

복잡한 상황에서는 팩토리 패턴을 통해 환경변수를 처리할 수 있습니다.

ex: development.env가 이미 구성되어 있지만
덜 민감한 정보들은 기본값을 사용할 수 있어 파일에서 따로 구성해줄 필요가 없습니다.

그럼 저 덜 민감한 정보들은 어디에 보관해야 할까요? 바로 팩토리 함수에서 구성해주면 됩니다.

src 폴더 아래 config라는 폴더를 만들어 그 안에 configuration.factory.ts를 생성해보겠습니다.

configuration.factory.ts를 아래와 같이 수정하여 기본 PORT3000으로 설정해줍니다.

export default () => ({
	PORT: process.env.PORT || 3000
});

이어서 app.module.ts의 내용을 수정해보겠습니다.forRoot 정적 메서드에 load 파라미터를 추가해줍니다.

해당 파라미터는 배열 형태이며, 팩토리 함수를 포함합니다.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import configurationFactory from './config/configuration.factory';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['development.local.env', 'development.env'],
      load: [configurationFactory]
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}
주의: load 파라미터가 배열이 가능한 이유는 다수의 팩토리 함수를 통해 환경변수를 처리할 수 있기 때문입니다.

app.controller.ts를 수정하여 getHelloConfigServiceget 메서드를 받아와 USERNAMEPORT를 반환하도록 해보겠습니다.

import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

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

  @Get()
  getHello() {
    const username = this.configService.get('USERNAME');
    const port = this.configService.get('PORT');
    return { username, port };
  }
}

http://127.0.0.1:3000에 접속해보면...

팩토리 함수를 사용하여 네임스페이스 구성하기

환경 변수는 = 기호로 keyvalue가 구분됩니다.

value 부분에서는 다음 계층으로 확장이 되지 않으므로 환경변수의 계층은 '편평하다' 라고 정의할 수있습니다.

현재는 유형에 따라 분류되지 않았다는걸 의미하겠죠?

환경변수 파일 development.local.env에 아래와 같은 정보들이 담겨있다고 가정해보겠습니다.

 

DB_HOST=example.com
DB_PASSWORD=12345678
PORT=3000

DB_HOSTDB_PASSWORD는 동일한 선상에 있는 항목이지만

다른 항목은 DB 접속과 그렇게 관련 없는 항목입니다. 한번 계층을 나누어볼까요?

먼저 아래와 같이 수정합니다.

{
  "DB_HOST": "example.com",
  "DB_PASSWORD": "12345678",
  "PORT": "3000"
}

이상적인 상황은 비슷한 성질의 자료들을 같은 네임스페이스로 묶어버려 분류하는 상황일것입니다.

{
  "database": {
    "host": "example.com",
    "password": "12345678"
  },
  "port": "3000"
}

아쉽게도 환경 변수 파일에서 이런 구성을 직접 할 수는 없습니다.

하지만 팩토리 함수를 사용하여 처리를 할 수 있는데요. configuration.factory.ts를 수정해야 합니다.

registerAs 이 함수를 통해 네임스페이스를 지정할 수 있습니다.

첫번째 파라미터로는 네임스페이스, 두번째 파라미터로는 Callback을 정의해줍니다.

반환하는 내용은 이미 정리된 객체를 반환합니다.

 

import { registerAs } from '@nestjs/config';

export default registerAs('database', () => ({
  host: process.env.DB_HOST,
  password: process.env.DB_PASSWORD
}));

app.controller.ts를 수정해봅시다.

코드를 보게되면 네임스페이스안의 환경변수를 사용하고자 한다면 . 을 통해 불러올 수 있습니다.

이는 Object 자료형을 다루는 방법과 같습니다.

import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

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

  @Get()
  getHello() {
    const database = this.configService.get('database');
    const db_host = this.configService.get('database.host'); // database 안의 host를 불러옵니다.
    const port = this.configService.get('PORT');
    return { database, db_host, port };
  }
}

http://127.0.0.1:3000에 접속해보면..

정말 유용하지 않나요? 우리가 원하는대로 분류를 할수 있게되었습니다.

 

 

main.ts에서 ConfigService 사용하기

환경 변수 파일에서 port를 구성하여 Nest를 시작할 때 해당 포트를 사용하고 싶을 때가 있습니다.

이때는 main.ts에서 처리해주어야 하는데요. ConfigService를 얻어오려면 app 인스턴스에서 제공하는 get 메서드를 사용해야 합니다.

import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService); // ConfigService 불러오기
  const port = configService.get('port');
  await app.listen(port);
}
bootstrap();

 

환경변수 파일을 확장하여 사용하는 방법

먼저 두 개의 환경변수가 종속 관계를 갖는다고 가정해보겠습니다.

APP_DOMAIN=example.com
APP_REDIRECT_URL=example.com/redirect_url

APP_REDIRECT_URLAPP_DOMAIN을 포함하고 있는것을 확인할 수 있습니다.

하지만 환경변수 파일에서는 변수를 선언하는 기능이 없기 때문에 관리 측면에서 약간의 어려운 측면이 있습니다.

다행히도 Nest에서는 이를 보완하기 위해 forRoot 객체 파라미터 안에 expandVariablestrue로 설정하여
환경 변수 파일을 파싱하여 변수 선언 기능과 똑같이 사용할 수 있도록 구현하였습니다.

${...}을 사용하여 지정된 환경 변수를 삽입할 수 있습니다.

아래는 development.local.env의 내용입니다.

APP_DOMAIN=example.com
APP_REDIRECT_URL=${APP_DOMAIN}/redirect_url

app.module.ts을 수정해보겠습니다.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['development.local.env', 'development.env'],
      expandVariables: true // 환경변수파일에 변수 삽입 기능 활성화
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

app.controller.ts를 수정하여 getHello 메서드가 APP_DOMAINAPP_REDIRECT_URL을 반환하도록 하겠습니다.

http://127.0.0.1:3000에 접속해 결과를 확인해 봅시다.

 

전역 ConfigModule

ConfigModule을 다른 모듈에서도 사용하고 싶을 경우 isGlobaltrue로 설정하여 전역 모듈로 사용할 수 있습니다.

이렇게 하면 ConfigModule을 다른 모듈에서 따로 불러오지 않아도 되는 장점이 있습니다.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['development.local.env', 'development.env'],
      isGlobal: true
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

 

 

마치며

환경 변수의 설정은 필수입니다. 다양한 환경에서 실행될 경우에는 ConfigModule과 같은 모듈을 사용하여
유지보수 비용을 줄여나가야 합니다. 아래는 오늘 배운 내용을 간단히 정리한 요약본입니다.

 

1. Nest에서는 환경변수를 관리할 수 있는 모듈인 ConfigModule을 제공합니다.

2. CofigModule은 Dynamic Module의 개념을 사용하여 구현합니다.

3. .env 파일을 통해 환경변수를 설정할 수 있습니다.

4. envFilePath을 통해 사용자 정의 환경변수 파일을 지정할 수 있습니다.

5. envFilePath는 우선순위를 설정할 수 있습니다.

6. 팩토리 함수와 load을 사용하여 환경변수를 처리할 수 있습니다.

7. 팩토리 함수에서 네임스페이스를 구성하여 각 환경 변수의 유형들을 정리할 수 있습니다.

8. main.ts에서 ConfigService를 가져와 환경 변수의 값을 얻어올 수 있습니다.

9. expandVariables를 통해 환경변수 파일에 변수를 선언하는 방법(${변수명})과 같이 삽입하여 사용할 수 있습니다.

10. isGlobal을 통해 ConfigModule을 전역 모듈로써 사용할 수 있습니다.