React에서 비동기 폼 검증은 어렵다 — 이를 해결하는 예측 가능한 방법
Source: Dev.to
비동기 폼 검증이 React에서 어려운 이유 – 예측 가능한 해결책
React에서 폼 검증을 구현할 때, 특히 서버와의 비동기 검증을 포함해야 할 경우, 복잡함에 부딪히기 쉽습니다. 이 글에서는 예측 가능한 방식으로 비동기 검증을 처리하는 방법을 단계별로 살펴보겠습니다.
🎯 목표
- 사용자 경험을 해치지 않으면서 비동기 검증을 수행한다.
- 코드 가독성과 유지보수성을 높인다.
- React Hook Form과 Yup(또는 Zod) 같은 스키마 기반 검증 라이브러리를 활용한다.
📦 사용된 라이브러리
| 라이브러리 | 용도 |
|---|---|
react-hook-form | 폼 상태 관리 및 기본 검증 |
@hookform/resolvers | Yup/Zod 스키마를 React Hook Form에 연결 |
yup (또는 zod) | 스키마 기반 동기/비동기 검증 |
axios | 서버 API 호출 (예시) |
Tip:
yup은validate메서드에async함수를 전달하면 비동기 검증을 자동으로 지원합니다.
🛠️ 기본 설정
npm install react-hook-form @hookform/resolvers yup axios
1️⃣ 스키마 정의 (Yup)
import * as yup from "yup";
const schema = yup.object().shape({
username: yup
.string()
.required("사용자 이름은 필수입니다.")
.min(3, "최소 3자 이상이어야 합니다.")
.test(
"unique-username",
"이미 사용 중인 사용자 이름입니다.",
async (value) => {
// 비동기 API 호출 예시
const response = await axios.get(`/api/users/exists?username=${value}`);
return !response.data.exists;
}
),
email: yup
.string()
.required("이메일은 필수입니다.")
.email("유효한 이메일 형식이 아닙니다.")
.test(
"unique-email",
"이미 등록된 이메일입니다.",
async (value) => {
const response = await axios.get(`/api/users/exists?email=${value}`);
return !response.data.exists;
}
),
password: yup
.string()
.required("비밀번호는 필수입니다.")
.min(8, "비밀번호는 최소 8자여야 합니다."),
});
핵심:
test메서드 안에서async함수를 반환하면 Yup이 자동으로 Promise를 처리합니다.
2️⃣ React Hook Form에 스키마 연결
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
function RegisterForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: yupResolver(schema),
mode: "onBlur", // 또는 "onChange"
});
const onSubmit = async (data) => {
// 실제 회원가입 로직
await axios.post("/api/users/register", data);
alert("회원가입이 완료되었습니다!");
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* ... 입력 필드 ... */}
</form>
);
}
주요 포인트
| 옵션 | 설명 |
|---|---|
mode: "onBlur" | 사용자가 필드에서 포커스를 뗄 때 검증을 실행합니다. 비동기 호출을 최소화할 수 있습니다. |
isSubmitting | 폼이 제출 중일 때 버튼 비활성화 등에 활용합니다. |
errors | 각 필드별 에러 메시지를 UI에 표시합니다. |
3️⃣ UI에 에러 표시하기
<input
type="text"
{...register("username")}
placeholder="사용자 이름"
/>
{errors.username && <p className="error">{errors.username.message}</p>}
<input
type="email"
{...register("email")}
placeholder="이메일"
/>
{errors.email && <p className="error">{errors.email.message}</p>}
<input
type="password"
{...register("password")}
placeholder="비밀번호"
/>
{errors.password && <p className="error">{errors.password.message}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "처리 중…" : "회원가입"}
</button>
4️⃣ 비동기 검증 최적화 팁
-
디바운스(debounce) 적용
- 사용자가 입력을 멈춘 뒤 일정 시간(
300ms정도) 후에 API 호출을 수행하면 불필요한 요청을 줄일 수 있습니다. lodash.debounce혹은useDebounce커스텀 훅을 활용하세요.
- 사용자가 입력을 멈춘 뒤 일정 시간(
-
캐시 활용
- 이미 검증된 값은 메모리(예:
Map)에 저장해 두고, 동일한 값에 대해 다시 호출하지 않도록 합니다.
- 이미 검증된 값은 메모리(예:
-
취소 토큰(Cancel Token) 사용
- 사용자가 입력을 바꾸면 이전 요청을 취소해 레이스 컨디션을 방지합니다.
axios.CancelToken혹은AbortController를 사용하세요.
- 사용자가 입력을 바꾸면 이전 요청을 취소해 레이스 컨디션을 방지합니다.
import { useRef } from "react";
function useAbortableFetch() {
const controllerRef = useRef(null);
const fetch = async (url) => {
if (controllerRef.current) {
controllerRef.current.abort(); // 이전 요청 취소
}
controllerRef.current = new AbortController();
const response = await fetch(url, {
signal: controllerRef.current.signal,
});
return response.json();
};
return fetch;
}
📊 전체 흐름 요약
- 스키마 정의 → Yup에 동기·비동기 규칙을 모두 선언.
- React Hook Form에
yupResolver연결 → 폼 상태와 검증 로직을 자동으로 동기화. - UI에서
register,errors,isSubmitting을 사용해 입력과 피드백을 관리. - 옵션(
mode,debounce,cache,abort)을 통해 비동기 호출을 최적화.
🏁 마무리
React에서 비동기 폼 검증은 복잡해 보이지만, Yup + React Hook Form 조합을 사용하면 구조화된 스키마와 자동 비동기 처리 덕분에 깔끔하게 구현할 수 있습니다.
- 예측 가능성: 검증 로직이 스키마 안에 모두 모여 있어 어디서든 재사용 가능.
- 유연성:
test메서드 안에 자유롭게 비동기 API 호출을 넣을 수 있음. - 성능:
mode, 디바운스, 캐시, 취소 토큰을 적절히 활용하면 불필요한 네트워크 요청을 최소화.
다음 단계: 프로젝트에 적용해보고, 필요에 따라 Zod 혹은 Superstruct 같은 다른 스키마 라이브러리로 교체해 보세요.
Happy coding! 🚀
핵심 문제: 검증이 결정론적이지 않음
대부분의 폼 솔루션은 상태 스냅샷이 아니라 이벤트를 기반으로 검증합니다.
예시 비동기 검증기
async function validateEmail(value: string) {
return api.checkEmailAvailability(value);
}
사용자가 빠르게 입력한다고 가정해 보세요:
taken@example.com을 입력 → 비동기 요청 A 시작john@example.com으로 변경 → 비동기 요청 B 시작- 요청 A가 요청 B보다 나중에 완료 ❌
UI에 “이메일이 이미 사용 중입니다”라는 잘못된 메시지가 표시됩니다.
이전 비동기 결과가 최신 입력을 덮어쓰는 전형적인 레이스 컨디션이 발생합니다.
문제 #1: 비동기 검증 레이스 조건
목표: 최신 검증 결과만이 의미를 갖도록 합니다.
많은 폼 라이브러리:
- 비동기 검증 실행을 추적하지 않음
- 검증을 안정적인 값 스냅샷에 연결하지 않음
- 오래된 결과가 승리하도록 허용
올바른 솔루션이 해야 할 일
- 비동기 검증 시도를 추적
- 오래된 비동기 결과 무시
- 항상 일관된 값 스냅샷에 대해 검증
이러한 조치가 없으면 비동기 검증은 절대 예측 가능하지 않습니다.
문제 #2: 교차 필드 검증은 취약합니다
실제 폼에서는 여러 필드를 함께 검증해야 할 경우가 많습니다. 예시:
- 비밀번호 확인은 비밀번호와 일치해야 함
- 종료 날짜는 시작 날짜보다 이후여야 함
- 특정 필드는 다른 필드가 활성화된 경우에만 필수
일반적인 접근 방식은 다른 필드를 감시하거나, 수동으로 재검증을 수행하거나, 숨겨진 의존성을 두는 것인데, 이는 디버깅이 어려운 암묵적인 동작을 초래합니다.
명시적인 교차 필드 검증
register("confirmPassword", {
validate: (value, values) =>
value !== values.password ? "Passwords do not match" : undefined,
});
- 마법이 없음
- 자동 재검증 없음
- 숨겨진 의존성 없음 – 읽기 쉬운 로직만 존재합니다.
Problem #3: 제출은 변경과 다르게 동작합니다
“변경 시에는 검증이 작동하지만, 제출 시에는 다르게 동작합니다.”
이는 많은 라이브러리에서 다음과 같은 이유로 발생합니다:
- 터치된 필드만 검증
- 제출 시에는 터치되지 않은 종속 필드를 건너뜀
결과: 예상치 못한 제출 시 오류가 발생합니다.
이를 해결하는 간단한 규칙
제출 시, 등록된 모든 필드를 검증합니다. 언제나. 이렇게 하면 제출 동작이 예측 가능하고 올바르게 됩니다.
솔루션: 예측 가능하고 async‑first 폼 엔진
- Validation timing is explicit (
change|blur|submit) - Async validation is race‑condition safe
- Validation always runs against value snapshots
- Cross‑field validation is explicit
- No automatic dependency re‑validation
Formora를 사용한 실제 예시
비동기 이메일 검증 + 비밀번호 확인
const form = useForm({
initialValues: {
email: "",
password: "",
confirmPassword: "",
},
validateOn: "change",
asyncDebounceMs: 500,
});
{
await new Promise((r) => setTimeout(r, 300));
if (value.includes("taken")) return "Email already taken";
},
})}
/>
value !== values.password ? "Passwords do not match" : undefined,
})}
/>
이 예시가 제공하는 장점:
- 오래된 오류가 표시되지 않는 비동기 검증
- 명시적인 필드 간 로직
- 예측 가능한 제출 동작
- 깔끔하고 디버깅하기 쉬운 코드
다른 개발자들이 Formora를 활용할 수 있는 이유
Formora는 모든 폼 라이브러리를 대체하려는 것이 아니라, 다음과 같은 상황에서 빛을 발합니다:
- 올바른 비동기 동작
- 명시적인 검증 로직
- 타입 안전하고 예측 가능한 상태
- 실제 애플리케이션에서 디버깅 가능한 폼 동작
앱에 비동기 검증, 필드 간 규칙, 혹은 복잡한 제출 로직이 있다면, Formora는 견고하고 예측 가능한 기반을 제공합니다.
최종 생각
폼은 본질적으로 복잡해서가 아니라, 검증 정확성이 종종 무시되기 때문에 어려워집니다. 비동기 로직, 필드 간 관계, 실제 사용자 행동은 마법이 아니라 예측 가능성을 요구합니다. Formora는 이러한 문제들을 명시적이고 신뢰할 수 있게 해결하기 위해 만들어졌습니다.
유용한 링크
- GitHub:
- npm: