본문으로 건너뛰기
Advertisement

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 (타입 안전)
}
Advertisement