“ 당신은 소프트웨어 품질을 추구할 수도 있고, 포인터 연산을 할 수도 있다. 그러나 두 개를 동시에 할 수는 없다. ”
지난 포스팅에서는 NestJS 프로젝트의 스캐폴딩을 통해 폴더 구조를 설정하고,
Module - Controller - Service 구조를 구현했습니다.
TypeORM을 사용하여 데이터베이스 설정을 진행하고, 데이터베이스 테이블을 생성하는 과정도 함께 살펴보았습니다.
이번에는 더 유연한 구조를 위해 환경 변수 설정을 통해 환경 변수를 관리하는 방법에 대해 알아보겠습니다.
아래는 이전 포스팅 과정입니다. 참고 부탁드립니다.
NestJS와 TypeORM을 활용한 데이터베이스 연동
느린 개발자
stack94.tistory.com
모듈 설치
아래 명령어를 실행하여 "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 패턴으로 구성해보는것을 공부해보겠습니다.