“ 당신은 소프트웨어 품질을 추구할 수도 있고, 포인터 연산을 할 수도 있다. 그러나 두 개를 동시에 할 수는 없다. ”
지난 포스팅에서는 NestJS 프로젝트의 스캐폴딩을 통해 폴더 구조를 설정하고,
Module - Controller - Service 구조를 구현했습니다.
TypeORM을 사용하여 데이터베이스 설정을 진행하고, 데이터베이스 테이블을 생성하는 과정도 함께 살펴보았습니다.
이번에는 더 유연한 구조를 위해 환경 변수 설정을 통해 환경 변수를 관리하는 방법에 대해 알아보겠습니다.
아래는 이전 포스팅 과정입니다. 참고 부탁드립니다.
모듈 설치
아래 명령어를 실행하여 "nestjs/config" 모듈을 설치합니다.
npm install --save @nestjs/config
NODE_ENV 환경 변수를 설정하기 위해 운영체제(OS)에 상관없이 사용가능한 "cross-env" 모듈 또한 설치합니다.
npm install -D cross-env
Env 파일 생성
저는 dev와 prod 구분을 하여 관리하기 위함으로 프로젝트 src폴더에 env폴더를 생성하고
. env.development ,. env.production 파일을 생성합니다.
# App Config
APP_PORT=5000
APP_NAME=app
FRONT_DOMAIN=localhost:3000
BACK_DOMAIN=localhost:5000
API_PREFIX=api
LANGUAGE=ko
환경변수 유효성 체크
객체 유효성을 검사하는 라이브러리로 joi 모듈을 많이 사용하는데 class-validator와 class-transformer를 활용하여 객체를 클래스로 변환하고 유효성을 검사하 ValidateConfig 클래스를 생성해 보겠습니다.
- plainToClass : 일반객체를 클래스로 변환하는 역할
- enableImplicitConversion : 옵션을 "true"로 설정하면 암시적 타입 변환을 활성화
- validateSync : 클래스의 유효성을 동기적으로 검사
- skipMissingProperties : 옵션을 false로 설정하여 누락된 속성을 체크
이 커스텀한 클래스의 기능은 유효성 검사 결과로 오류가 있으면 해당 오류를 문자열로 변환하고 Error 객체를 throw 하여 처리합니다. 그리고 모든 유효성 검사를 통과한 변환된 설정 객체를 반환합니다. 그래서 좀 더 일관성과 정확성을 보장할 수 있습니다.
src/utils/validate.config.ts 파일을 생성하여 구현합니다.
import { plainToClass } from 'class-transformer'
import { validateSync } from 'class-validator'
import { ClassConstructor } from 'class-transformer/types/interfaces'
function ValidateConfig<T extends object>(
config: Record<string, unknown>,
envClass: ClassConstructor<T>
) {
const ValidateConfig = plainToClass(envClass, config, {
enableImplicitConversion: true
})
const errors = validateSync(envClass, {
skipMissingProperties: false
})
if (errors.length > 0) {
throw new Error(errors.toString())
}
return ValidateConfig
}
export default ValidateConfig
환경변수 configs 구성
"src/configs" 폴더 안에 애플리케이션 환경, 데이터베이스 환경 및 환경 변수 설정과 관련된 다양한 모듈의 설정 파일을 구성합니다. 아래의 파일들을 "src/configs" 폴더 내에 생성합니다.
1. config.type.ts
먼저 설정할 환경에 대한 타입을 정의해 주겠습니다.
// TODO: src/configs/config.type.ts
export type AppConfig = {
nodeEnv: string // 현재 실행 환경 (development, production, test 등)
appName: string // 애플리케이션 이름
baseDir: string // 애플리케이션의 기본 디렉토리 경로
frontDomain: string // 프론트엔드 도메인 주소
backDomain: string // 백엔드 도메인 주소
port: number // 애플리케이션 포트 번호
apiPrefix: string // API 엔드포인트의 접두사
language: string // 기본 언어 설정
}
// TODO: 타입이 많아질 경우를 생각하여 구성
export type AppConfigType = {
app: AppConfig
}
2. app.config.ts
이제 app.config.ts 파일을 생성하여 애플리케이션에서 사용할 환경변수를 설정하고 유효성 검사를 하여 안정적이고 일관된 관리 구성을 해보겠습니다.
import { registerAs } from '@nestjs/config'
import { AppConfig } from './config.type'
import ValidateConfig from '../utils/validate.config'
import {
IsEnum,
IsInt,
IsOptional,
IsString,
IsUrl,
Max,
Min
} from 'class-validator'
import process from 'process'
// 실행 환경을 나타내는 Enum
enum Environment {
dev = 'development',
prod = 'production',
}
// 환경 변수 유효성을 검사하기 위한 클래스
class EnvironmentValidator {
@IsEnum(Environment)
@IsOptional()
NODE_ENV: Environment
@IsInt()
@Min(0)
@Max(65535)
@IsOptional()
APP_PORT: number
@IsUrl({ require_tld: false })
@IsOptional()
FRONTEND_DOMAIN: string
@IsUrl({ require_tld: false })
@IsOptional()
BACKEND_DOMAIN: string
@IsString()
@IsOptional()
API_PREFIX: string
@IsString()
@IsOptional()
LANGUAGE: string
}
// 설정 파일 등록 및 환경 변수 유효성 검사
export default registerAs<AppConfig>('app', () => {
// 환경 변수 유효성을 검사하고 오류 발생 시 예외 처리
ValidateConfig(process.env, EnvironmentValidator)
// 설정 객체 반환
return {
// NODE_ENV 환경 변수 값 또는 기본값으로 'development' 설정
nodeEnv: process.env.NODE_ENV || 'development',
// APP_NAME 환경 변수 값 또는 기본값으로 'app' 설정
appName: process.env.APP_NAME || 'app',
// PWD 환경 변수 값 또는 현재 작업 디렉토리로 설정
baseDir: process.env.PWD || process.cwd(),
// FRONT_DOMAIN 환경 변수 값 설정
frontDomain: process.env.FRONT_DOMAIN,
// BACK_DOMAIN 환경 변수 값 설정
backDomain: process.env.BACK_DOMAIN,
// APP_PORT 환경 변수 값 또는 PORT 환경 변수 값 또는 기본값으로 5000 설정
port: process.env.APP_PORT
? parseInt(process.env.APP_PORT, 10)
: process.env.PORT
? parseInt(process.env.PORT, 10)
: 5000,
// API_PREFIX 환경 변수 값 또는 기본값으로 'api' 설정
apiPrefix: process.env.API_PREFIX || 'api',
// LANGUAGE 환경 변수 값 또는 기본값으로 'ko' 설정 추후 i18n 설정을 위함
language: process.env.LANGUAGE || 'ko',
}
})
ConfigModule 설정
app.module.ts 파일을 열고 ConfigModule로 설정해 줍니다.
여기서 잠깐 옵션에 대해서 설명하면
- isGlobal : 'true'로 설정하면 이 모듈을 사용하는 어느 모듈에서나 설정을 사용가능하게 전역으로 설정됩니다.
- load : 설정 파일들을 로드하기 위한 설정 객체 또는 프로바이더 배열을 지정합니다.
- envFilePath : 환경 변수 파일의 경로를 지정합니다.
- TypeOrmModule.forRootAsync()
- useClass : TypeORM 설정을 구성
- dataSourceFactory : 데이터베이스 연결을 설정하고 객체를 초기화하기 위함
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import appConfig from './configs/app.config'
import { BoardsModule } from './boards/boards.module'
import { TypeOrmModule } from '@nestjs/typeorm'
import { DataSource, DataSourceOptions } from 'typeorm'
import { TypeormConfig } from './database/typeorm.config'
import process from 'process'
import path from 'path'
@Module({
imports: [
// TODO: 추가
ConfigModule.forRoot({
isGlobal: true,
load: [appConfig],
envFilePath: path.resolve( //TODO: src/env 폴더에 환경에 맞게 경로 설정
__dirname,
'..',
'src',
'env',
`${process.env.NODE_ENV}` === 'production'
? '.env.production'
: '.env.development'
)
}),
TypeOrmModule.forRootAsync({
useClass: TypeormConfig,
dataSourceFactory: async (options: DataSourceOptions) => {
return new DataSource(options).initialize()
}
}),
BoardsModule
]
})
export class AppModule {}
main.ts 구성
이제 main.ts로 와서 port 부분을 환경변수로 설정해 보고 잘 작동하는지 테스트해 보겠습니다.
@nestjs/config에 ConfigService를 사용하여 환경변수를 가져옵니다. 이때 제네릭으로 환경변수 타입을 지정해 줬던 AppConfigType을 설정해 줍니다. 그리고 아래처럼 port와 prefix를 지정합니다.. env.development에 APP_PORT를 5000으로 하고. env.production에 8000으로 해서 확인해 봅니다.
import { NestFactory } from '@nestjs/core'
import { ConfigService } from '@nestjs/config'
import { AppConfigType } from './configs/config.type'
import { AppModule } from './app.module'
import process from 'process'
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true })
// TODO: ConfigService를 통해 설정 값을 가져옴
const configService = app.get(ConfigService<AppConfigType>)
// TODO: 환경변수 API 전역 접두사 설정
// TODO: localhost:5000/api/ 설정
app.setGlobalPrefix(
configService.getOrThrow('app.apiPrefix', { infer: true }),
{
exclude: ['/']
}
)
// TODO: 환경변수 포트 설정
const port = configService.getOrThrow('app.port', { infer: true })
await app.listen(port)
console.log(`listening on port ${port}`)
}
void bootstrap()
실행하기
이제 cross-env를 활용해서 NODE_ENV를 적용하여 실행해 봅니다. package.json을 보겠습니다.
아래처럼 cross-env NODE_ENV=development 또는 production을 적용하여 실행합니다.
npm run start:dev
npm run start:prod
"scripts": {
"build": "nest build",
"start": "nest start",
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",
},
출처) 링크
이번에는 @nestjs/config으로 환경변수를 설정하는 방법에 대해서 포스팅해봤는데요. 설정할게 좀 많아 보이지만
이런 식으로 구성하게 되면 중앙 집중화로 설정값을 하드코딩 하지 않고 편리하게 업데이트할 수 있을 거 같습니다.
다음 포스팅은 데이터베이스 관련하여 환경변수를 설정해보고 Repository 패턴으로 구성해보는것을 공부해보겠습니다.