
개발하다가 Zod가 필요한지 타입/인터페이스로 충분한지 구분하는 법
핵심 판단 기준
개발 중 런타임에서 실제 데이터 검증이 필요한지가 가장 중요한 판단 기준이다. TypeScript는 컴파일 타임에만 타입 검사를 하므로, 외부에서 들어오는 데이터나 사용자 입력처럼 신뢰할 수 없는 데이터를 다룰 때는 Zod가 필요하다123.
1. Zod가 필요한 상황들
1.1 외부 데이터 소스 처리456
// ❌ TypeScript만으로는 런타임 안전성 보장 불가
interface ApiResponse {
id: number;
name: string;
email: string;
}
async function fetchUserData(): Promise<ApiResponse> {
const response = await fetch('/api/user');
const data = await response.json();
return data; // 실제 데이터 구조를 보장할 수 없음
}
// ✅ Zod로 런타임 검증
const ApiResponseSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email()
});
async function fetchUserDataSafe() {
const response = await fetch('/api/user');
const data = await response.json();
return ApiResponseSchema.parse(data); // 런타임에서 실제 검증
}
판단 포인트:
- API 응답 데이터
- JSON 파일에서 읽은 데이터
- 외부 서비스로부터 받은 데이터
- 데이터베이스에서 조회한 raw 데이터
1.2 사용자 입력 검증789
// 사용자 폼 데이터 검증
const UserRegistrationSchema = z.object({
username: z.string()
.min(3, "사용자명은 3자 이상")
.max(20, "사용자명은 20자 이하"),
email: z.string().email("올바른 이메일 형식이 아님"),
password: z.string()
.min(8, "비밀번호는 8자 이상")
.regex(/[A-Z]/, "대문자 포함 필요")
.regex(/[0-9]/, "숫자 포함 필요"),
age: z.number().min(13, "13세 이상만 가입 가능")
});
function validateUserInput(formData: FormData) {
const result = UserRegistrationSchema.safeParse({
username: formData.get('username'),
email: formData.get('email'),
password: formData.get('password'),
age: Number(formData.get('age'))
});
if (!result.success) {
return { errors: result.error.flatten().fieldErrors };
}
return { data: result.data };
}
판단 포인트:
- 웹 폼 입력
- URL 쿼리 파라미터
- 파일 업로드
- 명령줄 인수
1.3 환경 설정 및 구성 검증1011
// 환경 변수 검증
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
PORT: z.string().regex(/^\d+$/).transform(Number),
REDIS_URL: z.string().url().optional()
});
// 애플리케이션 시작 시 환경 변수 검증
export const env = EnvSchema.parse(process.env);
판단 포인트:
- 환경 변수
- 설정 파일 (JSON, YAML 등)
- 런타임 구성 옵션
2. TypeScript 타입/인터페이스로 충분한 상황들
2.1 내부 함수 간 데이터 전달122
// ✅ TypeScript 타입만으로 충분
interface User {
id: string;
name: string;
email: string;
}
function createUserProfile(user: User): UserProfile {
return {
displayName: user.name,
contactEmail: user.email,
userId: user.id
};
}
function processUsers(users: User[]): UserProfile[] {
return users.map(createUserProfile);
}
판단 포인트:
- 함수 간 매개변수 전달
- 내부 비즈니스 로직
- 컴포넌트 props (신뢰할 수 있는 데이터)
2.2 타입 정의 및 구조 명세1314
// ✅ 인터페이스로 충분한 경우
interface DatabaseEntity {
id: string;
createdAt: Date;
updatedAt: Date;
}
interface Product extends DatabaseEntity {
name: string;
price: number;
category: string;
}
interface OrderItem {
product: Product;
quantity: number;
unitPrice: number;
}
판단 포인트:
- 데이터 모델 정의
- API 응답 타입 명세 (문서화 목적)
- 컴포넌트 인터페이스 정의
2.3 라이브러리/프레임워크 내부 코드1516
// ✅ 내부 유틸리티 함수
function formatCurrency(amount: number, currency: string): string {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency
}).format(amount);
}
function calculateTax(price: number, taxRate: number): number {
return price * taxRate;
}
3. 실용적인 판단 체크리스트
3.1 “Zod 필요” 체크리스트31
- 데이터 소스를 신뢰할 수 없는가?
- 런타임에서 데이터가 예상과 다를 가능성이 있는가?
- 잘못된 데이터로 인한 애플리케이션 오류가 심각한가?
- 사용자에게 명확한 오류 메시지를 제공해야 하는가?
- 데이터 변환이나 정규화가 필요한가?
3.2 “TypeScript만으로 충분” 체크리스트217
- 데이터가 내부적으로 생성되고 관리되는가?
- 컴파일 타임 타입 검사만으로 충분한가?
- 런타임 오류 가능성이 낮은가?
- 단순한 타입 정의나 문서화가 목적인가?
- 성능이 매우 중요하고 검증 오버헤드를 피하고 싶은가?
4. 경계 상황 판단 가이드
4.1 API 개발 시 판단718
// 🤔 경계 상황: 내부 API 호출
// 판단 기준: API가 같은 팀/프로젝트인가? 외부 의존성인가?
// ✅ 내부 API - TypeScript만으로 충분할 수 있음
async function getInternalUserData(userId: string): Promise<User> {
const response = await internalApi.get(`/users/${userId}`);
return response as User; // 내부 API는 신뢰할 수 있음
}
// ✅ 외부 API - Zod 검증 필요
async function getExternalUserData(userId: string) {
const response = await externalApi.get(`/users/${userId}`);
const data = await response.json();
return UserSchema.parse(data); // 외부 API는 검증 필요
}
4.2 폼 처리 시 판단719
// 🤔 경계 상황: 간단한 폼 vs 복잡한 폼
// ✅ 간단한 검색 폼 - TypeScript만으로 충분
interface SearchForm {
query: string;
category?: string;
}
// ✅ 복잡한 사용자 등록 폼 - Zod 필요
const RegistrationSchema = z.object({
email: z.string().email(),
password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: "비밀번호가 일치하지 않습니다",
path: ["confirmPassword"]
});
5. 성능과 복잡성 고려사항
5.1 성능 임계점2021
// 성능이 중요한 경우의 판단
const isPerformanceCritical = (
frequency: 'high' | 'medium' | 'low',
dataSize: 'large' | 'medium' | 'small'
) => {
if (frequency === 'high' && dataSize === 'large') {
// 고빈도 + 대용량 = TypeScript 타입 선호
return 'typescript';
}
if (frequency === 'low' || dataSize === 'small') {
// 저빈도 또는 소용량 = Zod 사용 가능
return 'zod';
}
return 'depends';
};
5.2 팀 규모와 협업 고려1422
- 소규모 팀: Zod로 단일 진실 소스 유지
- 대규모 팀: TypeScript 인터페이스로 계약 명시, 경계에서만 Zod 사용
- 외부 협업: API 경계에서 반드시 Zod 검증
결론
핵심 원칙: 신뢰할 수 없는 데이터를 다룰 때는 Zod, 신뢰할 수 있는 내부 데이터는 TypeScript 타입으로 충분하다.
실용적 접근법:
- 데이터 경계에서 Zod로 검증
- 내부 로직에서는 TypeScript 타입 활용
- 단일 진실 소스: Zod 스키마에서 타입 추론
- 성능 vs 안전성 트레이드오프 고려
경험칙: 런타임 오류가 발생했을 때 “이 데이터가 예상과 달랐다면?”이라는 질문의 답이 “심각한 문제”라면 Zod를 사용하고, “괜찮다”라면 TypeScript 타입만으로 충분하다.
Footnotes
-
https://www.wisp.blog/blog/validating-typescript-types-in-runtime-using-zod ↩ ↩2
-
https://stevekinney.com/courses/full-stack-typescript/type-safety-vs-runtime-validation ↩ ↩2 ↩3
-
https://www.totaltypescript.com/when-should-you-use-zod ↩ ↩2
-
https://2ality.com/2020/06/validating-data-typescript.html ↩
-
https://exploringjs.com/ts/book/ch_validating-external-data.html ↩
-
https://www.allthingstypescript.dev/p/typescript-how-do-you-provide-types ↩
-
https://blog.bitsrc.io/7-powerful-use-cases-for-zod-schemas-b6df6d77bebc ↩ ↩2 ↩3
-
https://help.salesforce.com/s/articleView?id=platform.flow_ref_elements_screen_validate.htm\&language=en_US\&type=5 ↩
-
https://www.reddit.com/r/learnpython/comments/riija3/what_is_the_correct_way_to_validate_user_input/ ↩
-
https://blog.openreplay.com/validate-data-typescript-zod-examples/ ↩
-
https://betterstack.com/community/guides/scaling-nodejs/zod-explained/ ↩
-
https://www.reddit.com/r/typescript/comments/10f8kah/is_using_zod_as_the_primary_source_of_truth_for/ ↩ ↩2
-
https://dev.to/jareechang/zod-the-next-biggest-thing-after-typescript-4phh ↩
-
https://www.reddit.com/r/react/comments/1iu00tm/why_use_zod_or_yup_when_you_have_typescript/ ↩
-
https://www.linkedin.com/pulse/understanding-difference-between-compile-time-run-time-saïd-mouhoun-xyuuf ↩
-
https://stackoverflow.com/questions/58861115/what-is-the-best-way-to-automatically-validate-requests-using-typescript ↩
-
https://tighten.com/insights/form-validation-with-type-inference-made-easy-with-zod-the-best-sidekick-for-typescript/ ↩
-
https://dev.to/dzakh/zod-v4-17x-slower-and-why-you-should-care-1m1 ↩
-
https://dev.to/nicklucas/typescript-runtime-validators-and-dx-a-type-checking-performance-analysis-of-zodsuperstructyuptypebox-5416 ↩
-
https://www.allthingstypescript.dev/p/using-zod-schemas-as-source-of-truth ↩