본문으로 건너뛰기
Advertisement

18.1 타입 안전한 환경 변수 관리 — Zod 기반 config 모듈

환경 변수의 문제

// 문제: process.env 값은 string | undefined
const port = process.env.PORT // string | undefined
const timeout = process.env.TIMEOUT // string | undefined

// 런타임에서야 발견되는 오류들
app.listen(port) // NaN이 될 수 있음!

Zod 기반 환경 변수 검증

// config/env.ts
import { z } from 'zod'

const envSchema = z.object({
// 서버 설정
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),

// 데이터베이스
DATABASE_URL: z.string().url('올바른 데이터베이스 URL을 입력하세요.'),
DATABASE_POOL_SIZE: z.coerce.number().int().min(1).max(100).default(10),

// JWT 인증
JWT_SECRET: z.string().min(32, 'JWT 비밀키는 32자 이상이어야 합니다.'),
JWT_EXPIRES_IN: z.string().default('1h'),
REFRESH_TOKEN_SECRET: z.string().min(32),
REFRESH_TOKEN_EXPIRES_IN: z.string().default('7d'),

// 외부 서비스 (선택적)
REDIS_URL: z.string().url().optional(),
AWS_REGION: z.string().optional(),
AWS_S3_BUCKET: z.string().optional(),
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().int().optional(),

// 로그
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
})

// 타입 추론
type Env = z.infer<typeof envSchema>

// 앱 시작 시 검증 — 실패하면 즉시 종료
function validateEnv(): Env {
const result = envSchema.safeParse(process.env)

if (!result.success) {
console.error('❌ 환경 변수 검증 실패:')
result.error.issues.forEach(issue => {
console.error(` ${issue.path.join('.')}: ${issue.message}`)
})
process.exit(1)
}

return result.data
}

export const env = validateEnv()

도메인별 설정 분리

// config/database.config.ts
import { env } from './env'

export const databaseConfig = {
url: env.DATABASE_URL,
pool: {
max: env.DATABASE_POOL_SIZE,
min: 2,
idleTimeoutMillis: 30_000,
},
ssl: env.NODE_ENV === 'production' ? { rejectUnauthorized: true } : false,
} as const

// config/auth.config.ts
export const authConfig = {
jwt: {
secret: env.JWT_SECRET,
expiresIn: env.JWT_EXPIRES_IN,
},
refreshToken: {
secret: env.REFRESH_TOKEN_SECRET,
expiresIn: env.REFRESH_TOKEN_EXPIRES_IN,
},
} as const

// config/app.config.ts
export const appConfig = {
port: env.PORT,
nodeEnv: env.NODE_ENV,
isDevelopment: env.NODE_ENV === 'development',
isProduction: env.NODE_ENV === 'production',
isTest: env.NODE_ENV === 'test',
logLevel: env.LOG_LEVEL,
} as const

// 통합 설정 내보내기
export { env, databaseConfig, authConfig, appConfig }

.env 파일 전략

# .env.example (git에 커밋 — 예시 값)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters
REFRESH_TOKEN_SECRET=your-refresh-token-secret-at-least-32-chars

# .env (git에서 제외 — 실제 값)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://admin:realpass@localhost:5432/myapp_dev
JWT_SECRET=a-real-secret-key-that-is-long-enough-32chars

# .env.test (테스트 환경)
NODE_ENV=test
DATABASE_URL=postgresql://admin:realpass@localhost:5432/myapp_test

# .env.production (배포 시 환경 변수로 주입)
# .env 파일 사용하지 않고 시스템 환경 변수 직접 설정

Next.js 환경 변수 타입 안전성

// @t3-oss/env-nextjs 사용
// npm install @t3-oss/env-nextjs zod
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'

export const env = createEnv({
// 서버 전용 (클라이언트에서 접근 불가)
server: {
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
},

// 클라이언트 공개 (NEXT_PUBLIC_ 접두사 필요)
client: {
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_GA_ID: z.string().optional(),
},

// 실제 환경 변수와 연결
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
JWT_SECRET: process.env.JWT_SECRET,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
},
})

// 사용
import { env } from '@/config/env'
const apiUrl = env.NEXT_PUBLIC_API_URL // string (검증됨)

고수 팁

테스트 환경 변수 오버라이드

// test/setup.ts
import { vi } from 'vitest'

// 환경 변수 모킹
vi.stubEnv('DATABASE_URL', 'postgresql://test:test@localhost:5432/testdb')
vi.stubEnv('JWT_SECRET', 'test-secret-that-is-long-enough-for-testing')

// 또는 setupFiles에서 직접 설정
process.env.NODE_ENV = 'test'
process.env.DATABASE_URL = 'postgresql://...'
Advertisement