
@nestjs/config와 함께 Joi(validationSchema) 또는 class-validator(validate 함수) 두 가지로 검증할 수 있습니다.주니어 시절 가장 많이 하는 실수 중 하나가 환경 변수(Environment Variables) 누락입니다. 서버를 띄울 때 필요한 DATABASE_URL이나 JWT_SECRET 같은 필수 값이 .env 파일에 없거나 타입이 잘못돼도, Node.js는 아무 불평 없이 일단 서버를 띄웁니다.
그러다 사용자가 특정 API를 호출해 해당 환경 변수에 접근하는 순간 undefined 에러를 뱉으며 애플리케이션이 죽습니다. 환경 변수 오류는 런타임이 아니라 서버가 켜지는 부트스트랩(Bootstrap) 단계에서 즉시 터지게 만들어야 합니다. 이것이 서버 개발의 철칙이자 Fail Fast(빠르게 실패하기) 전략의 핵심입니다.
NestJS에서는 보통 @nestjs/config 패키지와 함께 두 가지 검증 방식을 사용합니다.
1. Joi 라이브러리
2. class-validator + class-transformer
NestJS 생태계와 가장 잘 어울리는 쪽은 class-validator지만, 레거시 시스템이나 팀 성향에 따라 Joi를 쓰는 곳도 많으므로 두 가지를 모두 살펴보겠습니다.
@nestjs/config 모듈은 내부적으로 .env 파일을 읽어들입니다. 이때 모듈 설정 옵션으로 validationSchema(Joi용) 또는 validate(class-validator 커스텀 함수용)를 넘겨주면, 환경 변수를 서버 메모리에 올리기 전에 유효성 검사를 수행합니다.
이 검사에서 단 하나라도 실패하면, NestJS 애플리케이션은 즉시 부트스트랩을 멈추고 어떤 환경 변수가 누락됐는지 명확한 에러 로그를 남긴 뒤 종료(Exit)됩니다.
먼저 joi 패키지를 설치합니다.
npm install joi
app.module.ts에서 ConfigModule을 초기화할 때 validationSchema를 넘겨줍니다.
// app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Joi를 이용한 검증 스키마 정의 validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('development', 'production', 'test', 'provision') .default('development'), PORT: Joi.number().default(3000), DATABASE_URL: Joi.string().required(), // 필수 값! JWT_SECRET: Joi.string().required(), }), }), ], }) export class AppModule {}
이 방식은 깔끔하지만, 코드에서 process.env.PORT를 꺼내 쓸 때 타입이 여전히 string | undefined로 잡힌다는 단점이 있습니다.
먼저 클래스와 데코레이터로 기대하는 환경 변수의 형태를 정의합니다.
// env.validation.ts import { plainToInstance } from 'class-transformer'; import { IsEnum, IsNumber, IsString, validateSync } from 'class-validator'; enum Environment { Development = 'development', Production = 'production', Test = 'test', } // 1. 검증할 환경 변수 클래스 (DTO와 유사) export class EnvironmentVariables { @IsEnum(Environment) NODE_ENV: Environment; @IsNumber() PORT: number; @IsString() DATABASE_URL: string; } // 2. 검증을 수행할 커스텀 함수 export function validate(config: Record<string, unknown>) { // 평범한 객체(config)를 우리가 정의한 클래스의 인스턴스로 변환합니다. const validatedConfig = plainToInstance( EnvironmentVariables, config, { enableImplicitConversion: true }, // '3000' 같은 문자열을 숫자로 자동 변환! ); const errors = validateSync(validatedConfig, { skipMissingProperties: false }); if (errors.length > 0) { throw new Error(`환경 변수 검증 실패: ${errors.toString()}`); } return validatedConfig; }
이제 app.module.ts에 만든 validate 함수를 연결합니다.
// app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { validate } from './env.validation'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, validate, // 우리가 만든 커스텀 validate 함수 연결 }), ], }) export class AppModule {}
.env에 DATABASE_URL을 적지 않았다면, 서버는 켜지지 않고 즉시 에러 로그를 뿜으며 죽습니다. 장애를 사전에 차단한 것입니다..env 파일의 모든 값은 본질적으로 string(문자열)입니다. 하지만 class-validator의 enableImplicitConversion: true 옵션을 사용하면 PORT=3000이라는 문자열이 서버 내부에서는 Number 타입으로 캐스팅돼 들어옵니다. 코드에서 parseInt()를 남발할 필요가 없어집니다.NestJS를 쓴다면 프레임워크의 철학(데코레이터와 클래스 기반)과 가장 잘 어울리는 class-validator 방식을 권합니다. API의 DTO를 검증할 때 쓰던 지식을 그대로 환경 변수 검증에도 사용할 수 있으니 일석이조입니다. "서버가 켜졌다는 것은 모든 환경 변수가 완벽하게 준비됐다는 뜻"이라는 믿음을 가질 수 있도록, 지금 환경 변수 검증 로직을 추가해 보세요.