TIL - 20260719

juni·2026년 7월 19일

TIL

목록 보기
408/468

0719 프론트엔드 실무 심화 (2/N): 폼 처리, 유효성 검증과 사용자 입력 UX


✅ 1. 폼이란 무엇인가?

  • 폼(Form)은 사용자가 값을 입력하고 제출하는 UI입니다.
  • 프론트엔드 실무에서 폼은 단순 input 묶음이 아니라, 입력값 관리, 유효성 검증, 에러 표시, 제출 상태, 서버 응답 처리까지 포함하는 기능 단위입니다.
  • 고객 상담 신청, 사전예약 신청, 관리자 상품 등록, 배너 등록, 주문 상태 변경, 검색 필터 모두 넓게 보면 폼입니다.
사용자 입력
  ↓
프론트 유효성 검증
  ↓
서버 제출
  ↓
서버 검증
  ↓
성공/실패 응답
  ↓
화면 피드백

➕ 1-1. 폼이 중요한 이유

  • 고객이 상담 신청을 완료할 수 있는지 결정합니다.
  • 관리자가 상품, 배너, 주문, 상담 정보를 정확히 입력할 수 있게 합니다.
  • 잘못된 입력값이 서버에 들어가는 것을 줄여줍니다.
  • 사용자 실수와 운영자 실수를 줄입니다.
  • 폼 UX가 나쁘면 기능이 있어도 전환율과 업무 효율이 떨어집니다.
폼이 불편함
  ↓
고객이 신청 포기
  ↓
상담 리드 감소

관리자 폼이 불명확함
  ↓
운영자가 잘못 저장
  ↓
데이터 정합성 문제 발생

✅ 2. 좋은 폼의 기준

  • 좋은 폼은 예쁘기만 한 폼이 아닙니다.
  • 사용자가 무엇을 입력해야 하는지 바로 알 수 있고, 실수했을 때 어떻게 고치면 되는지 알려주는 폼입니다.

➕ 2-1. 좋은 폼의 조건

입력해야 할 값이 명확함
필수값과 선택값이 구분됨
에러 메시지가 구체적임
제출 중 중복 클릭이 막힘
성공/실패 피드백이 있음
서버 에러를 사용자에게 이해 가능하게 보여줌
모바일에서도 입력하기 편함

➕ 2-2. 나쁜 폼의 예시

전화번호 입력했는데 계속 오류
어떤 필드가 문제인지 표시 안 됨
제출 버튼을 여러 번 누를 수 있음
저장 실패했는데 아무 메시지 없음
필수값 표시가 없음
성공했는지 실패했는지 알 수 없음
  • 이런 폼은 고객과 운영자 모두를 피곤하게 만듭니다.
  • 특히 관리자 페이지에서는 잘못된 입력이 운영 데이터 문제로 이어질 수 있습니다.

✅ 3. 프론트 검증과 백엔드 검증

  • 폼 검증은 프론트엔드와 백엔드 양쪽에서 해야 합니다.
  • 프론트 검증은 사용자 경험을 좋게 만드는 역할이고, 백엔드 검증은 데이터와 보안을 지키는 최종 방어선입니다.
구분역할
프론트 검증빠른 피드백, 입력 실수 감소
백엔드 검증보안, 데이터 정합성, 최종 저장 기준

➕ 3-1. 프론트 검증만 하면 안 되는 이유

사용자가 개발자 도구로 API 직접 호출
프론트 JS 우회 가능
비정상 payload 전송 가능
  • 프론트에서 버튼을 막거나 input을 제한해도 백엔드 검증은 반드시 필요합니다.
  • 백엔드 DTO validation, 권한 검사, 중복 검사는 절대 빠지면 안 됩니다.

➕ 3-2. 프론트 검증이 필요한 이유

사용자가 제출 전에 오류를 바로 확인
불필요한 API 호출 감소
모바일 입력 실수 감소
전환율 개선
관리자 업무 속도 개선
  • 백엔드가 검증하니까 프론트 검증이 필요 없다는 말도 틀립니다.
  • 둘 다 필요합니다. 역할이 다릅니다.

✅ 4. react-hook-form

  • react-hook-form은 React에서 폼 상태와 검증을 효율적으로 관리하기 위한 라이브러리입니다.
  • input이 많거나 유효성 검증이 필요한 폼에서는 useState를 여러 개 쓰는 것보다 훨씬 관리하기 쉽습니다.

➕ 4-1. 설치

npm install react-hook-form

➕ 4-2. 기본 예시

type CreateConsultFormValues = {
  name: string;
  phone: string;
  productId: number;
};

function ConsultForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<CreateConsultFormValues>({
    defaultValues: {
      name: '',
      phone: '',
      productId: 0,
    },
  });

  const onSubmit = async (values: CreateConsultFormValues) => {
    await consultApi.createConsult(values);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('name', {
          required: '이름을 입력해 주세요.',
        })}
        placeholder="이름"
      />
      {errors.name && <p>{errors.name.message}</p>}

      <input
        {...register('phone', {
          required: '전화번호를 입력해 주세요.',
        })}
        placeholder="01012345678"
      />
      {errors.phone && <p>{errors.phone.message}</p>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '신청 중...' : '상담 신청'}
      </button>
    </form>
  );
}
  • register로 input을 폼에 연결합니다.
  • handleSubmit으로 제출을 처리합니다.
  • errors로 필드별 오류를 표시할 수 있습니다.

✅ 5. zod를 활용한 스키마 검증

  • zod는 TypeScript 친화적인 검증 라이브러리입니다.
  • react-hook-form과 함께 쓰면 폼 검증 규칙을 스키마로 관리할 수 있습니다.

➕ 5-1. 설치

npm install zod @hookform/resolvers

➕ 5-2. 상담 신청 스키마 예시

import { z } from 'zod';

export const createConsultSchema = z.object({
  name: z
    .string()
    .min(2, '이름은 2자 이상 입력해 주세요.')
    .max(20, '이름은 20자 이하로 입력해 주세요.'),
  phone: z
    .string()
    .regex(/^010\d{8}$/, '전화번호는 010으로 시작하는 11자리 숫자여야 합니다.'),
  productId: z
    .number()
    .min(1, '상품을 선택해 주세요.'),
});

export type CreateConsultFormValues = z.infer<typeof createConsultSchema>;

➕ 5-3. react-hook-form 연결

const form = useForm<CreateConsultFormValues>({
  resolver: zodResolver(createConsultSchema),
  defaultValues: {
    name: '',
    phone: '',
    productId: product.id,
  },
});
  • zod를 쓰면 검증 규칙과 타입을 함께 관리할 수 있습니다.
  • 폼이 커질수록 스키마 기반 검증이 유지보수에 유리합니다.

✅ 6. defaultValues

  • defaultValues는 폼의 초기값입니다.
  • 등록 폼과 수정 폼에서 매우 중요합니다.

➕ 6-1. 등록 폼

const form = useForm<CreateProductFormValues>({
  defaultValues: {
    name: '',
    categoryId: undefined,
    price: 0,
    isVisible: true,
  },
});

➕ 6-2. 수정 폼

  • 수정 폼은 서버에서 가져온 데이터를 초기값으로 넣어야 합니다.
  • 단, 서버 데이터는 비동기로 들어오므로 reset을 함께 사용하는 경우가 많습니다.
const form = useForm<UpdateProductFormValues>({
  defaultValues: {
    name: '',
    price: 0,
    isVisible: true,
  },
});

useEffect(() => {
  if (product) {
    form.reset({
      name: product.name,
      price: product.price,
      isVisible: product.isVisible,
    });
  }
}, [product, form]);
  • 수정 폼에서 defaultValues만 믿고 서버 데이터를 넣지 않으면 빈 값으로 보일 수 있습니다.
  • 서버 데이터가 도착하면 reset으로 폼 값을 갱신하는 패턴이 안전합니다.

✅ 7. 폼 제출과 useMutation 연결

  • 폼 제출은 서버 데이터를 생성하거나 수정하는 작업입니다.
  • 따라서 TanStack Query의 useMutation과 연결하는 것이 좋습니다.

➕ 7-1. 상담 신청 제출 예시

const createConsultMutation = useMutation({
  mutationFn: (values: CreateConsultFormValues) => {
    return consultApi.createConsult(values);
  },
  onSuccess: () => {
    toast.success('상담 신청이 완료되었습니다.');
    form.reset();
    onClose();
  },
  onError: (error) => {
    toast.error('상담 신청 중 문제가 발생했습니다.');
  },
});

const onSubmit = (values: CreateConsultFormValues) => {
  createConsultMutation.mutate(values);
};

➕ 7-2. 버튼 상태

<button
  type="submit"
  disabled={createConsultMutation.isPending}
>
  {createConsultMutation.isPending ? '신청 중...' : '상담 신청'}
</button>
  • 제출 중에는 버튼을 비활성화해서 중복 제출을 막아야 합니다.
  • 하지만 프론트에서 막는 것은 UX이고, 백엔드에서도 중복 방지가 필요합니다.

✅ 8. 서버 에러 처리

  • 폼 제출 후 서버에서 에러가 올 수 있습니다.
  • 예를 들어 중복 신청, 권한 부족, 유효성 오류, 서버 장애 등이 있습니다.

➕ 8-1. 서버 에러 종류

400 Validation Error
401 Unauthorized
403 Forbidden
404 Not Found
409 Duplicated
500 Server Error

➕ 8-2. 중복 신청 에러 처리 예시

const createConsultMutation = useMutation({
  mutationFn: consultApi.createConsult,
  onError: (error: ApiError) => {
    if (error.code === 'CONSULT_DUPLICATED') {
      form.setError('phone', {
        message: '이미 신청된 전화번호입니다.',
      });
      return;
    }

    toast.error('상담 신청 중 문제가 발생했습니다.');
  },
});
  • 필드와 관련된 에러는 해당 필드에 표시하는 것이 좋습니다.
  • 전체 에러는 토스트나 상단 alert로 표시할 수 있습니다.

✅ 9. 필드 에러와 전역 에러

  • 폼 에러는 크게 필드 에러와 전역 에러로 나눌 수 있습니다.
구분예시표시 위치
필드 에러전화번호 형식 오류해당 input 아래
전역 에러서버 장애, 권한 부족폼 상단 또는 토스트
중복 에러이미 신청된 전화번호해당 필드 또는 폼 상단
인증 에러로그인 만료로그인 페이지 이동/전역 알림

➕ 9-1. 필드 에러 예시

<input {...form.register('phone')} />
{form.formState.errors.phone && (
  <p className="field-error">
    {form.formState.errors.phone.message}
  </p>
)}

➕ 9-2. 전역 에러 예시

{globalError && (
  <div role="alert" className="form-error-box">
    {globalError}
  </div>
)}
  • 사용자가 어떤 값을 고치면 되는지 알 수 있게 표시해야 합니다.
  • “오류가 발생했습니다”만 반복하면 UX가 좋지 않습니다.

✅ 10. 입력 마스킹과 정규화

  • 사용자가 입력한 값을 서버로 보내기 전에 정리해야 할 때가 있습니다.
  • 전화번호, 금액, 날짜, 사업자번호, 우편번호 등이 대표적입니다.

➕ 10-1. 전화번호 정규화

export function normalizePhone(value: string) {
  return value.replace(/\D/g, '');
}
const onSubmit = (values: CreateConsultFormValues) => {
  createConsultMutation.mutate({
    ...values,
    phone: normalizePhone(values.phone),
  });
};
  • 사용자가 010-1234-5678로 입력해도 서버에는 01012345678로 보낼 수 있습니다.
  • 백엔드에서도 다시 정규화/검증하는 것이 좋습니다.

➕ 10-2. 금액 입력 정리

export function normalizeNumber(value: string) {
  return Number(value.replace(/,/g, ''));
}
  • 화면에는 1,200,000처럼 보여주고, 서버에는 숫자 1200000으로 보내는 식입니다.

✅ 11. input type과 모바일 UX

  • 모바일에서 폼 입력은 input type만 잘 써도 UX가 좋아집니다.

➕ 11-1. 전화번호 입력

<input
  type="tel"
  inputMode="numeric"
  autoComplete="tel"
  placeholder="01012345678"
/>

➕ 11-2. 이메일 입력

<input
  type="email"
  autoComplete="email"
  placeholder="example@email.com"
/>

➕ 11-3. 숫자 입력

<input
  inputMode="numeric"
  placeholder="숫자만 입력"
/>
  • 모바일에서는 숫자 키패드가 뜨는지, 자동완성이 적절한지까지 봐야 합니다.
  • 고객 상담 신청 폼은 모바일 UX가 특히 중요합니다.

✅ 12. 필수값과 선택값 표시

  • 사용자는 어떤 값이 필수인지 알아야 합니다.
  • 필수값을 표시하지 않으면 제출 후 오류를 보고 다시 입력해야 합니다.

➕ 12-1. 예시

<label htmlFor="phone">
  전화번호 <span aria-hidden="true">*</span>
</label>
<input id="phone" {...form.register('phone')} />

➕ 12-2. 표시 기준

필수값:
라벨에 * 표시 또는 "필수" 표시

선택값:
"선택" 표시

주의 문구:
입력 예시 또는 도움말 제공
  • 필수값은 시각적으로 명확해야 합니다.
  • 단, 별표만 쓰면 접근성 측면에서 부족할 수 있으므로 에러 메시지도 함께 제공해야 합니다.

✅ 13. 로딩 상태

  • 폼 제출 중에는 사용자가 현재 처리 중이라는 것을 알아야 합니다.
  • 버튼 disabled, 텍스트 변경, 스피너, 전체 form 비활성화 등을 사용할 수 있습니다.

➕ 13-1. 제출 중 버튼

<button type="submit" disabled={mutation.isPending}>
  {mutation.isPending ? '저장 중...' : '저장'}
</button>

➕ 13-2. 제출 중 input 비활성화

<fieldset disabled={mutation.isPending}>
  <input {...form.register('name')} />
  <input {...form.register('phone')} />
  <button type="submit">저장</button>
</fieldset>
  • 저장 중 input을 수정할 수 없게 하면 중복 제출과 혼란을 줄일 수 있습니다.
  • 단, 너무 오래 걸리는 작업은 진행 상태나 안내 문구가 필요합니다.

✅ 14. 성공 피드백

  • 사용자가 제출을 완료했을 때 성공 여부를 명확히 알려줘야 합니다.

➕ 14-1. 성공 피드백 방식

토스트 메시지
완료 모달
페이지 이동
버튼 텍스트 변경
목록 자동 갱신
상세 화면 업데이트

➕ 14-2. 고객 상담 신청 성공 예시

상담 신청이 완료되었습니다.
담당자가 확인 후 순차적으로 연락드릴 예정입니다.

➕ 14-3. 관리자 상품 수정 성공 예시

상품 정보가 저장되었습니다.
  • 고객용 메시지는 안심감을 줘야 합니다.
  • 관리자용 메시지는 간결하고 명확한 것이 좋습니다.

✅ 15. 실패 피드백

  • 실패 메시지는 사용자가 다음 행동을 알 수 있게 작성해야 합니다.

➕ 15-1. 나쁜 실패 메시지

오류가 발생했습니다.
실패했습니다.
잘못된 요청입니다.

➕ 15-2. 좋은 실패 메시지

전화번호는 010으로 시작하는 11자리 숫자로 입력해 주세요.

이미 신청된 전화번호입니다. 기존 신청 내역 확인이 필요하면 상담원에게 문의해 주세요.

일시적인 문제로 저장하지 못했습니다. 잠시 후 다시 시도해 주세요.
  • 어떤 값을 고쳐야 하는지 알려주는 것이 좋습니다.
  • 서버 장애처럼 사용자가 고칠 수 없는 문제는 재시도 안내를 줍니다.

✅ 16. Confirm이 필요한 폼 작업

  • 모든 저장에 confirm을 띄우면 귀찮습니다.
  • 하지만 되돌리기 어렵거나 운영 영향이 큰 작업에는 confirm이 필요합니다.

➕ 16-1. Confirm이 필요한 작업

주문 취소
상담 완료/취소 처리
상품 삭제
배너 삭제
관리자 권한 변경
엑셀 다운로드
사전예약 오픈/종료
대량 상태 변경

➕ 16-2. Confirm 메시지 예시

이 상담을 완료 처리하시겠습니까?
완료 처리 후 고객에게 안내 알림이 발송될 수 있습니다.
이 배너를 삭제하시겠습니까?
삭제 후 고객 화면에서 즉시 노출되지 않습니다.
  • Confirm 메시지는 결과와 영향을 알려줘야 합니다.
  • 단순히 “정말 하시겠습니까?”만 쓰면 부족합니다.

✅ 17. Dirty 상태와 이탈 방지

  • Dirty 상태는 사용자가 폼 값을 변경했지만 아직 저장하지 않은 상태입니다.
  • 관리자 상품 수정, 배너 수정, 상세 설정 화면에서는 이탈 방지가 필요할 수 있습니다.

➕ 17-1. dirtyFields / isDirty

const {
  formState: { isDirty },
} = form;

➕ 17-2. 이탈 방지 예시

저장하지 않은 변경사항이 있습니다.
페이지를 벗어나면 입력한 내용이 사라집니다.
  • 고객 상담 신청 같은 짧은 폼에서는 과할 수 있습니다.
  • 관리자 상품 등록/수정처럼 입력값이 많은 폼에서는 유용합니다.

✅ 18. 검색 폼과 제출 폼의 차이

  • 검색 폼과 저장 폼은 성격이 다릅니다.
  • 검색 폼은 URL 상태와 연결되는 경우가 많고, 저장 폼은 서버 mutation과 연결됩니다.

➕ 18-1. 검색 폼

검색어
상태 필터
날짜 범위
유입경로
페이지
정렬
  • 검색 조건은 URL query string에 반영하는 것이 좋습니다.
  • 검색 버튼을 누르면 URL이 바뀌고, queryKey가 바뀌면서 목록 API가 다시 호출됩니다.

➕ 18-2. 저장 폼

상담 신청
상품 등록
상품 수정
배너 등록
상태 변경
관리자 생성
  • 저장 폼은 mutation을 사용합니다.
  • 성공 후 목록 invalidate, 모달 닫기, 토스트 표시 등을 처리합니다.

✅ 19. 관리자 검색 폼 예시

➕ 19-1. 검색 조건

keyword
status
source
startDate
endDate

➕ 19-2. 동작 흐름

폼 입력
  ↓
검색 버튼 클릭
  ↓
URL query string 업데이트
  ↓
useQuery queryKey 변경
  ↓
상담 목록 refetch

➕ 19-3. 예시 코드

const onSubmit = (values: SearchConsultFormValues) => {
  const params = new URLSearchParams();

  if (values.keyword) params.set('keyword', values.keyword);
  if (values.status) params.set('status', values.status);
  if (values.source) params.set('source', values.source);
  if (values.startDate) params.set('startDate', values.startDate);
  if (values.endDate) params.set('endDate', values.endDate);

  params.set('page', '1');

  router.push(`/admin/consults?${params.toString()}`);
};
  • 검색 시 page를 1로 초기화하는 것이 일반적입니다.
  • 필터 조건이 바뀌었는데 기존 page가 유지되면 빈 결과가 나올 수 있습니다.

✅ 20. 폼 컴포넌트 분리 기준

  • 폼이 커지면 컴포넌트를 나누는 것이 좋습니다.
  • 단, 너무 잘게 나누면 오히려 흐름이 안 보일 수 있습니다.

➕ 20-1. 분리하면 좋은 기준

기본 정보 영역
가격/지원금 영역
이미지 업로드 영역
노출 설정 영역
SEO 설정 영역
관리자 권한 설정 영역

➕ 20-2. 상품 수정 폼 예시

ProductForm
  ├─ ProductBasicSection
  ├─ ProductPriceSection
  ├─ ProductImageSection
  ├─ ProductDisplaySection
  └─ ProductSeoSection
  • 섹션 단위로 나누면 관리자가 보기에도 좋고, 코드도 이해하기 쉬워집니다.
  • 단, form context를 공유해야 하므로 FormProvider를 사용할 수 있습니다.

✅ 21. FormProvider

  • react-hook-form에서 하위 컴포넌트들이 같은 form 상태를 사용해야 할 때 FormProvider를 사용할 수 있습니다.

➕ 21-1. 예시

const form = useForm<ProductFormValues>({
  resolver: zodResolver(productSchema),
});

return (
  <FormProvider {...form}>
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <ProductBasicSection />
      <ProductPriceSection />
      <ProductDisplaySection />
      <button type="submit">저장</button>
    </form>
  </FormProvider>
);

➕ 21-2. 하위 컴포넌트

function ProductBasicSection() {
  const { register, formState: { errors } } = useFormContext<ProductFormValues>();

  return (
    <section>
      <input {...register('name')} />
      {errors.name && <p>{errors.name.message}</p>}
    </section>
  );
}
  • props로 register, errors를 계속 넘기지 않아도 됩니다.
  • 큰 폼에서 코드가 깔끔해집니다.

✅ 22. 폼 접근성 기본

  • 폼은 접근성도 중요합니다.
  • 라벨, 에러 메시지, 포커스 이동이 제대로 되어야 키보드 사용자나 보조기술 사용자도 사용할 수 있습니다.

➕ 22-1. 라벨 연결

<label htmlFor="phone">전화번호</label>
<input id="phone" {...form.register('phone')} />

➕ 22-2. 에러 메시지

<input
  id="phone"
  aria-invalid={!!errors.phone}
  aria-describedby={errors.phone ? 'phone-error' : undefined}
/>

{errors.phone && (
  <p id="phone-error" role="alert">
    {errors.phone.message}
  </p>
)}
  • 에러가 발생한 필드가 무엇인지 명확해야 합니다.
  • 모달 폼에서는 열릴 때 첫 입력 필드에 포커스를 주는 것도 좋습니다.

✅ 23. 실무 체크리스트

➕ 23-1. 폼 설계 체크리스트

  1. 필수값과 선택값이 명확한가?
  2. 입력 예시가 필요한 필드에 placeholder나 도움말이 있는가?
  3. 프론트 유효성 검증이 있는가?
  4. 백엔드 검증도 반드시 있는가?
  5. 필드 에러와 전역 에러가 구분되는가?
  6. 제출 중 버튼 중복 클릭이 막히는가?
  7. 성공/실패 피드백이 명확한가?
  8. 모바일에서 입력하기 편한 input type을 사용하는가?

➕ 23-2. react-hook-form 체크리스트

  1. defaultValues가 적절히 설정되어 있는가?
  2. 수정 폼에서 서버 데이터 도착 후 reset을 사용하는가?
  3. zod schema와 TypeScript 타입이 연결되어 있는가?
  4. 서버 에러를 form.setError로 필드에 연결할 수 있는가?
  5. isSubmitting 또는 mutation.isPending으로 중복 제출을 막는가?
  6. 큰 폼은 FormProvider로 섹션을 나눴는가?
  7. dirty 상태가 필요한 폼에 이탈 방지를 고려했는가?
  8. 폼 제출 후 reset/close/invalidate 처리가 적절한가?

➕ 23-3. 관리자 폼 체크리스트

  1. 상품/배너/상담 상태 변경처럼 운영 영향이 큰 작업에 confirm이 있는가?
  2. 상태 변경 사유나 메모가 필요한 곳에 입력 필드가 있는가?
  3. 저장 후 목록이 갱신되는가?
  4. 권한 없는 사용자는 저장 버튼을 볼 수 없거나 비활성화되는가?
  5. 백엔드에서도 권한을 다시 검사하는가?
  6. 대량 작업은 영향 범위를 confirm에서 설명하는가?
  7. 실패 시 운영자가 무엇을 해야 할지 알 수 있는 메시지가 있는가?
  8. 검색 폼은 URL query string과 연결되어 있는가?

✅ 24. AI를 활용해 폼 UX를 설계할 때 질문법

  • AI에게 폼을 설계시킬 때는 단순히 “폼 만들어줘”보다, 사용자가 누구인지, 어떤 값을 입력하는지, 어떤 검증이 필요한지, 성공/실패 시 화면이 어떻게 바뀌는지 알려줘야 합니다.

➕ 24-1. 좋은 질문 예시

React + react-hook-form + zod + TanStack Query로 상담 신청 폼을 만들고 싶어.

상황:
1. 고객이 상품 상세 페이지에서 상담 신청 모달을 열어 신청함
2. 입력값은 이름, 전화번호, 상품 ID, 유입경로, visitorId임
3. 전화번호는 010으로 시작하는 11자리 숫자여야 함
4. 같은 이벤트/상품/전화번호 조합은 백엔드에서 중복 신청을 막음
5. 중복 신청 시 서버는 CONSULT_DUPLICATED 에러 코드를 반환함
6. 제출 중에는 버튼 중복 클릭을 막아야 함
7. 성공하면 완료 메시지를 보여주고 모달을 닫음
8. 실패하면 필드 에러 또는 전역 에러로 구분해서 표시함
9. 모바일 입력 UX가 좋아야 함

요청:
- zod schema
- react-hook-form 구조
- useMutation 연결
- 서버 에러 setError 처리
- 버튼 로딩/disabled 처리
- 성공/실패 메시지
- 모바일 input type
- 접근성 체크리스트
를 실무 기준으로 작성해줘.

➕ 24-2. AI 답변 검증 기준

  1. 프론트 검증만으로 중복 방지를 끝내지 않는가?
  2. 백엔드 에러 코드를 필드 에러와 연결하는가?
  3. 제출 중 중복 클릭을 막는가?
  4. 전화번호 정규화와 검증을 구분하는가?
  5. 성공 후 reset/close 처리 기준이 있는가?
  6. 실패 메시지가 사용자가 이해할 수 있게 작성되어 있는가?
  7. 모바일 input type과 접근성을 고려하는가?
  8. 현재 프로젝트 규모에 맞게 과하지 않은 구조인가?

📌 요약

  • 폼은 사용자가 값을 입력하고 제출하는 UI이며, 입력값 관리, 유효성 검증, 서버 제출, 에러 처리, 성공/실패 피드백까지 포함합니다.
  • 좋은 폼은 필수값과 선택값이 명확하고, 오류가 발생했을 때 어떤 값을 어떻게 고쳐야 하는지 알려줍니다.
  • 프론트 검증은 사용자 경험을 좋게 만들고, 백엔드 검증은 보안과 데이터 정합성을 지키는 최종 방어선입니다.
  • 복잡한 폼은 react-hook-form과 zod를 함께 사용하면 타입과 검증 규칙을 일관되게 관리할 수 있습니다.
  • 수정 폼은 서버 데이터가 비동기로 들어오기 때문에 reset으로 초기값을 갱신하는 패턴이 중요합니다.
  • 폼 제출은 TanStack Query의 useMutation과 연결하고, 제출 중에는 버튼을 비활성화해 중복 클릭을 막아야 합니다.
  • 서버 에러는 필드 에러와 전역 에러로 나눠 표시해야 하며, 중복 신청 같은 에러는 form.setError로 해당 필드에 연결할 수 있습니다.
  • 전화번호, 금액, 날짜처럼 입력 형식이 중요한 값은 화면 표시와 서버 전송값을 구분해 정규화하는 것이 좋습니다.
  • 관리자 화면에서는 상태 변경, 삭제, 권한 변경, 사전예약 오픈/종료처럼 운영 영향이 큰 작업에 confirm을 제공해야 합니다.
  • 검색 폼은 URL query string과 연결하고, 저장 폼은 mutation과 연결하는 식으로 성격을 분리하면 유지보수가 쉬워집니다.

0개의 댓글