간단한 이벤트 처리하기
우리는 지금까지 리액트 컴포넌트를 만들고(App.jsx, UserProfile.jsx, ToggleSwitch.jsx), props로 데이터를 전달하며(UserProfile.jsx), state를 통해 컴포넌트 내부의 동적인 데이터를 관리(ToggleSwitch.jsx)하는 방법을 배웠습니다.
이제 웹 애플리케이션의 핵심인 사용자와의 상호작용을 구현해 볼 차례입니다.
사용자가 버튼을 클릭하거나, 입력 필드에 텍스트를 입력하거나, 마우스를 움직이는 등의 행위를 우리는 이벤트(Event)라고 부릅니다. 리액트에서는 이러한 이벤트를 어떻게 감지하고 처리하는지 이번 장에서 자세히 알아보겠습니다.
리액트 이벤트 시스템 개요
이벤트는 사용자 행동을 핸들러 함수로 받아 상태 변경이나 기본 동작 제어로 이어집니다. 먼저 이 흐름을 잡고 세부 예제를 보겠습니다.
리액트의 이벤트 시스템은 브라우저의 원시(Native) 이벤트 시스템과 유사하지만, 몇 가지 중요한 차이점이 있습니다.
- JSX 내부에서 이벤트 핸들러 정의: HTML에서
onclick="myFunction()"과 같이 속성으로 이벤트 핸들러를 정의하는 대신, 리액트 JSX에서는 자바스크립트 함수를 중괄호{}안에 직접 전달합니다.// HTML <button onclick="alert('클릭!')">클릭</button> // React (JSX) <button onClick={() => alert('클릭!')}>클릭</button> - CamelCase 속성 이름:
onClick,onChange,onMouseOver등 이벤트 속성 이름은 HTML의 소문자와 달리 CamelCase를 사용합니다. - 합성 이벤트(Synthetic Event): 리액트의 이벤트는 실제 브라우저의 이벤트 객체가 아니라, 리액트가 브라우저의 이벤트를 감싸서 제공하는 합성 이벤트 객체(SyntheticEvent)입니다. 이 합성 이벤트 객체는 모든 브라우저에서 일관된 방식으로 작동하도록 표준화되어 있어 크로스 브라우징 이슈를 줄여줍니다. 합성 이벤트 객체는
e.stopPropagation(),e.preventDefault()와 같은 표준 메서드를 동일하게 제공합니다.
버튼 클릭 이벤트 처리하기
가장 흔하게 사용되는 이벤트 중 하나인 버튼 클릭 이벤트를 처리하는 방법을 살펴보겠습니다.
예제: 메시지 변경 버튼MessageChanger.jsx 컴포넌트 생성:
src/components 폴더 안에 MessageChanger.jsx 파일을 생성합니다.
import { useState } from 'react';
function MessageChanger() {
// (1) state 선언: 초기 메시지는 "환영합니다!"
const [message, setMessage] = useState("환영합니다!");
// (2) 이벤트 핸들러 함수 정의
const handleClickHello = () => {
setMessage("안녕하세요!"); // state 업데이트
};
const handleClickGoodbye = () => {
setMessage("안녕히 가세요!"); // state 업데이트
};
return (
<div style={{ border: '1px dashed #999', padding: '15px', margin: '20px', borderRadius: '8px' }}>
<h2>메시지 변경하기</h2>
<p style={{ fontSize: '20px', fontWeight: 'bold' }}>{message}</p>
<button onClick={handleClickHello} style={{ margin: '5px' }}>
안녕하세요 버튼
</button>
<button onClick={handleClickGoodbye} style={{ margin: '5px' }}>
안녕히 가세요 버튼
</button>
</div>
);
}
export default MessageChanger;messagestate를 선언하고 초기값을 환영합니다!로 설정했습니다.handleClickHello와handleClickGoodbye두 개의 함수를 정의했습니다. 이 함수들은 버튼이 클릭될 때 호출될 이벤트 핸들러입니다.onClick={handleClickHello}와 같이button태그의onClick속성에 정의된 함수 이름을 전달했습니다. 함수를 호출하는 형태(handleClickHello())가 아님에 주의하세요! 함수 호출 형태로 작성하면 컴포넌트가 렌더링될 때마다 함수가 즉시 실행되어 버립니다. 우리는 이벤트가 발생했을 때 함수가 실행되기를 원하므로, 함수 자체를 참조로 전달해야 합니다.
App.jsx에서 MessageChanger 컴포넌트 사용:
src/App.jsx 파일을 열고, MessageChanger 컴포넌트를 불러와 사용합니다.
import './App.css';
// ... 다른 컴포넌트 임포트 ...
import MessageChanger from './components/MessageChanger'; // MessageChanger 컴포넌트 불러오기
function App() {
return (
<div className="App">
<h1>나의 React 앱</h1>
{/* ... 다른 컴포넌트 ... */}
<MessageChanger />
</div>
);
}
export default App;결과 확인: 브라우저를 확인하면 메시지가 표시된 버튼 두 개를 볼 수 있을 것입니다. 각 버튼을 클릭하면 메시지가 동적으로 변경되는 것을 확인해 보세요.
이벤트 객체(e) 사용하기
이벤트 핸들러 함수는 이벤트가 발생했을 때 해당 이벤트에 대한 정보를 담고 있는 이벤트 객체(SyntheticEvent)를 인자로 받습니다. 이 객체를 통해 이벤트 발생 요소, 마우스 좌표, 키보드 입력 값 등 다양한 정보에 접근할 수 있습니다.
예제: 입력 필드 값 실시간 반영InputMirror.jsx 컴포넌트 생성:
src/components 폴더 안에 InputMirror.jsx 파일을 생성합니다.
import { useState } from 'react';
function InputMirror() {
const [text, setText] = useState(""); // 입력 필드의 텍스트 상태
// 이벤트 핸들러: 입력 필드 값이 변경될 때마다 호출
const handleChange = (e) => { // (1) 이벤트 객체 'e'를 인자로 받습니다.
console.log(e.target.value); // 콘솔에 입력된 값 출력
setText(e.target.value); // (2) e.target.value로 입력 필드의 현재 값에 접근
};
return (
<div style={{ border: '1px solid #c9c9c9', padding: '15px', margin: '20px', borderRadius: '8px' }}>
<h2>입력 필드 미러</h2>
<input
type="text"
value={text} // (3) input의 value를 state에 연결 (제어 컴포넌트)
onChange={handleChange} // (4) onChange 이벤트 핸들러 등록
placeholder="여기에 입력하세요..."
style={{ padding: '8px', fontSize: '16px', width: '80%', marginBottom: '10px' }}
/>
<p>
입력된 내용: <span style={{ fontWeight: 'bold', color: '#007bff' }}>{text}</span>
</p>
</div>
);
}
export default InputMirror;handleChange함수는e라는 이름으로 이벤트 객체를 인자로 받습니다.e.target은 이벤트가 발생한 DOM 요소(여기서는<input>)를 참조합니다.e.target.value는 해당<input>요소의 현재 값을 의미합니다. 우리는 이 값을setText함수를 통해textstate에 업데이트합니다.<input>태그의value속성을textstate와 연결하는 것은 매우 중요합니다. 이를 통해 입력 필드의 값이 항상state와 동기화되며, 리액트가 입력 필드를 제어할 수 있게 됩니다. 이러한<input>요소를 제어 컴포넌트(Controlled Component)라고 부릅니다.
App.jsx에서 InputMirror 컴포넌트 사용:
import './App.css';
// ... 다른 컴포넌트 임포트 ...
import InputMirror from './components/InputMirror'; // InputMirror 컴포넌트 불러오기
function App() {
return (
<div className="App">
<h1>나의 React 앱</h1>
{/* ... 다른 컴포넌트 ... */}
<InputMirror />
</div>
);
}
export default App;결과 확인: 브라우저에서 입력 필드에 텍스트를 입력하면, 아래 텍스트도 실시간으로 똑같이 변경되는 것을 확인할 수 있을 것입니다.
InputMirror 예제처럼 입력값을 state와 연결하면, 사용자의 입력과 화면에 보이는 값이 하나의 데이터 흐름 안에서 동기화됩니다.
이벤트 핸들러에 인자 전달하기
때로는 이벤트 핸들러 함수에 추가적인 인자를 전달해야 할 때가 있습니다. 예를 들어, 여러 개의 아이템 중 특정 아이템을 클릭했을 때 그 아이템의 ID를 전달하고 싶을 때입니다.
이때는 이벤트 핸들러를 화살표 함수로 감싸서 정의하는 방법이 일반적입니다.
예제: 아이템 삭제 버튼import { useState } from 'react';
function ItemList() {
const [items, setItems] = useState([
{ id: 1, text: '첫 번째 아이템' },
{ id: 2, text: '두 번째 아이템' },
{ id: 3, text: '세 번째 아이템' },
]);
// (1) 삭제할 아이템의 ID를 인자로 받는 함수 정의
const handleDeleteItem = (idToDelete) => {
// filter() 메서드를 사용하여 해당 id를 가진 아이템을 제외하고 새로운 배열 생성
setItems(items.filter(item => item.id !== idToDelete));
};
return (
<div style={{ border: '1px solid #abc', padding: '15px', margin: '20px', borderRadius: '8px' }}>
<h2>아이템 목록</h2>
<ul>
{items.map(item => (
<li key={item.id} style={{ marginBottom: '10px' }}>
{item.text}
{/* (2) 화살표 함수로 감싸서 handleDeleteItem에 item.id를 전달 */}
<button
onClick={() => handleDeleteItem(item.id)}
style={{ marginLeft: '10px', backgroundColor: '#dc3545', color: 'white', border: 'none', padding: '5px 10px', borderRadius: '4px', cursor: 'pointer' }}
>
삭제
</button>
</li>
))}
</ul>
</div>
);
}
export default ItemList;handleDeleteItem함수는idToDelete라는 인자를 받아서 해당 ID를 가진 아이템을items배열에서 제거합니다.onClick={() => handleDeleteItem(item.id)}:button의onClick에는 화살표 함수() => ...를 사용했습니다. 이 화살표 함수가 클릭 이벤트 발생 시 실행되고, 그 안에서handleDeleteItem(item.id)를 호출함으로써 원하는item.id를 인자로 전달할 수 있게 됩니다.key={item.id}: 리스트를 렌더링할 때 각 아이템에keyprops를 부여하는 것은 매우 중요합니다. 리액트가 리스트의 각 아이템을 효율적으로 식별하고 업데이트하는 데 사용됩니다.key는 유일해야 하며, 보통 데이터의 고유 ID를 사용합니다.
이벤트 전파(Event Propagation)와 방지
브라우저에서 이벤트는 DOM 트리를 따라 전파됩니다. 예를 들어, 버튼을 클릭하면 버튼에 대한 클릭 이벤트뿐만 아니라, 그 버튼을 감싸고 있는 div, body 등 부모 요소들에게도 클릭 이벤트가 전달(버블링)됩니다. 때로는 이러한 이벤트 전파를 막거나, 기본 동작(예: 링크 클릭 시 페이지 이동, 폼 제출 시 새로고침)을 방지해야 할 필요가 있습니다.
-
e.stopPropagation(): 이벤트가 부모 요소로 전파되는 것을 막습니다.<div onClick={() => alert('부모 div 클릭!')}> <button onClick={(e) => { e.stopPropagation(); // 버튼 클릭 시 부모 div의 클릭 이벤트는 발생하지 않음 alert('버튼 클릭!'); }}> 버튼 </button> </div> -
e.preventDefault(): 이벤트의 기본 동작을 막습니다. 예를 들어,<a>태그의 기본 동작인 페이지 이동을 막거나,<form>태그의 기본 동작인 페이지 새로고침을 막을 때 사용합니다.<a href="https://google.com" onClick={(e) => { e.preventDefault(); // 링크 클릭 시 페이지 이동을 막음 alert('링크 이동이 차단되었습니다!'); }}> 구글로 이동 (차단) </a>
이 두 메서드는 이벤트가 의도치 않은 동작을 유발하거나 성능에 영향을 줄 때 유용하게 사용됩니다.
버튼 클릭, 입력 변경, 인자 전달, 전파 제어를 따로 외우기보다 같은 점검 순서로 묶어 보면 실수가 줄어듭니다.
이벤트를 읽을 때는 onClick, onChange 같은 속성 이름보다 먼저 함수가 언제 실행되는지 확인해야 합니다. 함수 참조를 넘기는지, 이벤트 객체에서 어떤 값을 읽는지, 필요할 때 전파와 기본 동작을 끊는지까지 이어서 보면 props, state, 이벤트 처리가 하나의 상호작용 흐름으로 연결됩니다.