13.5 환경 변수 타입 안전성 — Zod + process.env
환경 변수의 문제점
// ❌ 타입 없이 사용 — 위험!
const dbUrl = process.env.DATABASE_URL // string | undefined
const port = process.env.PORT // string | undefined
// 앱 시작 후에야 오류 발견
new Client({ connectionString: dbUrl }) // 런타임에 undefined 오류 가능
Zod를 사용한 런타임 검증
npm install zod
// src/config/env.ts
import { z } from 'zod'
const envSchema = z.object({
// 서버 설정
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().int().positive().default(3000),
HOST: z.string().default('0.0.0.0'),
// 데이터베이스
DATABASE_URL: z.string().url(),
DATABASE_POOL_SIZE: z.coerce.number().int().min(1).max(100).default(10),
// 인증
JWT_SECRET: z.string().min(32, 'JWT_SECRET은 최소 32자 이상이어야 합니다'),
JWT_EXPIRES_IN: z.string().default('7d'),
REFRESH_TOKEN_SECRET: z.string().min(32),
// 외부 서비스
REDIS_URL: z.string().url().optional(),
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().optional(),
SMTP_USER: z.string().optional(),
SMTP_PASS: z.string().optional(),
// AWS
AWS_REGION: z.string().optional(),
AWS_ACCESS_KEY_ID: z.string().optional(),
AWS_SECRET_ACCESS_KEY: z.string().optional(),
S3_BUCKET: z.string().optional(),
// 모니터링
SENTRY_DSN: z.string().url().optional(),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
})
// 타입 추출
export type Env = z.infer<typeof envSchema>
// 검증 실행 (앱 시작 시)
function parseEnv(): Env {
const result = envSchema.safeParse(process.env)
if (!result.success) {
console.error('\n❌ 환경변수 설정 오류:\n')
result.error.issues.forEach(issue => {
console.error(` [${issue.path.join('.')}] ${issue.message}`)
})
console.error('\n.env 파일을 확인하세요.\n')
process.exit(1)
}
return result.data
}
export const env = parseEnv()
설정 파일 패턴
// src/config/index.ts
import { env } from './env'
export const config = {
server: {
port: env.PORT,
host: env.HOST,
isDevelopment: env.NODE_ENV === 'development',
isProduction: env.NODE_ENV === 'production',
isTest: env.NODE_ENV === 'test',
},
database: {
url: env.DATABASE_URL,
poolSize: env.DATABASE_POOL_SIZE,
},
auth: {
jwtSecret: env.JWT_SECRET,
jwtExpiresIn: env.JWT_EXPIRES_IN,
refreshTokenSecret: env.REFRESH_TOKEN_SECRET,
},
mail: env.SMTP_HOST
? {
host: env.SMTP_HOST,
port: env.SMTP_PORT ?? 587,
user: env.SMTP_USER,
pass: env.SMTP_PASS,
}
: null,
aws: env.AWS_REGION
? {
region: env.AWS_REGION,
accessKeyId: env.AWS_ACCESS_KEY_ID!,
secretAccessKey: env.AWS_SECRET_ACCESS_KEY!,
s3Bucket: env.S3_BUCKET,
}
: null,
}
export type Config = typeof config
dotenv 통합
npm install dotenv
// src/index.ts (진입점 최상단에서 로드)
import 'dotenv/config' // 또는
import dotenv from 'dotenv'
dotenv.config({ path: '.env.local' }) // 커스텀 파일명
import { env } from './config/env' // 이후에 검증
// 이제 process.env에 .env 파일이 로드됨
환경별 .env 파일 전략
.env # 공통 기본값 (git에 커밋)
.env.development # 개발 환경 (git에 커밋, 민감정보 제외)
.env.production # 프로덕션 환경 (git에 커밋 가능, 실제 값은 CI/CD에서 주입)
.env.local # 로컬 오버라이드 (git 제외)
.env.test # 테스트 환경
# .env.example (git에 커밋 — 필요한 변수 목록 공유용)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=your-jwt-secret-min-32-chars-here
REFRESH_TOKEN_SECRET=your-refresh-secret-min-32-chars
테스트 환경 설정
// tests/setup.ts
process.env.NODE_ENV = 'test'
process.env.DATABASE_URL = 'postgresql://user:password@localhost:5432/testdb'
process.env.JWT_SECRET = 'test-jwt-secret-min-32-chars-long!'
process.env.REFRESH_TOKEN_SECRET = 'test-refresh-secret-min-32-chars!'
고수 팁
1. 환경변수 타입 선언 병행 (IDE 지원)
// src/types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv extends z.infer<typeof envSchema> {}
}
2. 조건부 설정 타입 안전하게 처리
// 선택적 서비스 설정을 타입 안전하게
function getMailConfig() {
if (!env.SMTP_HOST) return null
// 여기에 도달하면 SMTP_HOST가 있다고 확정됨
return {
host: env.SMTP_HOST, // string (undefined 아님)
port: env.SMTP_PORT ?? 587,
}
}
const mailConfig = getMailConfig()
if (mailConfig) {
// mailConfig.host: string (타입 안전)
}