본문 바로가기
나의 개발일지/node.js

NestJS 환경변수 관리 - process.env보단 ConfigModule을

by stella_gu 2024. 2. 7.

Express.js를 사용할 땐 Node.js 내장 객체인 process 객체로 환경 변수를 관리했어서 NestJS의 ConfigModule을 접했을 땐 왜 process.env를 두고 ConfigModule을 사용하는 건지 아리송했다.

 

왜 굳이..? 하지만 굳이 ConfigModule이란 걸 만든 데에는 이유가 있지 않을까 싶어서 개인 블로그 만들기 프로젝트를 하며 이것저것 시도해 보았다.

 

Configuration namespaces(app, db, secret 등으로 세분화해서 관리)까지 적용했다가 

현재는 후퇴한 상태다. 

 

class에 주입해서 사용하는 거 말고 일반 함수에서는 어떻게 해야 좋을까 고민하다가 일단 process.env로 접근하는 걸로 수정했다. redis 연결 부분을 함수로 export해서 main.ts에서 실행하게 해놨는데 redis module을 만들어서 app module에 import 하는 걸로 리팩토링 해야할 것 같다.

 

여러 삽질을 하다보니 ConfigModule은 미래의 나를 구원해줄 존재가 될 것 같은 느낌이 들었다. 물론 단순한 소규모 프로젝트 정도라면 process.env로 충분하겠지만 말이다.

 

유효성 검증, 타입 변환, 기본값 설정, Configuration namespaces 기능 등은 처음 접하면 복잡하고 설정하기 귀찮다고 느낄 수 있지만 해보니 안 쓸 수가 없다. 환경변수 이름 찾으려고 매번 .env 파일 확인하고 오타나서 에러나고 type 안 맞아서 에러나고... 하는 것들은 이제 없을 것이다.

 

 

 

ConfigModule 사용법을 코드로 알아보자

1. 아래 예시처럼 유효성 검증(class-validator 사용)과 타입 변환(class-transformer 사용), 기본값 설정을 함께 할 수 있다.

(단, AppModule에 ConfigModule import 시 load 옵션 설정해 줘야함)

하지만 프로젝트엔 적용하지 않았다. Configuration namespaces 기능을 사용해보았는데 예시는 이 다음에.

// src/config/environment.config.ts

import { IsNumber, IsString, IsOptional } from 'class-validator';
import { Transform } from 'class-transformer';

export class EnvironmentConfig {
  @IsOptional()
  @IsString()
  readonly API_URL: string = 'https://api.example.com'; // 기본값으로 https://api.example.com

  @IsNumber()
  @Transform(({ value }) => parseInt(value, 10), { toClassOnly: true }) // 숫자로 변환
  readonly PORT: number = 3000;	// 기본값으로 3000
}
// src/app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { EnvironmentConfig } from 'src/config/environment.config';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [() => ({ environment: new EnvironmentConfig() })],
      expandVariables: true,
    }),
  ],
})
export class AppModule {}

 

[참고 - 현재 글과 전혀 상관없지만..]

parseInt에 두 번째 인자로 10을 주지 않아도 default로 10이 들어가지만, "0x10"과 같은 문자열은 16진수로 해석되어 10진법으로 16이 반환된다.

 

 

 

2. 이번 개인 프로젝트에서는 Configuration namespaces를 사용하여 아래처럼 적용하였다. (유효성 검증은 joi 라이브러리 사용)

// src/config/env/app.env.ts

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

export default registerAs('App', () => ({
  mode: process.env.NODE_ENV,
  port: parseInt(process.env.PORT),
}));
// src/config/env/auth.env.ts

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

export default registerAs('Auth', () => ({
  salt: parseInt(process.env.SALT),
  sessionSecret: process.env.SESSION_SECRET,
}));
// src/config/env/database.env.ts

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

export default registerAs('Database', () => ({
  pgHost: process.env.DB_PG_HOST,
  pgPort: parseInt(process.env.DB_PG_PORT),
  pgUsername: process.env.DB_PG_USERNAME,
  pgPassword: process.env.DB_PG_PASSWORD,
  pgDatabase: process.env.DB_PG_DATABASE,
  redisHost: process.env.DB_REDIS_HOST,
  redisPort: parseInt(process.env.DB_REDIS_PORT),
}));
// src/config/env/validation-schema.ts

import * as Joi from 'joi';

export const validationSchema = Joi.object({
  NODE_ENV: Joi.string().required(),
  PORT: Joi.number().required(),
  SALT: Joi.number().required(),
  SESSION_SECRET: Joi.string().required(),
  DB_PG_USERNAME: Joi.string().required(),
  DB_PG_PASSWORD: Joi.string().required(),
  DB_PG_HOST: Joi.string().required(),
  DB_PG_PORT: Joi.number().required(),
  DB_PG_DATABASE: Joi.string().required(),
  DB_REDIS_HOST: Joi.string().required(),
  DB_REDIS_PORT: Joi.number().required(),
});
// src/app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import appConfig from './config/env/app.env';
import authConfig from './config/env/auth.env';
import databaseConfig from './config/env/database.env';
import { validationSchema } from './config/env/validation-schema';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [appConfig, databaseConfig, authConfig],
      isGlobal: true,
      validationOptions: validationSchema,
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

 

 

 

 

 

참고) Configuration namespaces를 적용하긴 좀 과하다 싶으면 환경변수 key를 아래처럼 따로 관리하는 것도 나쁘지 않을 것 같다.

여러군데서 사용되는 환경변수의 경우 변수명을 바꾸면 하나하나 찾아서 수정해줘야 하는 불편함 없이 하나의 파일에서 관리할 수 있으니 좀 편하지 않을까

// src/const/env-keys.const.ts

export const EnvKeys = {
  ENV_NODE_ENV: 'NODE_ENV',
  ENV_PORT: 'PORT',
  ENV_SALT: 'SALT',
  ENV_SESSION_SECRET: 'SESSION_SECRET',
  ENV_DB_PG_USERNAME: 'DB_PG_USERNAME',
  ENV_DB_PG_PASSWORD: 'DB_PG_PASSWORD',
  ENV_DB_PG_HOST: 'DB_PG_HOST',
  ENV_DB_PG_PORT: 'DB_PG_PORT',
  ENV_DB_PG_DATABASE: 'DB_PG_DATABASE',
  ENV_DB_REDIS_HOST: 'DB_REDIS_HOST',
  ENV_DB_REDIS_PORT: 'DB_REDIS_PORT',
  ENV_DB_REDIS_DOCKER_HOST: 'DB_REDIS_DOCKER_HOST',
  ENV_DB_REDIS_DOCKER_PORT: 'DB_REDIS_DOCKER_PORT',
  ENV_SWAGGER_API_USER: 'SWAGGER_API_USER',
  ENV_SWAGGER_API_PASSWORD: 'SWAGGER_API_PASSWORD',
};
// src/config/typeorm.config.ts

import { Injectable } from '@nestjs/common';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
import { UserEntity } from 'src/entities/users.entity';
import { PostEntity } from 'src/entities/posts.entity';
import { ConfigService } from '@nestjs/config';
import { EnvKeys } from 'src/common/const/env-keys.const';

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  constructor(private readonly configService: ConfigService) {}
  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'postgres',
      host: this.configService.get<string>(EnvKeys.ENV_DB_PG_HOST),
      port: this.configService.get<number>(EnvKeys.ENV_DB_PG_PORT),
      username: this.configService.get<string>(EnvKeys.ENV_DB_PG_USERNAME),
      password: this.configService.get<string>(EnvKeys.ENV_DB_PG_PASSWORD),
      database: this.configService.get<string>(EnvKeys.ENV_DB_PG_DATABASE),
      entities: [UserEntity, PostEntity],
      synchronize: true,
      logging: true,
    };
  }
}