어플리케이션은 다양한 환경에서 동작한다. 환경에 따라 다른 설정을 사용해야 한다.
외부에서 정의된 환경 변수는 process.env
를 이용하여 확인할 수 있다. 각 환경에 따라 환경 변수를 다르게 적용하여 이 문제를 해결하려 하지만, 값이 쉽게 변경되는 개발/테스트 환경에서는 다루기 어려워질 수 있다.
Node.js 어플리케이션에서는 보통 .env
파일을 사용 -> .env
파일을 교체함으로써 다양한 환경에서 실행 가능
Nest에서 이 기술을 사용하는 좋은 방법: ConfigModule
사용(적절한 .env
파일을 로드하는 ConfigService
를 제공)
$ npm i --save @nestjs/config
@nestjs/config
패키지는 내부적으로 dotenv 사용
@nestjs/config
는 타입스크립트 4.1 이상 요구
보통, 루트 모듈에 임포트하고 .forRoot()
스태틱 메서드로 동작을 제어한다. 이 단계에서는 환경 변수 키/값 쌍을 분석하고 리졸브한다. 이후에 다른 모듈에서 ConfigService
에 접근하기 위한 여러 옵션을 살펴본다.
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
위의 코드는
.env
파일을 분석/로드.env
의 키/값 쌍을 process.env
에 할당된 환경 변수와 머지ConfigService
로 접근할 수 있는 곳에 결과 저장forRoot()
는 ConfigService
프로바이더를 등록한다.
@nestjs/config
가 dotenv에 의존하므로 해당 패키지의 규칙을 사용하여 환경 변수 이름의 충돌을 해결한다. -> 키가 환경 변수와 .env
파일 모두에 있으면 런타임 환경 변수가 우선한다.
// .env sample
DATABASE_USER=test
DATABASE_PASSWORD=test
기본적으로 패키지는 루트 디렉토리에서 .env
파일을 찾는다. 다른 경로를 설정하려면 envFilePath
속성을 설정하면 된다.
ConfigModule.forRoot({
envFilePath: '.development.env',
});
// 여러 경로 설정 가능
ConfigModule.forRoot({
envFilePath: ['.development.env', '.env.development],
});
여러 파일에서 같은 키가 존재하는 경우 처음 발견된 값으로 설정된다.
다른 모듈에서 ConfigModule
을 사용하려면
ConfigModule
을 글로벌 모듈로 선언: isGlobal
속성 설정 -> 다른 모듈에서 ConfigModule
임포트 불필요ConfigModule.forRoot({
isGlobal: true,
});
관련 환경 변수들을 묶어서 관리할 수 있다.
커스텀 설정 파일은 설정 객체를 리턴하는 팩토리 함수를 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 {}
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
}
복잡한 설정 객체 계층을 관리하기 위해 네임스페이스를 사용할 수 있다.(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>,
) {}
ConfigModule.forRoot({
cache: true,
});
모든 설정 파일을 루트 모듈에 등록하는 대신 기능별 설정 파일을 다양한 모듈에서 부분적으로 설정할 수 있다. -> forFeature()
사용
import databaseConfig from './config/database.config';
@Module({
imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}
어플리케이션을 시작할 때 필요한 환경 변수가 제공되지 않거나 검증을 통과하지 못하는 경우 예외를 던져보자.
@nestjs/config
패키지는 두 가지 방법을 제공한다.
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 {}
@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
}
}
}
모듈 구성이 환경 변수에 따라 달라지며 이런 변수가 .env
에서 로드된 경우, ConfigModule.envVariablesLoaded
훅을 이용하여 process.env
개체와 상호작용하기 전에 파일이 로드되었는지 확인할 수 있다.
export async function getStorageModule() {
await ConfigModule.envVariablesLoaded;
return process.env.STORAGE === 'S3' ? S3StorageModule : DefaultStorageModule;
}
정말 편한 기능~ 왜 이제야 알았지
APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}
이걸 사용하려면 forRoot()
의 expandVariables
를 설정해야 한다.
@Module({
imports: [
ConfigModule.forRoot({
// ...
expandVariables: true,
}),
],
})
export class AppModule {}
main.ts
main.ts
파일에서 사용할 수도 있다. 이러면 PORT나 CORS 호스트 같은 변수를 저장할 수 있음
const configService = app.get(ConfigService);
const port = configService.get('PORT');