챕터 13: 이벤트 처리 마스터하기
서론
웹 애플리케이션은 사용자와의 상호작용으로 살아 움직입니다. 클릭, 타이핑, 마우스 이동, 스크롤 등 사용자의 모든 행동은 이벤트를 발생시킵니다. 이러한 이벤트를 적절히 처리하는 것이 인터랙티브한 웹 애플리케이션을 만드는 핵심입니다.
React에서 이벤트를 처리하는 방법은 일반 HTML과 비슷하지만, 몇 가지 중요한 차이점이 있습니다. 이번 챕터에서는 React의 이벤트 시스템을 완벽하게 이해하고, 다양한 이벤트를 처리하는 방법을 실습을 통해 마스터해보겠습니다.
본론
React 이벤트의 특징
React에서 이벤트를 처리할 때 알아야 할 중요한 차이점들:
- 카멜케이스(camelCase) 사용: onclick이 아닌 onClick
- 함수를 전달: 문자열이 아닌 함수를 전달
- SyntheticEvent: React는 브라우저 간 호환성을 위해 이벤트를 감싸서 제공
'use client'
import { useState } from 'react'
export default function EventBasics() {
const [message, setMessage] = useState('')
// 이벤트 핸들러 함수
const handleClick = () => {
setMessage('버튼이 클릭되었습니다!')
}
// 이벤트 객체 받기
const handleClickWithEvent = (e) => {
console.log('이벤트 객체:', e)
console.log('클릭한 요소:', e.target)
setMessage(`버튼 "${e.target.textContent}"를 클릭했습니다`)
}
return (
<div className="p-8">
<h2 className="text-2xl font-bold mb-4">이벤트 처리 기초</h2>
{/* 기본 클릭 이벤트 */}
<button
onClick={handleClick}
className="bg-blue-500 text-white px-4 py-2 rounded mr-2"
>
기본 클릭
</button>
{/* 이벤트 객체 활용 */}
<button
onClick={handleClickWithEvent}
className="bg-green-500 text-white px-4 py-2 rounded mr-2"
>
이벤트 객체 활용
</button>
{/* 인라인 핸들러 */}
<button
onClick={() => setMessage('인라인 핸들러 실행!')}
className="bg-purple-500 text-white px-4 py-2 rounded"
>
인라인 핸들러
</button>
{message && (
<p className="mt-4 p-4 bg-gray-100 rounded">{message}</p>
)}
</div>
)
}
주요 이벤트 타입들
웹 개발에서 자주 사용되는 이벤트들을 살펴보겠습니다:
'use client'
import { useState } from 'react'
export default function CommonEvents() {
const [eventLog, setEventLog] = useState([])
const addLog = (message) => {
setEventLog(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`])
}
return (
<div className="p-8">
<h2 className="text-2xl font-bold mb-6">다양한 이벤트 타입</h2>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-4">
{/* 마우스 이벤트 */}
<div className="border p-4 rounded">
<h3 className="font-bold mb-2">마우스 이벤트</h3>
<div
className="bg-blue-100 p-8 rounded text-center cursor-pointer"
onClick={() => addLog('클릭됨')}
onDoubleClick={() => addLog('더블클릭됨')}
onMouseEnter={() => addLog('마우스 진입')}
onMouseLeave={() => addLog('마우스 떠남')}
onMouseMove={() => console.log('마우스 이동중')}
>
마우스를 여기로 가져와보세요
</div>
</div>
{/* 키보드 이벤트 */}
<div className="border p-4 rounded">
<h3 className="font-bold mb-2">키보드 이벤트</h3>
<input
type="text"
className="w-full border rounded px-3 py-2"
placeholder="여기에 타이핑해보세요"
onKeyDown={(e) => addLog(`키 눌림: ${e.key}`)}
onKeyUp={(e) => addLog(`키 떼짐: ${e.key}`)}
onKeyPress={(e) => console.log('키 프레스:', e.key)}
/>
</div>
{/* 포커스 이벤트 */}
<div className="border p-4 rounded">
<h3 className="font-bold mb-2">포커스 이벤트</h3>
<input
type="text"
className="w-full border rounded px-3 py-2"
placeholder="포커스/블러 테스트"
onFocus={() => addLog('포커스 받음')}
onBlur={() => addLog('포커스 잃음')}
/>
</div>
{/* 폼 이벤트 */}
<div className="border p-4 rounded">
<h3 className="font-bold mb-2">폼 이벤트</h3>
<form onSubmit={(e) => {
e.preventDefault()
addLog('폼 제출됨')
}}>
<input
type="text"
className="w-full border rounded px-3 py-2 mb-2"
onChange={(e) => addLog(`입력 변경: ${e.target.value}`)}
placeholder="텍스트 입력"
/>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded"
>
제출
</button>
</form>
</div>
</div>
{/* 이벤트 로그 */}
<div className="border p-4 rounded bg-gray-50">
<h3 className="font-bold mb-2">이벤트 로그</h3>
<div className="h-96 overflow-y-auto space-y-1">
{eventLog.length === 0 ? (
<p className="text-gray-500">이벤트가 여기에 표시됩니다</p>
) : (
eventLog.map((log, index) => (
<div key={index} className="text-sm bg-white p-2 rounded">
{log}
</div>
))
)}
</div>
<button
onClick={() => setEventLog([])}
className="mt-2 text-sm text-red-500 hover:text-red-700"
>
로그 클리어
</button>
</div>
</div>
</div>
)
}
이벤트 핸들러에 매개변수 전달하기
때로는 이벤트 핸들러에 추가 데이터를 전달해야 할 때가 있습니다:
'use client'
import { useState } from 'react'
export default function EventParameters() {
const [selectedItem, setSelectedItem] = useState(null)
const [cart, setCart] = useState([])
const products = [
{ id: 1, name: '노트북', price: 1500000 },
{ id: 2, name: '마우스', price: 30000 },
{ id: 3, name: '키보드', price: 100000 },
{ id: 4, name: '모니터', price: 400000 }
]
// 매개변수를 받는 이벤트 핸들러
const handleSelectItem = (item) => {
setSelectedItem(item)
}
// 이벤트 객체와 매개변수를 함께 받기
const handleAddToCart = (e, item) => {
e.stopPropagation() // 이벤트 버블링 방지
setCart([...cart, item])
}
// 커링을 사용한 방법
const handleRemoveFromCart = (itemId) => (e) => {
setCart(cart.filter(item => item.id !== itemId))
}
return (
<div className="p-8">
<h2 className="text-2xl font-bold mb-4">상품 목록</h2>
<div className="grid grid-cols-2 gap-6">
{/* 상품 목록 */}
<div>
<h3 className="font-bold mb-3">상품 선택</h3>
<div className="space-y-2">
{products.map(product => (
<div
key={product.id}
onClick={() => handleSelectItem(product)}
className={`border p-4 rounded cursor-pointer transition-all ${
selectedItem?.id === product.id
? 'border-blue-500 bg-blue-50'
: 'hover:border-gray-400'
}`}
>
<div className="flex justify-between items-center">
<div>
<h4 className="font-semibold">{product.name}</h4>
<p className="text-gray-600">
{product.price.toLocaleString()}원
</p>
</div>
<button
onClick={(e) => handleAddToCart(e, product)}
className="bg-green-500 text-white px-3 py-1 rounded hover:bg-green-600"
>
담기
</button>
</div>
</div>
))}
</div>
</div>
{/* 장바구니 */}
<div>
<h3 className="font-bold mb-3">장바구니 ({cart.length})</h3>
<div className="border rounded p-4 min-h-[200px]">
{cart.length === 0 ? (
<p className="text-gray-500 text-center">장바구니가 비어있습니다</p>
) : (
<div className="space-y-2">
{cart.map((item, index) => (
<div
key={`${item.id}-${index}`}
className="flex justify-between items-center bg-gray-50 p-2 rounded"
>
<span>{item.name}</span>
<button
onClick={handleRemoveFromCart(item.id)}
className="text-red-500 hover:text-red-700"
>
삭제
</button>
</div>
))}
<div className="border-t pt-2 mt-2">
<p className="font-bold">
총액: {cart.reduce((sum, item) => sum + item.price, 0).toLocaleString()}원
</p>
</div>
</div>
)}
</div>
</div>
</div>
{/* 선택된 상품 정보 */}
{selectedItem && (
<div className="mt-6 p-4 bg-blue-100 rounded">
<h3 className="font-bold">선택된 상품</h3>
<p>{selectedItem.name} - {selectedItem.price.toLocaleString()}원</p>
</div>
)}
</div>
)
}
이벤트 버블링과 캡처링
이벤트가 DOM 트리를 통해 전파되는 방식을 이해하는 것이 중요합니다:
'use client'
import { useState } from 'react'
export default function EventPropagation() {
const [log, setLog] = useState([])
const addLog = (message) => {
setLog(prev => [...prev, message])
}
const clearLog = () => setLog([])
return (
<div className="p-8">
<h2 className="text-2xl font-bold mb-4">이벤트 전파</h2>
<div className="grid grid-cols-2 gap-6">
{/* 이벤트 버블링 예제 */}
<div>
<h3 className="font-bold mb-3">이벤트 버블링</h3>
<div
onClick={() => addLog('외부 DIV 클릭 (버블링)')}
className="bg-red-100 p-8 rounded cursor-pointer"
>
외부 DIV
<div
onClick={() => addLog('중간 DIV 클릭 (버블링)')}
className="bg-red-200 p-6 mt-4 rounded"
>
중간 DIV
<div
onClick={() => addLog('내부 DIV 클릭 (버블링)')}
className="bg-red-300 p-4 mt-4 rounded"
>
내부 DIV
</div>
</div>
</div>
</div>
{/* 이벤트 전파 중단 예제 */}
<div>
<h3 className="font-bold mb-3">이벤트 전파 중단</h3>
<div
onClick={() => addLog('외부 DIV 클릭 (중단)')}
className="bg-blue-100 p-8 rounded cursor-pointer"
>
외부 DIV
<div
onClick={(e) => {
e.stopPropagation()
addLog('중간 DIV 클릭 (전파 중단됨)')
}}
className="bg-blue-200 p-6 mt-4 rounded"
>
중간 DIV (stopPropagation)
<div
onClick={(e) => {
e.stopPropagation()
addLog('내부 DIV 클릭 (전파 중단됨)')
}}
className="bg-blue-300 p-4 mt-4 rounded"
>
내부 DIV (stopPropagation)
</div>
</div>
</div>
</div>
</div>
{/* 이벤트 로그 */}
<div className="mt-6 border rounded p-4 bg-gray-50">
<div className="flex justify-between items-center mb-2">
<h3 className="font-bold">이벤트 발생 순서</h3>
<button
onClick={clearLog}
className="text-sm text-red-500 hover:text-red-700"
>
클리어
</button>
</div>
<div className="space-y-1 max-h-40 overflow-y-auto">
{log.length === 0 ? (
<p className="text-gray-500">DIV를 클릭해보세요</p>
) : (
log.map((message, index) => (
<div key={index} className="text-sm bg-white p-2 rounded">
{index + 1}. {message}
</div>
))
)}
</div>
</div>
</div>
)
}
실습: 인터랙티브 게임 만들기
이벤트 처리를 활용한 간단한 반응 속도 게임을 만들어봅시다:
'use client'
import { useState, useEffect } from 'react'
export default function ReactionGame() {
const [gameState, setGameState] = useState('ready') // ready, waiting, click, finished
const [startTime, setStartTime] = useState(null)
const [reactionTime, setReactionTime] = useState(null)
const [scores, setScores] = useState([])
const [timeoutId, setTimeoutId] = useState(null)
const startGame = () => {
setGameState('waiting')
setReactionTime(null)
const delay = Math.random() * 3000 + 2000 // 2~5초 랜덤 대기
const id = setTimeout(() => {
setGameState('click')
setStartTime(Date.now())
}, delay)
setTimeoutId(id)
}
const handleClick = () => {
if (gameState === 'waiting') {
// 너무 빨리 클릭함
clearTimeout(timeoutId)
setGameState('ready')
setReactionTime('너무 빨리 클릭했습니다!')
} else if (gameState === 'click') {
// 정상 클릭
const time = Date.now() - startTime
setReactionTime(time)
setScores([...scores, time])
setGameState('finished')
}
}
const getAverageScore = () => {
if (scores.length === 0) return 0
return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
}
const getBestScore = () => {
if (scores.length === 0) return 0
return Math.min(...scores)
}
const reset = () => {
setGameState('ready')
setReactionTime(null)
}
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold text-center mb-8">
⚡ 반응 속도 테스트
</h1>
{/* 게임 영역 */}
<div
onClick={handleClick}
className={`h-64 rounded-lg flex items-center justify-center cursor-pointer transition-colors ${
gameState === 'ready' ? 'bg-blue-500 hover:bg-blue-600' :
gameState === 'waiting' ? 'bg-red-500' :
gameState === 'click' ? 'bg-green-500' :
'bg-gray-500'
}`}
>
<div className="text-white text-center">
{gameState === 'ready' && (
<>
<h2 className="text-2xl font-bold mb-2">클릭하여 시작</h2>
<p>초록색이 되면 최대한 빨리 클릭하세요!</p>
</>
)}
{gameState === 'waiting' && (
<>
<h2 className="text-2xl font-bold mb-2">기다리세요...</h2>
<p>초록색이 될 때까지 기다리세요</p>
</>
)}
{gameState === 'click' && (
<h2 className="text-3xl font-bold">지금 클릭!</h2>
)}
{gameState === 'finished' && (
<>
<h2 className="text-3xl font-bold mb-2">
{typeof reactionTime === 'number'
? `${reactionTime}ms`
: reactionTime}
</h2>
<button
onClick={(e) => {
e.stopPropagation()
reset()
}}
className="mt-4 bg-white text-blue-500 px-6 py-2 rounded hover:bg-gray-100"
>
다시 시도
</button>
</>
)}
</div>
</div>
{/* 버튼 */}
{gameState === 'ready' && (
<button
onClick={startGame}
className="w-full mt-6 bg-blue-500 hover:bg-blue-600 text-white py-3 rounded-lg font-bold transition-colors"
>
게임 시작
</button>
)}
{/* 점수 */}
<div className="mt-8 grid grid-cols-3 gap-4">
<div className="bg-gray-100 p-4 rounded text-center">
<p className="text-gray-600 text-sm">시도 횟수</p>
<p className="text-2xl font-bold">{scores.length}</p>
</div>
<div className="bg-gray-100 p-4 rounded text-center">
<p className="text-gray-600 text-sm">평균 속도</p>
<p className="text-2xl font-bold">{getAverageScore()}ms</p>
</div>
<div className="bg-gray-100 p-4 rounded text-center">
<p className="text-gray-600 text-sm">최고 기록</p>
<p className="text-2xl font-bold">{getBestScore()}ms</p>
</div>
</div>
{/* 기록 히스토리 */}
{scores.length > 0 && (
<div className="mt-6">
<h3 className="font-bold mb-2">최근 기록</h3>
<div className="flex gap-2 flex-wrap">
{scores.slice(-10).map((score, index) => (
<span
key={index}
className="bg-blue-100 text-blue-700 px-3 py-1 rounded text-sm"
>
{score}ms
</span>
))}
</div>
</div>
)}
</div>
)
}
커스텀 이벤트 훅 만들기
자주 사용하는 이벤트 로직을 커스텀 훅으로 만들 수 있습니다:
'use client'
import { useState, useEffect } from 'react'
// 커스텀 훅: 키보드 이벤트
function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false)
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === targetKey) {
setKeyPressed(true)
}
}
const handleKeyUp = (e) => {
if (e.key === targetKey) {
setKeyPressed(false)
}
}
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)
}
}, [targetKey])
return keyPressed
}
// 커스텀 훅: 마우스 위치
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
}
}, [])
return position
}
// 사용 예제
export default function CustomHooksExample() {
const spacePressed = useKeyPress(' ')
const enterPressed = useKeyPress('Enter')
const mousePosition = useMousePosition()
return (
<div className="p-8">
<h2 className="text-2xl font-bold mb-4">커스텀 이벤트 훅</h2>
<div className="space-y-4">
<div className="border rounded p-4">
<h3 className="font-bold mb-2">키보드 상태</h3>
<p>스페이스바: {spacePressed ? '눌림 ✅' : '안눌림 ❌'}</p>
<p>엔터키: {enterPressed ? '눌림 ✅' : '안눌림 ❌'}</p>
</div>
<div className="border rounded p-4">
<h3 className="font-bold mb-2">마우스 위치</h3>
<p>X: {mousePosition.x}px</p>
<p>Y: {mousePosition.y}px</p>
</div>
<div
className="border rounded p-8 bg-gray-100 relative overflow-hidden"
style={{ height: '200px' }}
>
<div
className="absolute w-4 h-4 bg-red-500 rounded-full transform -translate-x-1/2 -translate-y-1/2"
style={{
left: `${(mousePosition.x / window.innerWidth) * 100}%`,
top: `${(mousePosition.y / window.innerHeight) * 100}%`
}}
/>
<p className="text-center text-gray-600">
마우스를 움직여보세요
</p>
</div>
</div>
</div>
)
}
결론
이벤트 처리는 사용자와 웹 애플리케이션이 소통하는 창구입니다. 이번 챕터에서 우리는 React의 이벤트 시스템, 다양한 이벤트 타입, 이벤트 전파, 그리고 실제 활용 예제까지 다루었습니다.
중요한 것은 각 이벤트의 특성을 이해하고 적절한 상황에 올바른 이벤트를 사용하는 것입니다. onClick은 클릭에, onChange는 입력 변경에, onSubmit은 폼 제출에 사용하는 것처럼 말입니다. 또한 이벤트 버블링과 전파를 이해하면 더 효율적인 이벤트 처리가 가능합니다.
다음 챕터에서는 조건부 렌더링을 다루어, State와 이벤트를 활용하여 동적으로 UI를 변경하는 방법을 배워보겠습니다. 이벤트 처리와 조건부 렌더링을 결합하면 진정한 인터랙티브 웹 애플리케이션을 만들 수 있습니다!