Vue 3 프로젝트에서 vee-validate 4 + zod 조합으로 로그인 폼을 구현했는데, 로그인 버튼을 클릭해도 아무 일도 일어나지 않았다. 에러 메시지도 없고, 콘솔에도 아무것도 찍히지 않는 완전한 silent failure였다.
Table of contents
Open Table of contents
증상
- 로그인 버튼 클릭 시 아무 반응 없음
- 콘솔에 에러 없음
- 네트워크 요청 없음
- 유효성 검증 에러 메시지도 표시되지 않음
- ID/PW 입력 후 클릭해도 동일
문제 코드
<script setup lang="ts">
// https://vee-validate.logaretm.com/v4/guide/composition-api/validation/
import { useForm, useField } from 'vee-validate'
// https://vee-validate.logaretm.com/v4/integrations/zod-schema-validation/
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod' // https://zod.dev/
const schema = toTypedSchema(
z.object({
id: z.string().min(1, 'ID를 입력해주세요'),
pw: z.string().min(3, '비밀번호는 3자 이상이어야 합니다').max(20),
remember: z.boolean(), // 문제 지점
}),
)
const { handleSubmit, errors } = useForm({
validationSchema: schema,
// initialValues 미설정 — 이것이 원인
})
const { value: id } = useField<string>('id')
const { value: pw } = useField<string>('pw')
const { value: remember } = useField<boolean>('remember')
const onSubmit = handleSubmit(values => {
console.log('로그인 시도:', values)
// API 호출...
})
</script>
<template>
<form @submit.prevent="onSubmit">
<input v-model="id" type="text" placeholder="ID" />
<input v-model="pw" type="password" placeholder="Password" />
<label>
<input v-model="remember" type="checkbox" />
아이디 저장
</label>
<button type="submit">로그인</button>
</form>
</template>
원인
initialValues 미설정 + boolean 필드
useForm에서 initialValues를 지정하지 않으면, 모든 필드의 초기값이 undefined가 된다. 문자열 필드(id, pw)는 사용자가 입력하면 undefined → string으로 자연스럽게 타입이 변경되지만, 체크박스(remember)는 사용자가 건드리지 않으면 undefined인 채로 남는다.
zod의 z.boolean()은 undefined를 허용하지 않는다. 폼 제출 시 vee-validate가 zod 검증을 실행하면 remember 필드에서 검증 실패가 발생한다.
여기서 핵심적인 문제는 vee-validate의 handleSubmit이 검증 실패 시 아무것도 하지 않는다는 것이다. 에러 콜백을 등록하지 않으면 조용히 실패한다.
// handleSubmit의 시그니처
handleSubmit(
onSuccess: (values) => void,
onError?: (errors) => void // 이걸 등록하지 않으면 silent failure
)
왜 에러 메시지가 안 보이는가
errors 객체에는 remember 필드의 에러가 들어있지만, UI에 remember 필드의 에러를 표시하는 요소가 없다. 체크박스에 “필수 입력” 에러를 표시하는 UI는 보통 만들지 않기 때문이다.
해결
방법 1: initialValues 설정 (권장)
const { handleSubmit, errors } = useForm({
validationSchema: schema,
initialValues: {
id: '',
pw: '',
remember: false, // boolean 초기값 명시
},
})
방법 2: zod 스키마에서 default 사용
const schema = toTypedSchema(
z.object({
id: z.string().min(1, 'ID를 입력해주세요'),
pw: z.string().min(3, '비밀번호는 3자 이상이어야 합니다').max(20),
remember: z.boolean().default(false), // default로 undefined 방지
}),
)
방법 3: optional로 변경
remember: z.boolean().optional(), // undefined 허용
디버깅 팁: onError 콜백 등록
개발 중에는 항상 handleSubmit의 두 번째 인자로 에러 콜백을 등록해두면 silent failure를 방지할 수 있다.
const onSubmit = handleSubmit(
values => {
console.log('성공:', values)
},
errors => {
console.error('검증 실패:', errors) // 이제 에러가 보인다
}
)
교훈
- vee-validate + zod에서 boolean 필드는 반드시
initialValues를 설정한다 — 체크박스는 사용자가 건드리지 않으면undefined로 남는다 handleSubmit에 에러 콜백을 등록한다 — 없으면 검증 실패가 완전히 무시된다- zod 스키마에서
.default()를 활용한다 —initialValues를 깜빡해도 안전하다 - 테스트에서 실제 DOM 바인딩을 검증한다 — mock으로 우회하면 이런 타입 불일치를 발견할 수 없다
이 문제는 타입 시스템의 빈틈에서 발생한다. TypeScript는 useField<boolean>로 선언했으니 타입이 맞다고 생각하지만, 런타임에서는 undefined가 들어온다. zod가 이를 잡아주지만, vee-validate가 에러를 조용히 삼킨다. 세 라이브러리의 동작 방식을 모두 이해해야 디버깅할 수 있는 문제다.
참고 자료
- vee-validate Zod Integration — vee-validate 공식 Zod 스키마 연동 문서
- useForm API — useForm initialValues 동작 공식 문서