이벤트 처리
2. 이벤트 처리
React에서 사용자의 상호작용을 처리하는 방법을 배워보겠습니다. 클릭, 입력, 폼 제출 등 다양한 이벤트를 어떻게 처리하는지 알아봅시다.
2.1 React에서의 이벤트
React는 브라우저의 네이티브 이벤트를 감싸는 SyntheticEvent를 사용합니다. 이를 통해 브라우저 간 일관된 동작을 보장합니다.
2.1.1 기본 이벤트 처리
function Button() {
const handleClick = () => {
alert('버튼이 클릭되었습니다!');
};
return (
<button onClick={handleClick}>
클릭하세요
</button>
);
}
2.1.2 인라인 이벤트 핸들러
function App() {
return (
<div>
<button onClick={() => alert('인라인 핸들러!')}>
인라인 핸들러
</button>
<button onClick={() => console.log('콘솔에 출력')}>
콘솔 출력
</button>
</div>
);
}
2.2 다양한 이벤트 타입
2.2.1 마우스 이벤트
import { useState } from 'react';
function MouseEvents() {
const [message, setMessage] = useState('마우스를 움직여보세요');
return (
<div>
<div
onClick={() => setMessage('클릭됨!')}
onDoubleClick={() => setMessage('더블클릭됨!')}
onMouseEnter={() => setMessage('마우스 진입!')}
onMouseLeave={() => setMessage('마우스 벗어남!')}
onMouseMove={() => setMessage('마우스 이동 중...')}
>
<p>{message}</p>
</div>
<button onClick={() => setMessage('마우스를 움직여보세요')}>
초기화
</button>
</div>
);
}
2.2.2 키보드 이벤트
import { useState } from 'react';
function KeyboardEvents() {
const [keyInfo, setKeyInfo] = useState('키를 눌러보세요');
const handleKeyDown = (event) => {
setKeyInfo(`눌린 키: ${event.key} (코드: ${event.keyCode})`);
};
const handleKeyPress = (event) => {
if (event.key === 'Enter') {
alert('Enter 키가 눌렸습니다!');
}
};
return (
<div>
<input
type="text"
placeholder="여기에 타이핑하세요"
onKeyDown={handleKeyDown}
onKeyPress={handleKeyPress}
/>
<p>
{keyInfo}
</p>
<p>
Enter 키를 누르면 알림이 표시됩니다.
</p>
</div>
);
}
2.2.3 폼 이벤트
import { useState } from 'react';
function FormEvents() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleSubmit = (event) => {
event.preventDefault(); // 페이지 새로고침 방지
alert(`제출된 데이터:\n이름: ${formData.name}\n이메일: ${formData.email}\n메시지: ${formData.message}`);
};
const handleChange = (event) => {
const { name, value } = event.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleReset = () => {
setFormData({
name: '',
email: '',
message: ''
});
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
이름:
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</label>
</div>
<div>
<label>
이메일:
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</label>
</div>
<div>
<label>
메시지:
<textarea
name="message"
value={formData.message}
onChange={handleChange}
rows="4"
/>
</label>
</div>
<div>
<button type="submit">
제출
</button>
<button
type="button"
onClick={handleReset}
>
초기화
</button>
</div>
</form>
);
}
2.3 이벤트 객체 활용
2.3.1 SyntheticEvent 속성들
import { useState } from 'react';
function EventInfo() {
const [eventData, setEventData] = useState('이벤트가 발생하지 않았습니다');
const handleDetailedEvent = (event) => {
const info = {
type: event.type,
target: event.target.tagName,
clientX: event.clientX,
clientY: event.clientY,
key: event.key,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
altKey: event.altKey
};
setEventData(JSON.stringify(info, null, 2));
};
return (
<div>
<h3>이벤트 정보 확인하기</h3>
<div>
<button
onClick={handleDetailedEvent}
onMouseMove={handleDetailedEvent}
>
버튼 (클릭/호버)
</button>
<input
type="text"
placeholder="여기서 키 입력"
onKeyDown={handleDetailedEvent}
/>
</div>
<pre>
{eventData}
</pre>
</div>
);
}
2.4 이벤트 전파 제어
2.4.1 event.stopPropagation()
import { useState } from 'react';
function EventPropagation() {
const [clickLog, setClickLog] = useState([]);
const addLog = (message) => {
setClickLog(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`]);
};
const handleParentClick = () => {
addLog('부모 요소 클릭됨');
};
const handleChildClick = (event) => {
addLog('자식 요소 클릭됨');
// event.stopPropagation(); // 주석을 해제하면 부모로 이벤트가 전파되지 않음
};
const handleStopPropagationClick = (event) => {
event.stopPropagation();
addLog('전파 차단된 자식 요소 클릭됨');
};
return (
<div>
<h3>이벤트 전파 테스트</h3>
<div onClick={handleParentClick}>
<p>부모 요소 (파란색 영역)</p>
<button onClick={handleChildClick}>
일반 자식 버튼
</button>
<button onClick={handleStopPropagationClick}>
전파 차단 버튼
</button>
</div>
<div>
<h4>클릭 로그:</h4>
<button onClick={() => setClickLog([])}>
로그 지우기
</button>
<ul>
{clickLog.map((log, index) => (
<li key={index}>
{log}
</li>
))}
</ul>
</div>
</div>
);
}
2.4.2 event.preventDefault()
import { useState } from 'react';
function PreventDefaultExample() {
const [submitted, setSubmitted] = useState(false);
const handleNormalSubmit = (event) => {
alert('폼이 제출됩니다! (페이지가 새로고침될 수 있습니다)');
};
const handlePreventDefaultSubmit = (event) => {
event.preventDefault();
setSubmitted(true);
alert('preventDefault()로 기본 동작을 차단했습니다!');
};
const handleLinkClick = (event) => {
event.preventDefault();
alert('링크 클릭이 차단되었습니다!');
};
return (
<div>
<h3>preventDefault() 예제</h3>
<div>
<h4>폼 제출 테스트</h4>
<form onSubmit={handleNormalSubmit}>
<input type="text" placeholder="일반 폼" />
<button type="submit">일반 제출</button>
</form>
<form onSubmit={handlePreventDefaultSubmit}>
<input type="text" placeholder="차단된 폼" />
<button type="submit">차단된 제출</button>
</form>
{submitted && <p>폼 제출이 처리되었습니다!</p>}
</div>
<div>
<h4>링크 클릭 테스트</h4>
<p>
<a href="https://example.com" target="_blank" rel="noopener noreferrer">
일반 링크 (새 창에서 열림)
</a>
</p>
<p>
<a href="https://example.com" onClick={handleLinkClick}>
차단된 링크 (클릭해도 이동하지 않음)
</a>
</p>
</div>
</div>
);
}
2.5 실습: 인터랙티브 계산기
이벤트 처리를 활용한 간단한 계산기를 만들어봅시다:
import { useState } from 'react';
function Calculator() {
const [display, setDisplay] = useState('0');
const [previousValue, setPreviousValue] = useState(null);
const [operation, setOperation] = useState(null);
const [waitingForOperand, setWaitingForOperand] = useState(false);
const inputNumber = (num) => {
if (waitingForOperand) {
setDisplay(String(num));
setWaitingForOperand(false);
} else {
setDisplay(display === '0' ? String(num) : display + num);
}
};
const inputOperation = (nextOperation) => {
const inputValue = parseFloat(display);
if (previousValue === null) {
setPreviousValue(inputValue);
} else if (operation) {
const currentValue = previousValue || 0;
const newValue = calculate(currentValue, inputValue, operation);
setDisplay(String(newValue));
setPreviousValue(newValue);
}
setWaitingForOperand(true);
setOperation(nextOperation);
};
const calculate = (firstValue, secondValue, operation) => {
switch (operation) {
case '+':
return firstValue + secondValue;
case '-':
return firstValue - secondValue;
case '*':
return firstValue * secondValue;
case '/':
return firstValue / secondValue;
case '=':
return secondValue;
default:
return secondValue;
}
};
const performCalculation = () => {
const inputValue = parseFloat(display);
if (previousValue !== null && operation) {
const newValue = calculate(previousValue, inputValue, operation);
setDisplay(String(newValue));
setPreviousValue(null);
setOperation(null);
setWaitingForOperand(true);
}
};
const clear = () => {
setDisplay('0');
setPreviousValue(null);
setOperation(null);
setWaitingForOperand(false);
};
const handleKeyPress = (event) => {
const { key } = event;
if (key >= '0' && key <= '9') {
inputNumber(parseInt(key));
} else if (key === '+' || key === '-' || key === '*' || key === '/') {
inputOperation(key);
} else if (key === 'Enter' || key === '=') {
performCalculation();
} else if (key === 'Escape' || key === 'c' || key === 'C') {
clear();
}
};
// 스타일 객체들은 제거됨 (교육 목적으로 단순화)
return (
<div
onKeyDown={handleKeyPress}
tabIndex="0"
>
<h3>계산기</h3>
<p>
키보드로도 조작 가능합니다
</p>
<div>
{display}
</div>
<div>
<button onClick={clear}>
C
</button>
<button onClick={() => inputOperation('/')}>÷</button>
<button onClick={() => inputOperation('*')}>×</button>
<button onClick={() => inputOperation('-')}>-</button>
<button onClick={() => inputNumber(7)}>7</button>
<button onClick={() => inputNumber(8)}>8</button>
<button onClick={() => inputNumber(9)}>9</button>
<button onClick={() => inputOperation('+')}>+</button>
<button onClick={() => inputNumber(4)}>4</button>
<button onClick={() => inputNumber(5)}>5</button>
<button onClick={() => inputNumber(6)}>6</button>
<button onClick={performCalculation}>
=
</button>
<button onClick={() => inputNumber(1)}>1</button>
<button onClick={() => inputNumber(2)}>2</button>
<button onClick={() => inputNumber(3)}>3</button>
<button onClick={() => inputNumber(0)}>
0
</button>
<button onClick={() => {
if (display.indexOf('.') === -1) {
setDisplay(display + '.');
}
}}>
.
</button>
</div>
</div>
);
}
export default Calculator;
2.6 이벤트 처리 Best Practices
2.6.1 이벤트 핸들러 네이밍
// ✅ 좋은 예
const handleSubmit = () => { /* ... */ };
const handleButtonClick = () => { /* ... */ };
const handleInputChange = () => { /* ... */ };
// ❌ 나쁜 예
const submit = () => { /* ... */ };
const click = () => { /* ... */ };
const change = () => { /* ... */ };
2.6.2 이벤트 핸들러 분리
// ✅ 좋은 예 - 핸들러 분리
function TodoItem({ todo, onToggle, onDelete }) {
const handleToggle = () => {
onToggle(todo.id);
};
const handleDelete = () => {
onDelete(todo.id);
};
return (
<div>
<input type="checkbox" checked={todo.completed} onChange={handleToggle} />
<span>{todo.text}</span>
<button onClick={handleDelete}>삭제</button>
</div>
);
}
// ❌ 나쁜 예 - 인라인 핸들러
function TodoItem({ todo, onToggle, onDelete }) {
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>삭제</button>
</div>
);
}
2.6.3 useCallback으로 최적화
import { useState, useCallback } from 'react';
function OptimizedComponent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
// useCallback으로 핸들러 메모이제이션
const handleIncrement = useCallback(() => {
setCount(prev => prev + 1);
}, []);
const handleAddItem = useCallback(() => {
setItems(prev => [...prev, `아이템 ${prev.length + 1}`]);
}, []);
return (
<div>
<p>카운트: {count}</p>
<button onClick={handleIncrement}>증가</button>
<button onClick={handleAddItem}>아이템 추가</button>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
2.7 정리
React에서의 이벤트 처리를 학습했습니다:
- SyntheticEvent로 브라우저 간 일관된 이벤트 처리
- 다양한 이벤트 타입 (마우스, 키보드, 폼 등) 활용
- 이벤트 객체의 속성들을 통한 상세 정보 접근
- event.stopPropagation()과 preventDefault()로 이벤트 제어
- 이벤트 핸들러 최적화 및 Best Practices
다음 장에서는 조건부 렌더링에 대해 알아보겠습니다.