Logo

NestJS 앱의 환경 설정

NestJS 앱을 개발,테스트, 운영 등 다양한 환경에 배포하려면 어느 환경에 배포하느냐에 따라서 다르게 설정되야하는 값들이 생기기 마련이죠?

이번 포스팅에서는 NestJS 앱에서 이렇게 환경 별로 달라지는 설정 값들을 어떻게 효과적으로 관리할 수 있는지 알아보겠습니다.

실습 프로젝트 구성

먼저 간단한 실습을 위해서 NestJS 프로젝트가 하나 필요할 것 같은데요. 터미널에서 NestJS CLI 도구의 nest new 명령어를 실행하여 새로운 프로젝트를 구성하도록 하겠습니다.

$ nest new our-nestjs
⚡  We will scaffold your app in a few seconds..

? Which package manager would you ❤️  to use? (Use arrow keys)npm
  yarn
  pnpm

NestJS CLI를 설치하고 NestJS 프로젝트를 구성하는 기본적인 방법은 관련 포스팅을 참고 바랍니다.

@nestjs/config 패키지 설치

우선 NestJS에서 제공하는 @nestjs/config라는 npm 패키지를 실습 프로젝트에 설치하도록 하겠습니다.

$ npm i @nestjs/config
warn preInstall No repository field
┌ [1/4] 🔍  Resolving dependencies
└ Completed in 5.874s
┌ [2/4] 🚚  Fetching dependencies
│ info pruneDeps Excluding 1 dependency. For
│ more information use `--verbose`.
└ Completed in 7.545s
┌ [3/4] 🔗  Linking dependencies
└ Completed in 4.681s
info security We found `install` scripts which
turbo skips for security reasons. For more
information see
https://turbo.sh/install-scripts.
└─ @nestjs/core@9.3.9

success Saved lockfile "package-lock.json"
success Updated "package.json"

success Install finished in 18.174s

이 패키지를 사용하면 좀 더 쉽게 배포 환경에 따라 달라지는 설정 값을 관리할 수 있습니다.

ConfigModule 설정

최상위 앱 모듈에서 @nestjs/config 패키지의 ConfigModule를 불러와 설정해줍니다.

src/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({      cache: true,      isGlobal: true,    }),  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

위와 같이 설정만 해주면 바로 앱의 어디서든지 .env 파일이나 운영체제 수준에서 설정된 환경 변수에 접근할 수 있는 준비가 끝나는데요. 참고로 여기서 cache 옵션을 true로 준 이유는 한 번 읽은 환경 변수의 값을 캐싱하여 읽기 속도를 향상하기 위함이고, isGlobal 옵션을 true로 준 이유는 ConfigModule을 다른 모든 모듈에서 불러와야하는 번거로움을 피하기 위함입니다.

일반적으로 자바스크립트에서 환경 변수를 어떻게 다루는지에 대해서 생소하시다면 아래 관련 포스팅을 먼저 읽어보시면 도움이 되실 거에요!

ConfigService 사용

이제 ConfigModule 모듈에서 제공하는 ConfigService를 통해서 환경 변수에 접근해볼까요?

우선 환경 변수를 읽어야하는 서비스나 컨트롤러 클래스에 ConfigService를 생성자를 통해서 주입해야하는데요. 그 다음에는 get() 메서드에 인자로 환경 변수 이름을 넘기면 환경 변수의 값이 반환됩니다.

src/app.service.ts
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class AppService {
  constructor(private configService: ConfigService) {}
  getHello() {
    const host = this.configService.get<string>("HOST");    const port = this.configService.get<number>("PORT", 3000);    return {
      host,
      port,
    };
  }
}

여기서 get()은 제네릭(generic) 메서드이므로 <> 괄호 안에 타입을 명시하여 특정 자료형(data type)으로 환경 변수 값을 반환받을 수 있습니다. 다른 Node.js 앱처럼 process.env로 부터 통해 환경 변수를 읽어왔다면 직접 자료형을 변환을 했어야했을텐데요. 개발자 입장에서 매우 편리한 부분입니다.

뿐만 아니라 get() 메서드를 호출할 때 두 번째 인자로 환경 변수가 설정되지 않았을 때 사용할 기본값도 넘길 수도 있는데요. 이 두 번째 인자의 자료형도 <> 괄호 안으로 넘겼던 자료형에 일치하기 때문에 버그(bug)를 예방하는데 도움이 됩니다.

이제 테스트를 위해서 .env 파일에 HOSTPORT 환경 변수의 값을 저장하겠습니다.

.env
HOST=localhost
PORT=3001

그리고 터미널에서 / 엔드포인트를 호출해보면 다음과 같이 환경 변수의 값을 담은 JSON 전문이 응답될 것입니다.

$ curl http://localhost:3001/status
{"host":"localhost","port":"3001"}

설정 파일로 관리

환경 변수에 바로 접근하는 대신에 환경 변수의 값들을 별도의 설정 파일에 저장해두고 관리할 수도 있는데요.

예를 들어, 애플리케이션(app)과 관련된 설정과 데이터베이스(db)와 관련된 설정을 객체로 반환하는 함수를 작성해보겠습니다.

config/configuration.ts
export default () => ({
  app: {
    host: process.env.HOST || "localhost",
    port: parseInt(process.env.PORT, 10) || 3000,
  },
  db: {
    host: process.env.DB_HOST || "127.0.0.1",
    port: parseInt(process.env.DB_PORT, 10) || 5432,
  },
});

그리고 최상위 모듈에서 ConfigModule을 불러올 때 load 옵션으로 이 함수를 설정해줍니다.

src/app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import configuration from "./config/configuration";
@Module({
  imports: [
    ConfigModule.forRoot({
      cache: true,
      isGlobal: true,
      load: [configuration],    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

그 다음 서비스 클래스에서 동일한 방법으로 ConfigService를 통해 객체에 저장된 환경 변수 값에 접근합니다.

src/app.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AppService {
  constructor(
    private configService: ConfigService,
  ) {}

  getHello() {
    const host = this.configService.get<string>('app.host');    const port = this.configService.get<number>('app.port', 3000);    const dbHost = this.configService.get<string>('db.host');    const dbPort = this.configService.get<number>('db.port', 5432);    return {
      host,
      port,
      dbHost,
      dbPort,
    };
  }
}

이제 테스트를 위해서 .env 파일에 DB_HOSTDB_PORT 환경 변수의 값을 추가하겠습니다.

.env
HOST=localhost
PORT=3001
DB_HOST=127.0.0.1DB_PORT=5432

그리고 터미널에서 / 엔드포인트를 호출해보면 다음과 같이 애플리케이션 뿐만 아니라 데이터베이스에 대한 환경 변수의 값을 담은 JSON 전문이 응답될 것입니다.

$ curl http://localhost:3001/status
{"host":"localhost","port":3001,"dbHost":"127.0.0.1","dbPort":5432}

ConfigType 사용

이번에는 ConfigModule 모듈에서 제공하는 ConfigType를 통해서 환경 변수에 접근해보려고 하는데요. ConfigType을 사용하면 ConfigService보다 좀 더 type-safe한 방식으로 환경 변수를 읽어올 수 있으며, 기능 별로 환경 변수를 분리하여 관리하기가 용이해집니다.

우선 위에서 작성한 설정 파일을 아래와 같이 두개의 파일로 분리할건데요. @nestjs/config 패키지의 registerAs 함수를 사용하여 무엇과 관련된 설정인지를 명시해줍니다.

config/app.config.ts
import { registerAs } from "@nestjs/config";

export default registerAs("app", () => ({
  host: process.env.HOST || "localhost",
  port: parseInt(process.env.PORT, 10) || 3000,
}));
config/db.config.ts
import { registerAs } from "@nestjs/config";

export default registerAs("db", () => ({
  host: process.env.DB_HOST || "127.0.0.1",
  port: parseInt(process.env.DB_PORT, 10) || 5432,
  name: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
}));

그리고 최상위 모듈에서 ConfigModule을 불러올 때 load 옵션으로 appConfig을 설정해줍니다.

src/app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import appConfig from "./config/app.config";
@Module({
  imports: [
    ConfigModule.forRoot({
      cache: true,
      isGlobal: true,
      load: [appConfig],    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

그러면 서비스 클래스로 appConfig를 주입할 수 있는데요. 이 때 자료형을 ConfigType을 사용하여 자료형을 명시해줄 수 있습니다.

src/app.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';import appConfig from './config/app.config';
@Injectable()
export class AppService {
  constructor(
    @Inject(appConfig.KEY)    private config: ConfigType<typeof appConfig>,  ) {}

  getHello() {
    const host = this.config.host;    const port = this.config.port;    return {
      host,
      port,
    };
  }
}

그러면 마치 진짜 객체를 다루는 것처럼 this.config 객체를 통해 hostport 속성을 읽어올 수 있습니다. 코드 편집기에서 자동 완성을 해주기 때문에 ConfigService를 사용할 때 대비 오타가 발생할 확률이 현저하게 줄어드는 이점이 있습니다.

기능별 설정 관리

위에서 설정 파일을 두개로 분리해놓고 두 번째 파일은 아직 사용하지 않았는데요. 이 데이터베이스 관련 설정은 데이터베이스 모듈에서 사용해보겠습니다.

config/db.config.ts
import { registerAs } from "@nestjs/config";

export default registerAs("db", () => ({
  host: process.env.DB_HOST || "127.0.0.1",
  port: parseInt(process.env.DB_PORT, 10) || 5432,
  name: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
}));

먼저 터미널에서 NestJS CLI 도구를 통해서 우선 db 모듈과 서비스를 생성하겠습니다.

$ nest generate module db && nest generate service db

그리고 db 모듈에서 ConfigModuleforRoot() 메서드 대신에 forFeature 메서드를 사용하여 dbConfig를 설정해줍니다.

src/db.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { DbService } from "./db.service";
import { DbController } from "./db.controller";
import dbConfig from "../config/db.config";
@Module({
  imports: [ConfigModule.forFeature(dbConfig)],  providers: [DbService],
  controllers: [DbController],
})
export class DbModule {}

그러면 db 서비스에서 마찬가지 방식으로 dbConfig를 주입받아서 데이터베이스 관련 설정 값들을 읽어올 수 있습니다.

src/db.service.ts
import { Inject, Injectable } from "@nestjs/common";
import { ConfigType } from "@nestjs/config";import dbConfig from "../config/db.config";
@Injectable()
export class DbService {
  constructor(
    @Inject(dbConfig.KEY)    private config: ConfigType<typeof dbConfig>  ) {}

  getUrl() {
    const { user, password, host, port, name } = this.config;    return `${user}:${password}@${host}:${port}/${name}`;
  }
}

main.ts에서 설정 값 읽기

마지막으로 main.ts 파일에서 어떻게 환경 설정 값을 읽어올 수 있는지에 대해서 알아보겠습니다. 마찬가지로 ConfigServiceConfigType 모두 사용이 가능한데요.

예를 들어, 애플리케이션이 리스닝하는 포트를 환경 변수에서 읽어와서 설정하고 싶다면 다음과 같은 두가지 방식을 사용할 수 있겠습니다.

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);  const port = configService.get<number>("PORT", 3000);  await app.listen(port);
}
bootstrap();
import { NestFactory } from "@nestjs/core";
import { ConfigType } from "@nestjs/config";import { AppModule } from "./app.module";
import appConfig from "./config/app.config";
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const config = app.get < ConfigType < typeof appConfig >> appConfig.KEY;  const port = config.port;  await app.listen(port);
}
bootstrap();

전체 코드

실습 프로젝트의 코드는 아래에서 직접 확인하고 실행해볼 수 있습니다.

마치면서

이상으로 간단한 실습을 통해서 NestJS 앱에서 어떻게 환경 설정을 할 수 있는지에 대해서 알아보았습니다.

NestJS에 관련된 다른 포스팅은 관련 태그를 참고 바라겠습니다.