C
React/Hooks/Lesson 11

SyntheticEvent — onChange/onSubmit 타입과 preventDefault 시점

25분·theory
이 챕터
3/5
TypeScript

SyntheticEvent — onChange/onSubmit 타입과 preventDefault 시점

💡 왜 배워야 할까요? — 매일 쓰는데 타입을 어림짐작

🎯 `onChange={(e) => ...}` 의 `e` 가 무엇인지 모르고 쓰면 `e.target.value` 가 any 가 되어 자동완성·오타 검증을 모두 잃습니다.
💼 React 의 이벤트 객체는 브라우저 `Event` 의 래퍼 (`SyntheticEvent`). 추가 기능 있지만 시그니처는 거의 같음.
`onChange`, `onClick`, `onSubmit`, `onKeyDown` — 각각 타입이 다르고, 대상 요소(`HTMLInputElement` vs `HTMLButtonElement`)도 타입에 들어갑니다.
🔗 `e.preventDefault()` 호출 시점을 놓치면 폼이 새로고침되거나 링크가 이동해버립니다.
📈 이벤트 위임은 React 가 알아서 합니다 — 모든 이벤트는 사실 root 에서 한 번에 처리.
🏢 실무에서는
폼 한 줄 만들 때마다 부닥치는 일. 5가지 타입을 외워두면 평생 안 헷갈립니다. `useState`·`useEffect` 처럼 매일 쓰니까 외우는 게 빠릅니다.

React 이벤트 5가지 핵심 타입

1. SyntheticEvent — 브라우저 이벤트의 래퍼

React 이벤트는 표준 Event 위에 cross-browser 호환 + 풀링 + 위임 기능을 더한 래퍼.

2. 자주 쓰는 5가지 시그니처

ts
// (a) onChange — input/textarea/select
(e: React.ChangeEvent<HTMLInputElement>) => void
(e: React.ChangeEvent<HTMLTextAreaElement>) => void
(e: React.ChangeEvent<HTMLSelectElement>) => void

// (b) onSubmit — form
(e: React.FormEvent<HTMLFormElement>) => void

// (c) onClick — button/div/...
(e: React.MouseEvent<HTMLButtonElement>) => void

// (d) onKeyDown — input/textarea
(e: React.KeyboardEvent<HTMLInputElement>) => void

// (e) onFocus / onBlur
(e: React.FocusEvent<HTMLInputElement>) => void

3. target vs currentTarget

tsx
<button onClick={(e) => {
  e.target;        // EventTarget — 실제 클릭이 일어난 요소 (자식일 수도)
  e.currentTarget; // HTMLButtonElement — 핸들러가 붙은 요소 (button 확정)
}}>
  <span>클릭</span>
</button>

span 을 클릭하면 e.target = span, e.currentTarget = button. 핸들러를 단 요소의 타입을 확실히 받으려면 currentTarget.

4. preventDefault — 호출 시점

tsx
<form onSubmit={(e) => {
  e.preventDefault();  // 새로고침 방지 — 첫 줄에
  // ... 폼 처리
}}>
tsx
<a href="/x" onClick={(e) => {
  if (modalOpen) e.preventDefault(); // 조건부
}}>

5. checkbox 는 value 가 아니라 checked

tsx
<input type="checkbox" onChange={(e) => setAgree(e.target.checked)} />

6. 이벤트 위임 — React 가 알아서

사용자가 <button onClick={...}> 라 적어도, 실제로는 root container 에 한 번만 listener 가 붙고 React 가 가상 트리를 거슬러 올라가며 핸들러 호출. 사용자는 이 디테일을 몰라도 됨.

💻 🅰️ 타입 없이 — e 가 any
// ❌ 타입 없이 — e.target 의 정체 모름
import { useState } from 'react';

export function LoginForm() {
  const [email, setEmail] = useState('');

  const onChange = (e) => {
    setEmail(e.target.value);     // 운 좋으면 OK
    setEmail(e.target.valeu);     // ❌ 오타 통과
    setEmail(e.target.checked);   // input 인지 checkbox 인지 모름
  };

  const onSubmit = (e) => {
    // preventDefault 빼먹기 쉬움
    console.log({ email });
    // → 새로고침 → 콘솔 지워짐
  };

  return (
    <form onSubmit={onSubmit}>
      <input onChange={onChange} />
      <button type="submit">로그인</button>
    </form>
  );
}
💻 🅱️ 타입 명시 — IDE 가 자동완성
// ✅ 이벤트 타입 명시
import { useState, type FormEvent, type ChangeEvent, type KeyboardEvent } from 'react';

export function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [remember, setRemember] = useState(false);

  // input 의 onChange
  const onChangeEmail = (e: ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
    // setEmail(e.target.valeu); ← ❌ TS 가 즉시 빨간 줄
  };

  // checkbox 도 ChangeEvent<HTMLInputElement> — 다만 e.target.checked
  const onChangeRemember = (e: ChangeEvent<HTMLInputElement>) => {
    setRemember(e.target.checked);
  };

  // form 의 onSubmit
  const onSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log({ email, password, remember });
  };

  // input 에서 Enter 키만 처리
  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') console.log('Enter');
  };

  // 공용 핸들러 — name 으로 분기
  const onChangeShared = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    if (name === 'email') setEmail(value);
    if (name === 'password') setPassword(value);
  };

  return (
    <form onSubmit={onSubmit}>
      <input name="email" value={email} onChange={onChangeShared} onKeyDown={onKeyDown} />
      <input name="password" type="password" value={password} onChange={onChangeShared} />
      <label>
        <input type="checkbox" checked={remember} onChange={onChangeRemember} />
        자동 로그인
      </label>
      <button type="submit">로그인</button>
    </form>
  );
}

💡 💡 React 이벤트 실전 5

1. 자주 쓰는 5가지만 외워라

  • input/textarea/select 의 onChange: ChangeEvent<HTMLInputElement>
  • form 의 onSubmit: FormEvent<HTMLFormElement>
  • button 의 onClick: MouseEvent<HTMLButtonElement>
  • input 의 onKeyDown: KeyboardEvent<HTMLInputElement>
  • focus/blur: FocusEvent<HTMLInputElement>

2. onSubmit 의 첫 줄은 항상 e.preventDefault()
빼먹으면 폼이 GET/POST 로 페이지 전체 새로고침.

3. checkbox 는 e.target.checked

tsx
onChange={(e) => setAgree(e.target.checked)}

4. e.currentTarget 이 안전
e.target 은 실제 클릭된 자식 요소(span 등) 일 수 있음. 핸들러를 단 요소의 타입은 currentTarget.

5. e.stopPropagation() 은 React 트리 안에서만
외부 라이브러리(jQuery 등) 가 document 에 listener 를 달았다면 React stopPropagation 으로 못 막음. 그땐 e.nativeEvent.stopImmediatePropagation().

⚡ 직접 실행해보기 — 이벤트 객체 구조

이벤트 객체에 어떤 정보가 들어있는지 console.log 로 확인.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가

확인 퀴즈

input 의 onChange 핸들러의 가장 정확한 TS 타입은?
💡 `React.ChangeEvent<HTMLInputElement>` 가 정확합니다. `e.target` 이 `HTMLInputElement` 로 추론되어 `value`, `checked`, `name` 등에 자동완성·타입 검증이 모두 적용됩니다. `Event` 는 브라우저 표준이고 React SyntheticEvent 가 아니라 부적합. `SyntheticEvent` 단독은 target 의 구체 타입을 알려주지 않습니다.
먼저 읽으면 좋은 개념: useReducer — Redux 의 기반
다음 추천: 커스텀 Hooks
SyntheticEvent — 이벤트 타입 - React