본문 바로가기

Backend/NestJS

[NestJS] Nest Config 환경 변수 관리

728x90
dotenv가 아닌 NestJS에서 제공하는
Nest/config로 환경 변수를 관리하는 방법을 공부한 기록이다.

개요

응용 프로그램은 종종 다른 환경에서 실행되기 때문에

환경 변수를 해당 환경에 따라 다르게 사용해야 한다. 

 

이전 기록에서 dotenv 라이브러리를 이용해

환경변수를 관리하는 법을 다뤘었다.

 

[Dotenv] dotenv를 이용한 환경 변수 구성

dotenv 라이브러리를 이용한 환경 변수 구성에 대한 공부 기록이다. 개요 일반적으로 서비스를 개발할 때 로컬 또는 개발 환경에서 개발하고, 개발한 코드를 스테이지 서버(테스트 환경)에서 테스

choi-records.tistory.com

 

환경 변수는 보통 env 파일에서 사용되는데

환경에 맞는 env파일을 로드하는 Config Module을 nest에서 @nestjs/config 패키지를 통해 제공하고 있다.

@nestjs/config는 내부적으로 dotenv를 사용한다.

설치

$npm i -D @nestjs/config
$yarn add -D @nestjs/config

ConfigModule

기본 사용법

패키지를 설치하면 ConfigModule을 가져올 수 있다.

forRoot 메서드를 이용해 App 모듈에서 동적 모듈로 가져와보자.

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath:
        process.env.NODE_ENV === 'production'
          ? '.production.env'
          : process.env.NODE_ENV === 'stage'
          ? '.stage.env'
          : '.development.env',
    }),
    EmailModule,
    PostsModule,
  ],
  controllers: [AppController, UsersController],
  providers: [AppService, UsersService],
})
export class AppModule {}

먼저 NODE_ENV에 따라 다른 env파일에서 값을 가져오는 작업을 해보자.

NODE_ENV는 package.json에서 scripts 값을 바꿔줌으로써 변경할 수 있다.

//package.json
"scripts": {
    ...
    "start:dev": "cross-env NODE_ENV=production nest start --watch",
    ...
    }

cross-env 라이브러리를 사용하면,

NODE_ENV 변수값을 쉽게 바꿀 수 있다.

 

NODE_ENV를 development로 바꿔주고 테스트해 보자.

//.development.env
TEST_VALUE=development

.development.env 파일에 

위와 같이 테스트를 위한 변수를 선언했다.

 

이제 AppController에서 위에서 동적으로 import 한 ConfigModule의 

ConfigService 프로바이더를 주입하고, TEST_VALUE 값을 반환해 보자.

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

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

  @Get()
  getENV(): string {
    return this.configService.get('TEST_VALUE');
  }
}

정상적으로 반환된 것을 볼 수 있다.

 

작은 프로젝트라면 위와 같은 방법으로 분리해도 상관없지만

프로젝트의 규모가 커지고, 모듈이 많아진다면 환경 변수 또한 관심사를 분리해 묶어서 관리해야 한다.

 

커스텀 Config 파일

이전 기록에서 만들었던 EmailService 프로바이더가 있다.

해당 프로바이더는 내 이메일 계정, 비밀번호를 사용해서 메일을 보내주는 라이브러리를 사용한다.

때문에 개인 정보를 환경 변수로 보호하고자 한다.

 

[NestJS]User API 만들기(2) (Provider 프로바이더 회원가입 구현)

Provider에 대한 공부 기록이다. 이전 기록에서 Provider(서비스 파일)에 대해 간단하게 다뤄봤다. [NestJS Overview] Service.ts(서비스 파일, 반환 클래스, 예외처리) NestJS 서비스 파일에 대한 기록이다. 개

choi-records.tistory.com

이를 커스텀 Config 파일을 통해 email이라는 관심사로 분리해 보자.

 

먼저 폴더 구조를 보자.

src 하위 config 디렉터리에

대략적인 동작 원리를 알아보자.

 

먼저 emailConfig 파일에서 registerAs 메서드를 이용해

TFactory와 ConfigFactoryKeyHost를 합친 타입의 함수를 반환한다.

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

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,
}));

registerAs의 정의는 아래와 같다.

import { ConfigModule } from '..';
import { ConfigFactory } from '../interfaces';
import { ConfigObject } from '../types';
export interface ConfigFactoryKeyHost<T = unknown> {
    KEY: string;
    asProvider(): {
        imports: [ReturnType<typeof ConfigModule.forFeature>];
        useFactory: (config: T) => T;
        inject: [string];
    };
}
/**
 * Registers the configuration object behind a specified token.
 */
export declare function registerAs<TConfig extends ConfigObject, TFactory extends ConfigFactory = ConfigFactory<TConfig>>(token: string, configFactory: TFactory): TFactory & ConfigFactoryKeyHost<ReturnType<TFactory>>;

쉽게 말하면 'email'이라는 토큰으로 ConfigFactory를 등록하는 함수이다.

 

ConfigFactory를 등록하는 이유는

App 모듈에서 동적으로 ConfigModule을 정할 때,

커스텀 Config 파일을 등록하는 속성인 load에서 ConfigFactory 배열을 값으로 받기 때문이다.

   load?: Array<ConfigFactory>;

 

이제 app 모듈을 수정해 보자.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import emailConfig from './config/emailConfig';
import { validationSchema } from './config/validationSchema';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: [`${__dirname}/config/env/.${process.env.NODE_ENV}.env`],
      load: [emailConfig],
      validationSchema,
    }),
    UsersModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

ConfigFactory의 변수명은 중요하지 않다.

어느 Config 파일에서 가져왔는지가 중요하다.(어떤 토큰으로 어떤 ConfigFactory가 반환됐는지)

 

validationSchema는 joi 라이브러리를 이용해

환경 변수의 값에 대한 유효성 검사를 하는 역할을 한다.

//validationSchema.ts
import * as Joi from 'joi';

export const validationSchema = Joi.object({
  EMAIL_SERVICE: Joi.string().required(),
  EMAIL_AUTH_USER: Joi.string().required(),
  EMAIL_AUTH_PASSWORD: Joi.string().required(),
  EMAIL_BASE_URL: Joi.string().required().uri(),
});

만약 required() 메서드를 이용한 환경 변수가

envFilePath로 지정된 경로에 없다면 오류가 발생한다.

 

이제 Global로 설정한 환경 변수를 사용해 보자.

//email.service.ts
import { Inject, Injectable } from '@nestjs/common';
import Mail = require('nodeMailer/lib/mailer');
import * as nodemailer from 'nodemailer';
import emailConfig from 'src/config/emailConfig';
import { ConfigType } from '@nestjs/config';

interface EmailOptions {
  to: string;
  subject: string;
  html: string;
}

@Injectable()
export class EmailService {
  private transporter: Mail;

  constructor(
    @Inject(emailConfig.KEY) private config: ConfigType<typeof emailConfig>,
  ) {
    this.transporter = nodemailer.createTransport({
      service: config.service,
      auth: {
        user: config.auth.user,
        pass: config.auth.pass,
      },
    });
  }
  async sendMemberJoinVerification(
    emailAddress: string,
    signupVerifyToken: string,
  ) {
    const baseUrl = this.config.baseUrl;
    const url = `${baseUrl}/users/email-verify?signupVerifyToken=${signupVerifyToken}`;
    const mailOptions: EmailOptions = {
      to: emailAddress,
      subject: '가입 인증 메일',
      html: `
            가입확인 버튼을 누르시면 가입 인증이 완료됩니다.<br/>
            <form action="${url}" method="POST">
                <button>가입확인</button>
            </form>
            `,
    };
    return await this.transporter.sendMail(mailOptions);
  }
}

emailConfig는 Inject 데커레이터를 이용해 주입된다.

 

이메일을 보내는 로직을 생략하고

단순히 환경 변수를 가져오는 부분만 빼 보자면

constructor(
    @Inject(emailConfig.KEY) private config: ConfigType<typeof emailConfig>,
  ) {
    this.transporter = nodemailer.createTransport({
      service: config.service,
      auth: {
        user: config.auth.user,
        pass: config.auth.pass,
      },
    });
  }

위와 같다.

emailConfig의 Key 즉 위에서 지정했던 'email' 토큰을 이용해 emailConfig를 주입하고,

주입한 config 멤버 변수를 통해 registerAs의 콜백함수로 전달했던 객체에 접근한 것이다.


참고

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

 

06장 동적 모듈을 활용한 환경변수 구성

.

wikidocs.net

마치며

이번 환경 변수를 공부하면서 내가 많이 부족하다는 것을 느꼈다.

NODE_ENV 관련 내부 동작 과정과 파일의 경로 지정, NestJS의 Factory Pattern에도 미숙한 것 같다.

 

공식문서 Fundamentals 부분부터 확실하게 정독하고,

다른 기본 지식에 대한 공부를 병행해야겠다.

 

잘못된 정보에 대한 피드백은 환영입니다.

감사합니다.

728x90