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 호출 감소
모바일 입력 실수 감소
전환율 개선
관리자 업무 속도 개선
- 백엔드가 검증하니까 프론트 검증이 필요 없다는 말도 틀립니다.
- 둘 다 필요합니다. 역할이 다릅니다.
- 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>;
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으로 보내는 식입니다.
- 모바일에서 폼 입력은 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>
<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를 사용할 수 있습니다.
- 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. 폼 설계 체크리스트
- 필수값과 선택값이 명확한가?
- 입력 예시가 필요한 필드에 placeholder나 도움말이 있는가?
- 프론트 유효성 검증이 있는가?
- 백엔드 검증도 반드시 있는가?
- 필드 에러와 전역 에러가 구분되는가?
- 제출 중 버튼 중복 클릭이 막히는가?
- 성공/실패 피드백이 명확한가?
- 모바일에서 입력하기 편한 input type을 사용하는가?
- defaultValues가 적절히 설정되어 있는가?
- 수정 폼에서 서버 데이터 도착 후 reset을 사용하는가?
- zod schema와 TypeScript 타입이 연결되어 있는가?
- 서버 에러를 form.setError로 필드에 연결할 수 있는가?
- isSubmitting 또는 mutation.isPending으로 중복 제출을 막는가?
- 큰 폼은 FormProvider로 섹션을 나눴는가?
- dirty 상태가 필요한 폼에 이탈 방지를 고려했는가?
- 폼 제출 후 reset/close/invalidate 처리가 적절한가?
➕ 23-3. 관리자 폼 체크리스트
- 상품/배너/상담 상태 변경처럼 운영 영향이 큰 작업에 confirm이 있는가?
- 상태 변경 사유나 메모가 필요한 곳에 입력 필드가 있는가?
- 저장 후 목록이 갱신되는가?
- 권한 없는 사용자는 저장 버튼을 볼 수 없거나 비활성화되는가?
- 백엔드에서도 권한을 다시 검사하는가?
- 대량 작업은 영향 범위를 confirm에서 설명하는가?
- 실패 시 운영자가 무엇을 해야 할지 알 수 있는 메시지가 있는가?
- 검색 폼은 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 답변 검증 기준
- 프론트 검증만으로 중복 방지를 끝내지 않는가?
- 백엔드 에러 코드를 필드 에러와 연결하는가?
- 제출 중 중복 클릭을 막는가?
- 전화번호 정규화와 검증을 구분하는가?
- 성공 후 reset/close 처리 기준이 있는가?
- 실패 메시지가 사용자가 이해할 수 있게 작성되어 있는가?
- 모바일 input type과 접근성을 고려하는가?
- 현재 프로젝트 규모에 맞게 과하지 않은 구조인가?
📌 요약
- 폼은 사용자가 값을 입력하고 제출하는 UI이며, 입력값 관리, 유효성 검증, 서버 제출, 에러 처리, 성공/실패 피드백까지 포함합니다.
- 좋은 폼은 필수값과 선택값이 명확하고, 오류가 발생했을 때 어떤 값을 어떻게 고쳐야 하는지 알려줍니다.
- 프론트 검증은 사용자 경험을 좋게 만들고, 백엔드 검증은 보안과 데이터 정합성을 지키는 최종 방어선입니다.
- 복잡한 폼은
react-hook-form과 zod를 함께 사용하면 타입과 검증 규칙을 일관되게 관리할 수 있습니다.
- 수정 폼은 서버 데이터가 비동기로 들어오기 때문에
reset으로 초기값을 갱신하는 패턴이 중요합니다.
- 폼 제출은 TanStack Query의
useMutation과 연결하고, 제출 중에는 버튼을 비활성화해 중복 클릭을 막아야 합니다.
- 서버 에러는 필드 에러와 전역 에러로 나눠 표시해야 하며, 중복 신청 같은 에러는
form.setError로 해당 필드에 연결할 수 있습니다.
- 전화번호, 금액, 날짜처럼 입력 형식이 중요한 값은 화면 표시와 서버 전송값을 구분해 정규화하는 것이 좋습니다.
- 관리자 화면에서는 상태 변경, 삭제, 권한 변경, 사전예약 오픈/종료처럼 운영 영향이 큰 작업에 confirm을 제공해야 합니다.
- 검색 폼은 URL query string과 연결하고, 저장 폼은 mutation과 연결하는 식으로 성격을 분리하면 유지보수가 쉬워집니다.