HooneyLog
© 2026 Seunghoon Shin. All rights reserved.
모든 게시글
Backend
2026. 3. 28.•
1

NestJS 환경 변수 검증의 업계 표준: Joi vs class-validator

Seunghoon Shin
작성자 Seunghoon Shin풀스택 개발자

NestJS 환경 변수 검증의 업계 표준: Joi vs class-validator

1. 문제의 배경

"로컬에서는 잘 되는데 배포만 하면 죽어요!" 주니어 시절 가장 많이 하는 실수 중 하나가 바로 환경 변수(Environment Variables) 누락입니다. 서버를 띄울 때 필요한 DATABASE_URL이나 JWT_SECRET 같은 필수 값들이 .env 파일에 없거나 타입이 잘못되었을 때, Node.js는 아무런 불평 없이 일단 서버를 띄웁니다.

그러다 사용자가 특정 API를 호출해서 해당 환경 변수에 접근하는 순간, undefined 에러를 뱉으며 애플리케이션이 뻗어버리는 대참사가 발생합니다. 환경 변수 오류는 런타임(Runtime)이 아니라, 서버가 켜지는 부트스트랩(Bootstrap) 단계에서 즉시 터지게 만들어야 합니다. 이것이 서버 개발의 철칙이자 'Fail Fast(빠르게 실패하기)' 전략의 핵심입니다.

2. 해결 방안 탐색

NestJS 커뮤니티와 업계 표준에서는 이 문제를 해결하기 위해 주로 @nestjs/config 패키지와 함께 두 가지 검증 방식을 사용합니다.

  1. Joi 라이브러리 사용 (과거부터 이어져 온 전통의 강자)
    • 장점: 스키마 정의가 매우 직관적이고 강력합니다.
    • 단점: TypeScript의 타입 추론과 완벽하게 맞물리지 않아, 검증 스키마와 TypeScript 인터페이스를 두 번 작성해야 하는 번거로움이 있습니다.
  2. class-validator와 class-transformer 사용 (NestJS의 철학에 가장 부합하는 최신 트렌드)
    • 장점: DTO를 검증할 때 쓰던 데코레이터 방식을 그대로 사용할 수 있어 코드의 일관성이 높고, 클래스 기반이므로 TypeScript 타입 추론이 매우 강력합니다.
    • 단점: 설정 코드가 Joi에 비해 아주 약간 더 길어질 수 있습니다.

최근의 업계 트렌드는 NestJS의 생태계와 가장 잘 어울리는 class-validator 방식을 선호하는 추세입니다. 하지만 레거시 시스템이나 팀의 성향에 따라 Joi를 쓰는 곳도 많으므로 두 가지를 모두 알아보겠습니다.

3. 핵심 개념 및 아키텍처

NestJS의 @nestjs/config 모듈은 내부적으로 .env 파일을 읽어들입니다. 이때 모듈의 설정 옵션으로 validationSchema (Joi용) 또는 validate (class-validator 커스텀 함수용)를 넘겨주면, 환경 변수를 서버 메모리에 올리기 전에 유효성 검사를 수행합니다.

만약 이 검사에서 단 하나라도 실패하면, NestJS 애플리케이션은 즉시 부트스트랩을 멈추고 어떤 환경 변수가 누락되었는지 명확한 에러 로그를 남기고 종료(Exit) 됩니다.

4. 구현 및 트러블슈팅

방법 1: Joi를 이용한 환경 변수 검증 (전통적인 방식)

먼저 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로 잡힌다는 단점이 있습니다.

방법 2: class-validator를 이용한 환경 변수 검증 (추천하는 모던 방식)

먼저 클래스와 데코레이터를 이용해 우리가 기대하는 환경 변수의 형태를 정의합니다.

// 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 {}

5. 결과 및 Trade-off

  • 안정성 확보: 이제 서버를 배포할 때 실수로 .env에 DATABASE_URL을 적지 않았다면, 서버는 켜지지 않고 즉시 에러 로그를 뿜으며 죽습니다. 장애를 사전에 완벽히 차단한 것입니다.
  • 타입 캐스팅의 이점 (class-validator): .env 파일의 모든 값은 본질적으로 무조건 string(문자열)입니다. 하지만 class-validator의 enableImplicitConversion: true 옵션을 사용하면, PORT=3000이라는 문자열이 서버 내부에서는 깔끔하게 Number 타입으로 캐스팅되어 들어옵니다. 코드를 짤 때 parseInt()를 남발할 필요가 없어집니다.
  • Trade-off: 초기 세팅을 위해 보일러플레이트 코드를 약간 더 작성해야 합니다. 하지만 한 번 세팅해 두면 프로젝트 내내 가져다주는 심리적 안정감이 이 단점을 가볍게 압도합니다.

6. 마치며

NestJS를 사용하신다면 프레임워크의 철학(데코레이터와 클래스 기반)과 가장 잘 어울리는 class-validator 방식을 사용하는 것을 강력히 추천합니다. API의 DTO를 검증할 때 쓰던 지식을 그대로 환경 변수 검증에도 사용할 수 있으니 일석이조입니다. "서버가 켜졌다는 것은, 모든 환경 변수가 완벽하게 준비되었다는 뜻이다."라는 믿음을 가질 수 있도록 지금 당장 환경 변수 검증 로직을 추가해 보세요!

← 이전 글NestJS에서 직렬화와 역직렬화 완벽 가이드: DTO와 class-transformer의 마법
다음 글 →TypeORM vs Prisma: Supabase 환경에서의 최적의 ORM 선택과 운영 전략