React 교재 · 기초 7편

React 이벤트 핸들링 — 합성 이벤트와 패턴

onClick·onChange 가 어떻게 작동하는지, 인자 전달은 어떻게, 그리고 흔한 함정.

버튼 클릭 위에 퍼지는 파동 일러스트 — React 이벤트 컨셉

4편에서 onClick 기본 패턴을 짧게 봤다. 이번 7편은 본격적으로 — React 가 왜 합성 이벤트 (SyntheticEvent) 시스템을 따로 만들었는지, preventDefault·stopPropagation 사용법, 핸들러에 인자를 깨끗이 넘기는 4가지 패턴까지.

1. 합성 이벤트 — DOM 이벤트의 wrapper

React 의 onClick·onChange 가 호출하는 함수에 들어오는 첫 인자는 SyntheticEvent — 브라우저 native 이벤트를 React 가 한 번 감싼 객체.

function Input() { const handleChange = (e) => { console.log(e.target.value); // DOM 과 동일하게 사용 가능 console.log(e.type); // 'change' console.log(e.nativeEvent); // 진짜 브라우저 이벤트 }; return <input onChange={handleChange} />; }

왜 wrapper 인가 — 브라우저 호환성. IE·Safari·Chrome 의 이벤트 차이를 React 가 흡수해 모든 브라우저에서 같은 API. 2026 년엔 IE 가 죽었지만 모바일 브라우저 미묘한 차이는 여전. 또 React 18 부터는 root 단일 listener + 이벤트 위임으로 메모리 효율도 잡았다.

TypeScript 사용자 — 이벤트 타입은 React.ChangeEvent<HTMLInputElement> · React.MouseEvent<HTMLButtonElement> 식으로 명시. IDE 자동완성이 강력. e.target.value 가 string 인지 number 인지도 정확히 알려준다.

2. preventDefault · stopPropagation — 가장 자주 쓰는 두 메서드

두 가지 — preventDefault (브라우저 기본 동작 막기) 와 stopPropagation (부모 이벤트로 전파 막기). 5편 차이.

function LoginForm() { const handleSubmit = (e) => { e.preventDefault(); // form 의 기본 동작 (페이지 새로고침) 차단 // ... fetch 로 데이터 전송 }; return <form onSubmit={handleSubmit}>...</form>; } function NestedClick() { return ( <div onClick={() => alert('부모')}> <button onClick={(e) => { e.stopPropagation(); // 자식 클릭이 부모로 안 올라감 alert('자식'); }}> 클릭 </button> </div> ); }

실전 빈도 — preventDefault 가 압도적으로 많다. form 제출, anchor 태그 동작 차단, drag 기본 동작 등. stopPropagation 은 모달 외부 클릭 닫기 같은 특수 케이스.

3. 인자 전달 — 4가지 패턴

핸들러에 추가 정보 (item ID 등) 를 넘기는 방법. 4편에서 인라인 화살표 패턴을 봤지만 옵션이 더 있다.

function TodoList({ todos }) { // 패턴 1 — 인라인 화살표 (가장 흔함) return todos.map(todo => ( <button onClick={() => deleteTodo(todo.id)}>삭제</button> )); } // 패턴 2 — data 속성 + 핸들러 (재렌더 최적화) function TodoList2({ todos }) { const handleClick = (e) => { deleteTodo(Number(e.currentTarget.dataset.id)); }; return todos.map(todo => ( <button data-id={todo.id} onClick={handleClick}>삭제</button> )); } // 패턴 3 — 고차 함수 (currying) const makeDelete = (id) => () => deleteTodo(id); <button onClick={makeDelete(todo.id)}>삭제</button> // 패턴 4 — 별도 컴포넌트로 분리 (가장 깔끔) function TodoItem({ todo, onDelete }) { return <button onClick={() => onDelete(todo.id)}>삭제</button>; }

실전 선택 — 항목 수가 적으면 패턴 1, 수백 개 이상이면 패턴 2 (인라인 함수가 매 렌더마다 새 함수 객체 생성 → 자식 memo 가 깨짐. 16편에서 다시 등장). 패턴 4 가 가독성·재사용 둘 다 우위.

4. 흔한 함정 — 핸들러 안에서 state 옛 값

6편에서 봤듯 setter 는 비동기. 이벤트 핸들러 안에서 같은 state 를 여러 번 갱신할 때 자주 어긋난다.

function Counter() { const [count, setCount] = useState(0); const handleDouble = () => { setCount(count + 1); // count=0 → 1 예약 setCount(count + 1); // count=0 (아직 안 바뀜) → 1 예약 // 결과: 1 (2 가 아님) }; // 해결: 함수형 업데이트 const handleDoubleFix = () => { setCount(prev => prev + 1); // 0 → 1 setCount(prev => prev + 1); // 1 → 2 ✅ }; }
비동기 핸들러 안에서onClickasync 함수라면, await 후의 코드에서도 count 는 클릭 시점의 옛 값. setCount(prev => prev + 1) 함수형 패턴이나 useRef 로 최신 값 추적 필요. 11편 useEffect 의 closure 함정과 같은 뿌리.

이벤트 핸들링이 손에 잡히면 React 의 "사용자 액션 → state 변경 → UI 재렌더" 사이클이 완성된다. 8편에서는 그 state 가 다양해질 때 UI 를 조건부로 그리는 패턴으로 들어간다.

다음 글

React 교재 8편 — 조건부 렌더링. 삼항 · && · null 반환 + 0 표시 함정 + early return.

© 2026 주나이테크(주) @JUNAITECH