React 실무 가이드 · Part 4

이벤트와 폼

사용자 입력을 받아 처리하는 실무 패턴 익히기

작성 기준2026년 7월버전과 지원 현황은 이후 달라질 수 있으니 공식 문서를 함께 확인하세요.

이 파트에서 다루는 내용

이벤트 처리제어와 비제어폼 제출검증과 오류 표시
01

이벤트 핸들러는 함수를 넘깁니다

onClick에는 실행 결과가 아니라 함수 자체를 넘겨야 합니다. 괄호를 붙이면 렌더링 시점에 바로 실행됩니다.

기본 동작 막기
preventDefault

폼 제출이나 링크 이동의 기본 동작을 막을 때 씁니다. 폼에서 페이지가 새로고침되는 것을 막는 용도로 가장 많이 사용합니다.

이벤트 버블링
전파

자식에서 발생한 이벤트가 부모로 올라갑니다. 목록 행 클릭과 행 안의 버튼 클릭이 겹칠 때 전파를 멈춰야 합니다.

자주 하는 실수jsx
// 잘못: 렌더링될 때 즉시 실행됩니다
<button onClick={handleDelete()}>삭제</button>

// 올바름: 함수를 넘깁니다
<button onClick={handleDelete}>삭제</button>

// 인자가 필요하면 화살표 함수로 감쌉니다
<button onClick={() => handleDelete(order.id)}>삭제</button>

첫 번째 코드는 클릭하지 않았는데도 삭제가 실행되고, 그 결과로 다시 렌더링되면서 무한 반복에 빠질 수 있습니다.

02

제어 컴포넌트가 기본입니다

입력값을 state로 관리하고 화면에 그 값을 표시하는 방식을 제어 컴포넌트라고 합니다. 입력값이 항상 state와 일치하므로 예측이 쉽습니다.

반대로 DOM이 값을 갖게 두고 필요할 때만 읽는 방식이 비제어 컴포넌트입니다. 입력이 많은 화면에서 렌더링 부담이 적습니다.

제어 컴포넌트
권장 기본값
  • 값이 state에 있어 언제든 확인·조작 가능
  • 입력 중 실시간 검증과 형식 변환이 쉬움
  • 입력할 때마다 렌더링이 발생
비제어 컴포넌트
제한적 사용
  • DOM이 값을 보관하고 제출 시점에 읽음
  • 입력이 아주 많은 폼에서 유리
  • 파일 입력은 비제어로만 다룹니다
흔한 경고
주의

value에 undefined를 넣었다가 나중에 값을 넣으면 비제어에서 제어로 바뀌었다는 경고가 나옵니다. 초기값을 빈 문자열로 둡니다.

여러 입력을 한 state로 관리jsx
function OrderForm({ onSubmit }) {
  const [form, setForm] = useState({ receiver: "", phone: "", memo: "" });

  const handleChange = (event) => {
    const { name, value } = event.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="receiver" value={form.receiver} onChange={handleChange} />
      <input name="phone" value={form.phone} onChange={handleChange} />
      <textarea name="memo" value={form.memo} onChange={handleChange} />
    </form>
  );
}

input의 name 속성과 state의 키 이름을 맞추면 핸들러 하나로 여러 입력을 처리할 수 있습니다.

03

폼 제출에서 놓치기 쉬운 것들

폼은 단순해 보이지만 실무에서 문제가 자주 생깁니다. 특히 중복 제출과 오류 처리를 빠뜨리는 경우가 많습니다.

중복 제출
가장 흔한 사고

버튼을 빠르게 두 번 누르면 요청이 두 번 갑니다. 주문이나 결제에서는 실제 피해로 이어집니다.

오류 메시지 위치
사용성

어떤 필드가 왜 틀렸는지 해당 입력 옆에 표시합니다. 상단에 뭉뚱그려 보여 주면 사용자가 찾지 못합니다.

검증 시점
판단

입력 중 즉시 검증하면 아직 다 치지 않았는데 오류가 뜹니다. 처음에는 포커스가 빠질 때 검증하고, 한 번 오류가 난 필드만 입력 중 재검증하는 방식이 무난합니다.

서버 검증은 별도
보안

화면 검증은 사용성을 위한 것이지 보안 장치가 아닙니다. 서버에서 반드시 다시 검증합니다. Spring Boot 코스 Part 5와 이어집니다.

제출 처리 기본형jsx
function OrderForm() {
  const [form, setForm] = useState({ receiver: "", phone: "" });
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (event) => {
    event.preventDefault();          // 새로고침 방지
    if (isSubmitting) return;        // 중복 제출 방지

    const nextErrors = validate(form);
    setErrors(nextErrors);
    if (Object.keys(nextErrors).length > 0) return;

    setIsSubmitting(true);
    try {
      await createOrder(form);
      // 성공 처리
    } catch (error) {
      setErrors({ form: "주문 처리에 실패했습니다. 잠시 후 다시 시도해 주세요." });
    } finally {
      setIsSubmitting(false);        // 실패해도 반드시 해제합니다
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* 입력 필드들 */}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "처리 중..." : "주문하기"}
      </button>
    </form>
  );
}

finally에서 상태를 해제하는 부분이 중요합니다. 실패했을 때 버튼이 계속 비활성 상태로 남으면 사용자가 다시 시도할 수 없습니다.

04

폼이 복잡해지면 라이브러리를 검토합니다

필드가 많아지고 검증 규칙이 늘어나면 직접 관리하는 코드가 빠르게 복잡해집니다. 이 시점에는 폼 전용 라이브러리를 검토할 만합니다.

다만 도입 자체가 목적이 되면 안 됩니다. 입력 세 개짜리 폼에 라이브러리를 붙이면 오히려 코드가 늘어납니다.

  • 필드가 열 개를 넘고 필드 간 의존 규칙이 있다면 검토합니다.
  • 같은 형태의 폼이 여러 화면에 반복되면 공통 처리 이점이 큽니다.
  • 간단한 폼은 useState로 충분합니다.
버전

버전별 참고

본문은 React 19 기준입니다. 18과 달라지는 부분은 아래에 정리합니다.

  • React 19에서는 form의 action에 함수를 직접 넘기고, 제출 상태와 낙관적 업데이트를 다루는 훅이 추가됐습니다. 위 예제의 수동 처리 상당 부분을 대체할 수 있습니다.
  • 다만 React 18 프로젝트에서는 사용할 수 없습니다. 이 파트의 기본 패턴은 두 버전 모두에서 동작합니다.
  • 새 프로젝트를 19로 시작한다면 공식 문서의 폼 관련 훅을 함께 확인하는 편이 좋습니다.
체크

이 파트 완료 기준