본문 바로가기

Backend/NestJS

NestJS - 대충 서비스 만들어보기 (7)

이번 포스팅에선 config 를 구성해보도록 하겠습니다.

본래는 프로젝트를 하면 이런것부터 해야하지만 당장 처음엔 딱히 config 로 넣을만한것도 없어서 미루다가,

대충 만들면서 하드코딩으로 들어가있는 slack 관련된 설정값들을 config 로 옮기려 합니다.

 

- dotenv

 

config 를 구성할 때 dotenv 라이브러리를 사용하려고 합니다.

dotenv 는 .env 파일을 읽어서 환경 변수로 등록해주는 역할을 하고, 그럼 우리는 환경 변수로부터 필요한 세팅 값들을

사용할 수 있습니다.

팀에서 관리하는 레포들에 dotenv 를 적용해야지 생각만하고 아직 못했었는데 이런때에라도 좀 써보게 되었네요.

 

> npm install dotenv

 

먼저 dotenv 라이브러리를 설치합니다.

 

 

그리고 당장 .production.env 파일은 쓸 일이 없을 것 같긴 하지만 src 하위에 config/env 디렉토리를 생성하고

.development.env 와 .production.env 파일을 생성합니다.

 

SLACK_TOKEN=...;
SLACK_CHANNEL=...;

 

그리고 우선 .development.env 파일에만 위와 같이 환경 변수로 등록할 값을 추가하겠습니다.

지금은 slack 관련된 내용만 넣고있으나 추후엔 DB, redis 주소 및 접근 정보등이 들어갈 것 같습니다.

 

...
import { config } from 'dotenv';
import * as path from 'path';

config({
  path: path.resolve(__dirname, 'config/env/.development.env'),
});

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();

 

좀 더 우아하게 해야겠지만 일단 단순히 main.ts 에서 dotenv 의 config 함수를 호출하는 코드를 추가했습니다.

저렇게 .env 파일 위치를 지정해 config 로 호출하면 dotenv 가 파일을 읽어 등록해줍니다.

 

import { Injectable } from '@nestjs/common';
import axios from 'axios';

@Injectable()
export class SlackService {
  private token: string;
  private channel: string;

  constructor() {
    this.token = '...';
    this.channel = '...';
    console.info(process.env.SLACK_TOKEN, process.env.SLACK_CHANNEL);
  }
  ...
}

 

원래 하드코딩되어 있던 부분을 바로 바꾸기 전에 환경 변수에 제대로 세팅이 되었는지를 한번 체크해봤습니다.

그런데 이 상태로는 둘 다 undefined 로 출력이 됩니다.

 

 

위 구조는 npm run start:dev 실행시 생성되는 dist 디렉토리인데 여기에 생성했던 config 디렉토리가 보이지 않습니다.

이유는 Nest 가 빌드를 할 때 default 동작이 ts 파일 외의 asset 은 제외하도록 되어있기 때문입니다.

 

{
  ...,
  "compilerOptions": {
    ...,
    "assets": [
      {
        "include": "./config/env/*.env",
        "outDir": "./dist/src"
      }
    ]
  }
}

 

nest-cli.json 파일에서 compilerOptions 를 위와 같이 수정해줍니다.

내용이 너무 명확해서 딱히 설명할것도 없긴 하지만.. 컴파일할 때 .env 파일도 포함시키도록 하면서 해당 결과물은 dist/src 에 위치하도록

설정한 내용입니다. dotenv 에서 path 의 경로를 보면 outDir 의 위치가 이해되실거라 생각합니다.

이렇게 수정하고 다시 확인하니 이제 process.env 에서 SLACK_TOKEN 과 SLACK_CHANNEL 값이 세팅된게 확인되었습니다.

 

...
@Injectable()
export class SlackService {
  private token: string;
  private channel: string;

  constructor() {
    this.token = process.env.SLACK_TOKEN;
    this.channel = process.env.SLACK_CHANNEL;
  }
  ...
}

 

이제 SlackService 에서 token 과 channel 은 위와 같이 수정해주면 됩니다.

 

- @nestjs/config

 

이젠 위에서 main.ts 에 붙여놨던 dotenv 의 설정을 별도의 config 모듈로 만들어보겠습니다.

저대로 두면 보기도 안좋고, 모듈화를 해서 config 가 필요한 각 컴퍼넌트가 ConfigModule 또는 ConfigService 를 주입받아

사용하면 깔끔해질 것 같습니다.

 

먼저 main.ts 에 추가했었던 dotenv 관련된 내용은 제거하고, dotenv 라이브러리도 제거합니다.

 

> npm install @nestjs/config

 

그리고 @nestjs/config 라이브러리를 추가해줍니다.

위에서 dotenv 를 제거한 이유는, @nestjs/config 가 자체적으로 dotenv 를 내장해 활용하기 때문입니다.

라이브러리 페이지에서도 dotenv 를 베이스로 한다고 나와있습니다.

 

import { Module } from '@nestjs/common';
import { AdsModule } from './ads/ads.module';
import { SlackModule } from './slack/slack.module';
import { ConfigModule } from '@nestjs/config';
import * as path from 'path';

@Module({
  imports: [
    AdsModule,
    SlackModule,
    ConfigModule.forRoot({
      envFilePath: path.resolve(
        __dirname,
        process.env.NODE_ENV === 'production'
          ? 'config/env/.production.env'
          : 'config/env/.development.env',
      ),
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

 

메인인 app.module.ts 의 imports 부분에 위와 같이 @nestjs/config 에서 제공하는 ConfigModule 을 동적 모듈로 추가합니다.

동적 모듈로 가져오기 위해선 forRoot 메서드를 호출해야하는데, 이 부분은 조금 이따 다시 보겠습니다.

envFilePath 내용은 이전에 main.ts 에서 dotenv 를 사용했을 때와 거의 비슷한 코드입니다.

 

import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SlackService } from './slack.service';

@Module({
  providers: [SlackService, ConfigService],
  exports: [SlackService],
})
export class SlackModule {}

 

추가한 ConfigModule 은 프로바이더인 ConfigService 가 따로 있습니다. 이 ConfigService 를 사용해 환경 변수 값을 가져와야 하니

SlackService 에서 사용할 수 있도록 SlackModule 의 프로바이더에 ConfigService 를 추가해줍니다.

 

import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class SlackService {
  private token: string;
  private channel: string;

  constructor(private readonly configService: ConfigService) {
    this.token = this.configService.get('SLACK_TOKEN');
    this.channel = this.configService.get('SLACK_CHANNEL');
  }
  ...
}

 

이제 SlackService 에서 ConfigService 를 주입받아 위와 같이 사용하면, 정상 동작하는 것을 볼 수 있습니다.

 

- 동적 모듈

 

위에서 사용했던 동적 모듈에 대해 짧게 보자면 이전까지는 전부 정적 모듈을 사용해 서버를 구성하고 있었습니다.

동적이라는 이름 그대로 동적 모듈은 모듈이 생성될 때 동적으로 변수들이 정해집니다.

여기에 대표적인 모듈이 위에서 작성한 Config 입니다. 이미 위에서 한 것처럼 Config 의 경우엔 서버의 운영 환경인 production,

development, staging 등에 따라 변수들이 달라지게 됩니다. 이런 경우에 우리는 동적 모듈로 구성할 수 있습니다.

 

...

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

 

지금까지 사용했던 정적 모듈과는 다르게 ConfigModule 을 동적 모듈로 구성할 땐 forRoot 메서드를 호출했습니다.

forRoot 는 동적 모듈을 리턴하는 static method 입니다.

지금은 nest 에서 제공하는 ConfigModule 을 사용했지만, 직접 동적 모듈을 반환하는 static method 를 작성할 때도

관례상 forRoot 나 register 네이밍을 사용합니다. 아마 작업하다보면 종종 이런 메서드를 보게 될겁니다.

여기서 비동기가 붙게되면 forRootAsync 나 registerAsync 를 사용합니다.

 

ConfigModule 외에 이런 메서드를 제공하는건 nest 에서 배치잡을 작성할 때 사용하는 ScheduleModule 이나 메시징을 처리할 때

사용할 수 있는 BullModule 이 forRoot 메서드를 지원했습니다.

 

ConfigModule 의 forRoot 메서드는 ConfigModuleOptions 타입의 파라미터를 받는데,

이 옵션을 받아 동적으로 ConfigModule 을 생성합니다.

 

env 관련 코드를 좀 더 정리할 필요가 있는데 추가 내용은 다음 포스팅에서 담도록 하겠습니다.