Vue 3 폼 검증을 재사용 가능하게 설계하는 법

Vue 3 폼 검증을 재사용 가능하게 설계하는 법 관련 이미지
Photo by Alltechbuzz_net via Pixabay

회원가입이나 주문 화면의 폼은 값을 입력받는 것보다 검증과 오류 표시, 서버 전송, 중복 제출 방지가 더 어렵습니다. 필드마다 watch와 오류 문구를 복사하면 규칙 변경 시 여러 화면을 고쳐야 하고 클라이언트와 서버의 판단도 달라질 수 있습니다. Vue 3에서는 원본 값, 검증 결과, 사용자 상호작용 상태와 서버 오류를 분리해 폼 흐름을 예측 가능하게 만드는 것이 중요합니다.

클라이언트 검증의 역할

클라이언트 검증은 필수값 누락과 형식 오류를 즉시 알려 사용자 경험을 개선합니다. 하지만 요청은 개발자 도구나 다른 클라이언트에서 직접 보낼 수 있으므로 보안 경계가 아닙니다. 서버는 동일한 업무 규칙과 권한을 다시 검증해야 합니다. 서버가 최종 판단자이며 클라이언트 규칙은 빠른 안내라는 역할을 명확히 합니다.

값과 오류를 별도 상태로 관리하기

reactive 객체에 폼 값을 두고 errors에는 필드별 오류를 저장할 수 있습니다. touched는 사용자가 필드를 한 번이라도 떠났는지, dirty는 최초 값과 달라졌는지를 나타냅니다. 처음 화면을 열자마자 모든 오류를 빨간색으로 표시하면 부담스러우므로 touched 이후 또는 제출 시도 후에 오류를 보여 주는 정책이 일반적입니다.

상태 체크포인트

  • values는 사용자가 편집하는 원본 값을 담습니다.
  • errors는 화면 표시용 검증 결과를 담습니다.
  • touched와 dirty의 의미를 구분합니다.
  • isSubmitting으로 중복 제출을 막습니다.
  • 서버 필드 오류와 전체 업무 오류를 나눕니다.

검증 시점을 필드 특성에 맞추기

입력할 때마다 검증하면 빠른 피드백을 줄 수 있지만 이메일 중복 확인 같은 서버 요청은 과도하게 발생합니다. 필수 형식은 입력 또는 blur 시 검증하고, 비동기 검증은 디바운스와 취소를 적용합니다. 느린 이전 응답이 최신 입력의 오류를 덮지 않도록 요청 ID나 AbortController를 사용합니다. 최종 제출 때는 모든 필드를 다시 검증합니다.

실무 원칙: 비동기 검증 결과는 요청을 시작했을 때의 값과 현재 값이 같은 경우에만 화면에 반영해야 합니다.

composable로 규칙과 동작 분리하기

useForm이나 useField composable은 값, 오류, 검증 함수, 제출 상태를 묶어 여러 화면에서 재사용할 수 있습니다. 다만 모든 업무 폼을 하나의 거대한 composable로 만들면 옵션이 늘고 타입 추론이 어려워집니다. 공통적인 필드 상태 관리와 제출 흐름만 일반화하고 주문 한도나 쿠폰 조건 같은 업무 규칙은 기능별 함수로 유지합니다.

computed로 제출 가능 상태 만들기

오류 개수와 필수값, 제출 중 여부에서 계산할 수 있는 canSubmit은 computed로 표현합니다. 별도 ref를 watch로 갱신하면 조건 하나를 빠뜨려 값이 어긋날 수 있습니다. 버튼을 비활성화하더라도 사용자가 왜 제출할 수 없는지 알 수 있어야 하며, 키보드 제출과 직접 API 호출을 대비해 submit 함수 안에서 다시 검사합니다.

서버 오류를 필드에 연결하기

서버가 안정적인 오류 코드와 필드 경로를 반환하면 이메일 중복, 재고 부족 같은 오류를 해당 입력 가까이에 표시할 수 있습니다. 데이터베이스의 내부 메시지를 그대로 보여 주거나 문자열 문구를 파싱하지 않습니다. 여러 필드에 걸친 오류는 폼 상단 요약에 표시하고 사용자가 수정할 첫 필드로 초점을 이동할 수 있습니다.

중복 제출과 멱등성

isSubmitting으로 버튼을 잠그는 것은 같은 브라우저의 빠른 중복 클릭을 줄이지만 네트워크 재시도와 새로고침까지 막지는 못합니다. 주문과 결제처럼 중요한 작업은 서버의 멱등성 키가 필요합니다. 요청 성공 후 라우트 이동이 실패해도 다시 제출하지 않도록 생성된 주문 ID를 상태에 보관하고 결과 조회 화면으로 이동합니다.

접근성을 포함한 오류 표시

label과 input을 명시적으로 연결하고 오류가 있는 필드에는 aria-invalid와 설명 요소의 ID를 연결합니다. 색상만으로 오류를 구분하지 않고 텍스트로 해결 방법을 제공합니다. 제출 후 오류 요약으로 초점을 옮기거나 첫 오류 필드에 초점을 주되 사용자의 입력 중에 초점을 강제로 이동시키지 않습니다.

초기값과 수정 화면

서버에서 기존 값을 불러오는 수정 화면은 로딩 완료 시점을 최초 값으로 잡아야 dirty 판단이 정확합니다. 저장 성공 후에는 현재 값을 새로운 기준값으로 갱신합니다. props 객체를 직접 수정하지 말고 폼용 복사본을 만들며, 중첩 객체의 얕은 복사 한계를 확인합니다. 날짜와 숫자 입력은 화면 문자열과 서버 자료형의 변환 지점을 명확히 둡니다.

테스트 순서

  1. 빈 값, 경계 길이와 잘못된 형식을 검증합니다.
  2. touched 전후의 오류 표시를 확인합니다.
  3. 빠른 입력 중 오래된 비동기 응답을 무시하는지 봅니다.
  4. 서버 필드 오류와 전체 오류를 각각 표시합니다.
  5. 더블 클릭과 느린 네트워크에서 한 번만 제출되는지 시험합니다.
  6. 저장 후 dirty 상태가 초기화되는지 확인합니다.
  7. 키보드 탐색과 스크린 리더용 속성을 검사합니다.

폼 라이브러리를 사용해도 상태와 서버 계약의 설계는 남습니다. 라이브러리가 제공하는 스키마, touched, 제출 처리를 활용하되 업무 규칙이 특정 화면 컴포넌트에 흩어지지 않게 합니다. 사용자가 오류 원인을 이해하고 수정한 뒤 안전하게 다시 시도할 수 있는 흐름이 좋은 폼의 기준입니다.

한 줄 요약: Vue 폼은 값·오류·상호작용·제출 상태를 분리하고 서버 재검증과 비동기 취소, 접근성을 함께 설계해야 합니다.