지난 시간에는 state와 useState를 배우면서 자연스럽게 onClick이라는 이벤트 핸들러를 사용했습니다. 그 자체로도 동작은 했지만, 리액트의 이벤트 처리 방식에는 몇 가지 알아둘 것들이 있습니다. 이번 시간에는 이벤트 핸들링을 본격적으로 다뤄보겠습니다.
리액트에서 이벤트 처리하는 법
리액트에서 이벤트는 JSX 속성으로 핸들러 함수를 전달하는 방식으로 처리합니다. HTML의 onclick이 아니라 camelCase의 **onClick**을 사용한다는 점만 다릅니다.
function App() {
function handleClick() {
alert('버튼이 클릭되었습니다!');
}
return <button onClick={handleClick}>클릭</button>;
}
export default App;여기서 한 가지 자주 하는 실수가 있습니다. 함수를 호출(handleClick())하면 안 되고, 함수 자체(handleClick)를 전달해야 합니다.
<button onClick={handleClick()}>클릭</button>이렇게 쓰면 컴포넌트가 렌더링되는 즉시 handleClick()이 실행되어 alert이 떠버립니다. 게다가 onClick에는 handleClick의 반환값(undefined)이 등록되므로 진짜 클릭에는 아무 일도 일어나지 않습니다.
<button onClick={handleClick}>클릭</button>함수 참조만 넘기고 호출은 리액트가 클릭 시점에 합니다. 이 차이를 꼭 기억해두세요.
인라인 핸들러
간단한 핸들러는 아예 JSX 안에 화살표 함수로 직접 작성하기도 합니다.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
카운트: {count}
</button>
);
}() => setCount(count + 1)은 클릭 시점에 호출되는 익명 함수입니다. 한 줄짜리 단순한 핸들러는 이렇게 인라인으로 두는 게 편하고, 로직이 길어지면 별도 함수로 빼는 게 가독성에 좋습니다. 정해진 규칙은 없고 팀이나 본인의 취향에 따릅니다.
함수에 인자 전달하기
핸들러 함수에 인자를 넘겨야 할 때는 인라인 화살표 함수로 감싸야 합니다.
function App() {
function handleClick(name) {
alert(`${name}님 안녕하세요!`);
}
return (
<>
<button onClick={() => handleClick('철수')}>철수에게 인사</button>
<button onClick={() => handleClick('영희')}>영희에게 인사</button>
</>
);
}다음과 같이 쓰면 안 됩니다.
<button onClick={handleClick('철수')}>...</button>위에서 봤듯이 이건 렌더링 즉시 호출되어 버립니다. 인자를 넘기려면 반드시 화살표 함수로 한 번 감싸서 "클릭될 때 호출하라"는 의미를 만들어야 합니다.
이벤트 객체
이벤트 핸들러는 첫 번째 매개변수로 이벤트 객체를 받습니다. 이 객체에는 어떤 요소에서 어떤 이벤트가 일어났는지에 대한 정보가 담겨 있습니다.
import { useState } from 'react';
function InputDemo() {
const [text, setText] = useState('');
function handleChange(e) {
setText(e.target.value);
}
return (
<div>
<input type="text" value={text} onChange={handleChange} />
<p>입력값: {text}</p>
</div>
);
}
export default InputDemo;매개변수 이름은 보통 e 또는 event로 짓습니다. e.target은 이벤트가 발생한 DOM 요소이고, e.target.value로 입력값을 꺼낼 수 있습니다.
리액트의 이벤트 객체는 정확히는 브라우저의 네이티브 이벤트가 아니라 **합성 이벤트(SyntheticEvent)**입니다. 리액트가 모든 브라우저에서 동일하게 동작하도록 한 번 감싼 객체죠. API는 네이티브 이벤트와 거의 같아서 평소에는 신경 쓸 일이 없습니다. e.preventDefault(), e.target, e.key 같은 익숙한 속성/메서드를 그대로 사용할 수 있습니다.
자주 쓰는 이벤트들
가장 많이 사용하는 이벤트 핸들러를 정리하면:
onClick— 클릭onChange— 입력 요소(input, textarea, select)의 값이 바뀔 때onSubmit— 폼이 제출될 때onKeyDown/onKeyUp— 키보드 키가 눌리거나 떼질 때onMouseEnter/onMouseLeave— 마우스가 요소 위에 들어오거나 벗어날 때onFocus/onBlur— 포커스 진입/해제
각각의 이벤트는 그에 맞는 정보를 이벤트 객체에 담아줍니다. onChange라면 e.target.value를, onKeyDown이라면 e.key(누른 키 이름)를 보면 됩니다.
function SearchBox() {
function handleKeyDown(e) {
if (e.key === 'Enter') {
alert('엔터 키가 눌렸습니다');
}
}
return <input type="text" onKeyDown={handleKeyDown} />;
}기본 동작 막기
브라우저는 어떤 이벤트에 대해 기본 동작을 가지고 있습니다. 폼이 제출되면 페이지가 새로고침되고, 링크를 클릭하면 페이지가 이동합니다. 이 기본 동작을 막으려면 이벤트 객체의 **preventDefault()**를 호출합니다.
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
function handleSubmit(e) {
e.preventDefault(); // 폼 제출로 인한 페이지 새로고침을 막음
console.log('제출된 이메일:', email);
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">로그인</button>
</form>
);
}
export default LoginForm;폼은 제출 버튼을 누르거나 입력창에서 엔터를 치면 자동으로 onSubmit이 발화됩니다. 이때 e.preventDefault()가 없으면 브라우저가 페이지를 새로고침해버려서, 우리가 작성한 처리 로직이 의미를 잃습니다.
이벤트 핸들러를 props로 전달하기
이벤트 핸들러도 그냥 함수일 뿐이므로, props로 자식 컴포넌트에 전달할 수 있습니다. 이 패턴은 자식이 일으킨 이벤트를 부모가 처리해야 할 때 매우 자주 사용됩니다.
function Button({ label, onClick }) {
return (
<button onClick={onClick} style={{ padding: '8px 16px' }}>
{label}
</button>
);
}
export default Button;import Button from './Button';
function App() {
function handleSave() {
alert('저장되었습니다');
}
function handleCancel() {
alert('취소되었습니다');
}
return (
<>
<Button label="저장" onClick={handleSave} />
<Button label="취소" onClick={handleCancel} />
</>
);
}부모는 어떤 행동을 할지(handler)를 전달하고, 자식은 언제 그 행동이 일어나는지(클릭)를 알려주는 구조입니다. 핸들러 prop의 이름은 관례상 on으로 시작하게 짓습니다 (onClick, onSave, onItemSelect ...).
핸들러 안에서 state 변경하기
지난 글에서 본 패턴인데, 이벤트 핸들러 안에서 state를 변경하는 것이 가장 흔한 패턴입니다.
import { useState } from 'react';
function Toggle() {
const [isOn, setIsOn] = useState(false);
function handleToggle() {
setIsOn(prev => !prev);
}
return (
<div>
<p>현재 상태: {isOn ? 'ON' : 'OFF'}</p>
<button onClick={handleToggle}>토글</button>
</div>
);
}
export default Toggle;이벤트가 일어나면 → 핸들러가 실행되고 → state가 갱신되고 → 화면이 다시 그려진다. 이 흐름이 리액트 앱의 가장 기본적인 패턴입니다.
직접 해보기
간단한 입력 폼을 만들어보겠습니다. 사용자가 이름과 메시지를 입력하고 "추가"를 누르면 화면 아래에 메시지가 추가되는 컴포넌트입니다 (다음 글 #7, #8에서 이걸 더 확장합니다).
src/MessageForm.jsx를 만듭니다.
import { useState } from 'react';
function MessageForm() {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
const [lastSubmitted, setLastSubmitted] = useState(null);
function handleSubmit(e) {
e.preventDefault();
if (!name || !message) return;
setLastSubmitted({ name, message });
setName('');
setMessage('');
}
return (
<div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
placeholder="이름"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div style={{ marginTop: '8px' }}>
<input
type="text"
placeholder="메시지"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<button type="submit" style={{ marginTop: '8px' }}>추가</button>
</form>
{lastSubmitted && (
<p style={{ marginTop: '12px' }}>
마지막 입력: <strong>{lastSubmitted.name}</strong> — {lastSubmitted.message}
</p>
)}
</div>
);
}
export default MessageForm;src/App.jsx에 연결합니다.
import MessageForm from './MessageForm';
function App() {
return (
<>
<h1>메시지 폼</h1>
<MessageForm />
</>
);
}
export default App;이름과 메시지를 입력하고 엔터나 "추가" 버튼을 눌러보세요. 마지막에 제출된 값이 아래에 표시되고, 입력 필드는 비워집니다. e.preventDefault()를 빼면 폼이 새로고침되어 입력값이 사라지는 것도 한 번 실험해보세요.
위 코드에서 lastSubmitted && (...) 부분이 처음 보이실 텐데, 이것은 조건부 렌더링입니다. 값이 있으면 보여주고 없으면 안 보여주는 패턴이죠. 다음 글인 #7에서 자세히 다루겠습니다.
마무리
이번 글에서는 리액트의 이벤트 처리를 살펴봤습니다. 핵심은:
onClick처럼 camelCase 속성으로 핸들러를 등록한다- 함수를 호출하지 말고 전달한다 (
{handleClick}이지{handleClick()}이 아니다) - 인자를 넘기려면 화살표 함수로 감싼다
- 핸들러는 첫 매개변수로 이벤트 객체(
e)를 받는다 e.preventDefault()로 브라우저 기본 동작을 막을 수 있다- 핸들러도 props로 자식에 내려보낼 수 있다 (
on으로 시작하는 이름)
또한 MessageForm 예제에서 슬쩍 본 {lastSubmitted && ...} 패턴이 바로 다음 주제로 이어집니다. 다음 글인 "리액트 기초 강좌 #7 조건부 렌더링"에서는 화면의 일부를 상태에 따라 보였다 안 보였다 하거나 다른 모습으로 바꾸는 다양한 패턴을 정리해보겠습니다.