[NestJS docs] Configuration

nakkim·2022년 10월 19일
0

NestJS docs

목록 보기
10/10
post-custom-banner

어플리케이션은 다양한 환경에서 동작한다. 환경에 따라 다른 설정을 사용해야 한다.
외부에서 정의된 환경 변수는 process.env를 이용하여 확인할 수 있다. 각 환경에 따라 환경 변수를 다르게 적용하여 이 문제를 해결하려 하지만, 값이 쉽게 변경되는 개발/테스트 환경에서는 다루기 어려워질 수 있다.

Node.js 어플리케이션에서는 보통 .env 파일을 사용 -> .env 파일을 교체함으로써 다양한 환경에서 실행 가능

Nest에서 이 기술을 사용하는 좋은 방법: ConfigModule 사용(적절한 .env 파일을 로드하는 ConfigService를 제공)

$ npm i --save @nestjs/config

@nestjs/config 패키지는 내부적으로 dotenv 사용

@nestjs/config는 타입스크립트 4.1 이상 요구

Getting started

보통, 루트 모듈에 임포트하고 .forRoot() 스태틱 메서드로 동작을 제어한다. 이 단계에서는 환경 변수 키/값 쌍을 분석하고 리졸브한다. 이후에 다른 모듈에서 ConfigService에 접근하기 위한 여러 옵션을 살펴본다.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

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

위의 코드는

  1. 디폴트 위치(프로젝트 루트 디렉토리)의 .env 파일을 분석/로드
  2. .env의 키/값 쌍을 process.env에 할당된 환경 변수와 머지
  3. ConfigService로 접근할 수 있는 곳에 결과 저장

forRoot()ConfigService 프로바이더를 등록한다.

@nestjs/config가 dotenv에 의존하므로 해당 패키지의 규칙을 사용하여 환경 변수 이름의 충돌을 해결한다. -> 키가 환경 변수와 .env 파일 모두에 있으면 런타임 환경 변수가 우선한다.

// .env sample
DATABASE_USER=test
DATABASE_PASSWORD=test

Custom env file path

기본적으로 패키지는 루트 디렉토리에서 .env 파일을 찾는다. 다른 경로를 설정하려면 envFilePath 속성을 설정하면 된다.

ConfigModule.forRoot({
  envFilePath: '.development.env',
});

// 여러 경로 설정 가능
ConfigModule.forRoot({
  envFilePath: ['.development.env', '.env.development],
});

여러 파일에서 같은 키가 존재하는 경우 처음 발견된 값으로 설정된다.

Use module globally

다른 모듈에서 ConfigModule을 사용하려면

  • 해당 모듈에서 import
  • ConfigModule을 글로벌 모듈로 선언: isGlobal 속성 설정 -> 다른 모듈에서 ConfigModule 임포트 불필요
ConfigModule.forRoot({
  isGlobal: true,
});

Custom configuration files

관련 환경 변수들을 묶어서 관리할 수 있다.
커스텀 설정 파일은 설정 객체를 리턴하는 팩토리 함수를 export.

export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432
  }
});

ConfigModule.forRoot()load 속성을 이용해서 위 파일을 로드할 수 있다.

import configuration from './config/configuration';

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

Using the ConfigService

환경 변수에 접근하려면 ConfigService를 주입해야 한다. ConfigModule.forRoot()에서 isGlobal속성을 설정하지 않는 한, 사용할 모듈에 ConfigModule을 임포트해야 한다.

constructor(private configService: ConfigService) {}

// get an environment variable
const dbUser = this.configService.get<string>('DATABASE_USER');

// get a custom configuration value
const dbHost = this.configService.get<string>('database.host');

+ 인터페이스를 타입 힌트로 사용해서 설정 객체를 가져올 수도 있음

interface DatabaseConfig {
  host: string;
  port: number;
}

const dbConfig = this.configService.get<DatabaseConfig>('database');

const port = dbConfig.port;

get() 메서드는 두 번째 매개변수로 디폴트 값을 넘겨줄 수 있다.

// database.host가 없을 경우 localhost로 설정
const dbHost = this.configService.get<string>('database.host', 'localhost');

ConfigService는 두 옵셔널 제네릭을 가진다. 첫 번째는 존재하지 않는 프로퍼티에 접근하는 것을 막는다.

interface EnvironmentVariables {
  PORT: number;
  TIMEOUT: string;
}

// somewhere in the code
constructor(private configService: ConfigService<EnvironmentVariables>) {
  const port = this.configService.get('PORT', { infer: true });

  // TypeScript Error: this is invalid as the URL property is not defined in EnvironmentVariables
  const url = this.configService.get('URL', { infer: true });
}

infer를 설정할 경우 인터페이스에 기반하여 타입을 자동 추론한다.
+ 중첩된 커스텀 설정 객체의 프로퍼티의 타입도 추론 가능

constructor(private configService: ConfigService<{ database: { host: string } }>) {
  const dbHost = this.configService.get('database.host', { infer: true })!;
  // typeof dbHost === "string"                                          |
  //                                                                     +--> non-null assertion operator
}

두 번째 제네릭은 첫 번째에 의존한다. strictNullChecks가 켜져 있을 때 ConfigService 메서드가 반환할 수 있는 모든 undefined 타입을 제거한다.

// ...
constructor(private configService: ConfigService<{ PORT: number }, true>) {
  //                                                               ^^^^
  const port = this.configService.get('PORT', { infer: true });
  //    ^^^ The type of port will be 'number' thus you don't need TS type assertions anymore
}

Configuration namespaces

복잡한 설정 객체 계층을 관리하기 위해 네임스페이스를 사용할 수 있다.(forRoot()load 이용하여 등록)

export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432
  }
});

// namespaced configuration object
export deafult registerAs('database', () => ({
  host: process.env.DATABASE_HOST,
  port: process.env.DATABASE_PORT || 5432
}));

// other file
const dbHost = this.configService.get<string>('database.host');

다른 대안은 database 네임스페이스를 직접 인젝트하는 것이다. This allows us to benefit from strong typing:

constructor(
  @Inject(databaseConfig.KEY)
  private dbConfig: ConfigType<typeof databaseConfig>,
) {}

Cache environment variables

ConfigModule.forRoot({
  cache: true,
});

Partial registration

모든 설정 파일을 루트 모듈에 등록하는 대신 기능별 설정 파일을 다양한 모듈에서 부분적으로 설정할 수 있다. -> forFeature() 사용

import databaseConfig from './config/database.config';

@Module({
  imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}

Schema validation

어플리케이션을 시작할 때 필요한 환경 변수가 제공되지 않거나 검증을 통과하지 못하는 경우 예외를 던져보자.
@nestjs/config 패키지는 두 가지 방법을 제공한다.

  • Joi 빌트인 밸리데이터: 객체 스키마 정의/검증 가능
  • 커스텀 validate() 함수 사용

$ npm i --save joi

validationSchema 프로퍼티를 통해 검증 스키마를 정의할 수 있다.

import * as Joi from 'joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test', 'provision')
          .default('development'),
        PORT: Joi.number().default(3000),
      }),
    }),
  ],
})
export class AppModule {}

기본적으로 모든 스키마 키는 옵셔널이다.

  • default(): 기본값 설정
  • required(): 값이 정의되어야 한다고 요구
  • 참고

알 수 없는 환경 변수(스키마에 키가 없는 변수)는 허용되며 유효성 검사 예외를 트리거하지 않는다. 기본적으로 모든 유효성 검사 에러는 리포트됨 -> validationOptions를 설정해서 행동을 변경할 수 있다.

import * as Joi from 'joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test', 'provision')
          .default('development'),
        PORT: Joi.number().default(3000),
      }),
      validationOptions: {
        allowUnknown: false, // unknown 키를 제어할 건지
        abortEarly: true, // true -> 첫 에러에서 검증 정지
      },
    }),
  ],
})
export class AppModule {}

Custom getter functions

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

  get isAuthEnabled(): boolean {
    return this.configService.get('AUTH_ENABLED') === 'true';
  }
}
@Injectable()
export class AppService {
  constructor(apiConfigService: ApiConfigService) {
    if (apiConfigService.isAuthEnabled) {
      // Authentication is enabled
    }
  }
}

Environment variables loaded hook

모듈 구성이 환경 변수에 따라 달라지며 이런 변수가 .env에서 로드된 경우, ConfigModule.envVariablesLoaded 훅을 이용하여 process.env 개체와 상호작용하기 전에 파일이 로드되었는지 확인할 수 있다.

export async function getStorageModule() {
  await ConfigModule.envVariablesLoaded;
  return process.env.STORAGE === 'S3' ? S3StorageModule : DefaultStorageModule;
}

Expandable variables

정말 편한 기능~ 왜 이제야 알았지

APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}

이걸 사용하려면 forRoot()expandVariables를 설정해야 한다.

@Module({
  imports: [
    ConfigModule.forRoot({
      // ...
      expandVariables: true,
    }),
  ],
})
export class AppModule {}

Using in the main.ts

main.ts 파일에서 사용할 수도 있다. 이러면 PORT나 CORS 호스트 같은 변수를 저장할 수 있음

const configService = app.get(ConfigService);

const port = configService.get('PORT');
profile
nakkim.hashnode.dev로 이사합니다
post-custom-banner

0개의 댓글