JavaScript에서 이벤트(event)는 사용자와의 상호작용에 따라 발생하는 일련의 사건을 의미한다.
마우스를 올리거나 클릭하거나, 키보드로 글자를 입력하는 등의 동작이 모두 이벤트로 간주된다.
이벤트의 기본 형식은 아래와 같다.
<JSXElement 이벤트_속성={이벤트_핸들러} />
HTML에서 이벤트 속성은 특정 동작 수행 시 실행할 JavaScript 코드를 지정하는 속성이었다.
리액트에서도 이벤트 속성의 목적은 비슷하지만, HTML과 달리 몇 가지 중요한 차이점이 있다.
| 구분 | HTML | React |
|---|---|---|
| 네이밍 | 소문자 (onclick) | 카멜케이스 (onClick) |
| 값 | 문자열 | 함수 참조 |
| 예시 | <button> | <button onClick={handleClick}> |
<!-- HTML -->
<button onclick="alert('클릭!')">클릭</button>
<!-- React -->
<button onClick={() => alert('클릭!')}>클릭</button>
중요한 규칙:
// ✅ 올바른 코드 - DOM 표준 이벤트
<button onClick={handleClick}>클릭</button>
<input onChange={handleChange} />
// ❌ 잘못된 코드 - DOM이 모르는 이벤트
<button onCustomClick={handleClick}>클릭</button> // 작동 안 함!
// ✅ 커스텀 컴포넌트에서는 자유롭게 정의 가능
function MyButton({ onCustomClick, onSpecialAction }) {
return (
<button onClick={onCustomClick}>
클릭
</button>
);
}
// 사용
<MyButton
onCustomClick={handleClick}
onSpecialAction={handleSpecial}
/>
단, 그 props를 실제 DOM 이벤트에 연결하는 시점에는 반드시 DOM 표준 이벤트 이름을 사용해야 한다.
| 이벤트 속성 | 설명 |
|---|---|
onClick ★ | 클릭 이벤트 발생 시 호출 |
onDoubleClick | 더블 클릭 이벤트 발생 시 호출 |
onMouseEnter | 마우스가 요소 위로 올라갈 때 호출 |
onMouseLeave | 마우스가 요소에서 이동했을 때 호출 |
onMouseMove | 마우스가 요소 위에서 움직일 때 호출 |
onMouseDown | 마우스 버튼을 눌렀을 때 호출 |
onMouseUp | 마우스 버튼을 눌렀다 뺐을 때 호출 |
onContextMenu | 마우스 오른쪽 버튼 클릭 시 호출 |
onDrag | 드래그할 때 호출 |
onDrop | 드래그한 요소를 놓을 때 호출 |
| 이벤트 속성 | 설명 |
|---|---|
onKeyDown ★ | 키를 눌렀을 때 호출 (권장) |
onKeyUp ★ | 키를 눌렀다 뺐을 때 호출 |
참고:
onKeyPress는 더 이상 권장되지 않습니다(deprecated). 대신onKeyDown을 사용하세요.
| 이벤트 속성 | 설명 |
|---|---|
onChange ★ | 입력 필드의 값이 변경될 때 호출 (실시간) |
onInput | 입력 필드에서 입력이 발생할 때 호출 |
onFocus | 입력 필드에 포커스가 들어올 때 호출 |
onBlur | 입력 필드에서 포커스가 빠져나갈 때 호출 |
onSubmit ★ | 폼이 제출될 때 호출 |
onChange vs onInput:
onChange는 실시간으로 동작합니다 (HTML과 다름)onInput은 거의 사용하지 않으며, React에서는 onChange 사용을 권장합니다| 이벤트 속성 | 설명 |
|---|---|
onTouchStart | 터치가 시작될 때 호출 |
onTouchMove | 터치 중에 움직일 때 호출 |
onTouchEnd | 터치가 끝났을 때 호출 |
| 이벤트 속성 | 설명 |
|---|---|
onLoad | 이미지나 iframe 등이 로드될 때 호출 |
onError | 이미지나 iframe 등에서 오류 발생 시 호출 |
onScroll | 요소가 스크롤될 때 호출 |
이벤트 핸들러는 이벤트가 발생했을 때 실행할 함수이다.
JSX 요소에 이벤트 속성을 이용해 이벤트를 등록할 때, 이벤트 속성에는 이벤트 핸들러를 할당해야 한다.
function App() {
function handleClick() {
alert("버튼 눌림!");
}
return (
<button onClick={handleClick}>누르기</button>
);
}
| 역할 | 이름 | 의미 |
|---|---|---|
| 이벤트 핸들러(함수) | handleClick | "버튼 눌리면 실행될 함수" |
| 이벤트 속성 | onClick | 버튼에 "클릭 이벤트 달아주세요"라는 속성 |
컴포넌트 내부나 외부에 이벤트 핸들러를 정의하고, 해당 함수를 참조하여 이벤트 속성에 할당하는 방식이다.
function App() {
function handleClick() {
alert("클릭됨!");
}
// ✅ 올바른 방법 - 함수 참조만 전달
return <button onClick={handleClick}>클릭</button>;
}
주의사항: 함수를 호출하면 안 된다!
// ❌ 잘못된 코드 - 함수가 즉시 실행됨
<button onClick={handleClick()}>클릭</button>
// ✅ 올바른 코드 - 함수 참조만 전달
<button onClick={handleClick}>클릭</button>
handleClick()처럼 괄호를 붙이면 렌더링 시점에 함수가 즉시 실행되고, 그 반환값(보통 undefined)이 onClick에 할당된다.
이벤트 속성 안에 화살표 함수를 직접 작성하는 방법이다.
function App() {
return (
<button onClick={() => alert("클릭됨!")}>클릭</button>
);
}
장점: 간단한 로직에 적합
단점: 복잡한 로직은 가독성 저하
이벤트 핸들러에 추가 매개변수를 전달해야 하는 경우가 많다. 여러 가지 방법이 있지만, 실무에서는 주로 화살표 함수로 감싸는 방법을 사용한다.
function App() {
function handleClick(id, name) {
alert(`${id}번 ${name} 클릭됨!`);
}
return (
<div>
<button onClick={() => handleClick(1, "첫번째")}>버튼 1</button>
<button onClick={() => handleClick(2, "두번째")}>버튼 2</button>
</div>
);
}
장점:
단점:
이벤트 객체(event)가 필요한 경우, 화살표 함수의 매개변수로 받아서 전달할 수 있다.
function App() {
function handleClick(id, event) {
event.preventDefault(); // 기본 동작 막기
alert(`${id}번 클릭됨!`);
console.log(event.target); // 클릭된 요소
}
return (
<button onClick={(e) => handleClick(1, e)}>클릭</button>
);
}
이벤트 객체 순서: React는 이벤트 객체를 마지막 매개변수로 자동 전달하지 않으므로, 명시적으로 전달해야 한다.
가장 흔한 사용 사례는 배열을 렌더링할 때 각 항목에 고유한 값을 전달하는 것이다.
function TodoList() {
const todos = [
{ id: 1, text: "리액트 공부" },
{ id: 2, text: "운동하기" },
{ id: 3, text: "독서하기" }
];
function handleDelete(id) {
alert(`${id}번 할 일 삭제`);
}
function handleEdit(id, currentText) {
const newText = prompt("수정할 내용", currentText);
alert(`${id}번을 "${newText}"로 수정`);
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>삭제</button>
<button onClick={() => handleEdit(todo.id, todo.text)}>수정</button>
</li>
))}
</ul>
);
}
function App() {
function handleClick(id, name, event) {
alert(`${id}번 ${name} 클릭됨!`);
}
return (
<button onClick={handleClick.bind(null, 1, "버튼")}>클릭</button>
);
}
장점: 함수 참조가 일정하게 유지됨
단점: 가독성이 떨어지고, 현대적인 React에서는 잘 사용하지 않음
함수가 함수를 반환하는 패턴이다.
function App() {
function handleClick(id) {
return function(event) {
alert(`${id}번 클릭됨!`);
};
}
// handleClick(1)은 함수를 반환하므로 올바른 핸들러가 됨
return (
<button onClick={handleClick(1)}>클릭</button>
);
}
장점: 함수형 프로그래밍 스타일, 메모이제이션에 유리
단점: 초보자에게는 복잡해 보일 수 있음
DOM 요소에 데이터를 저장하고, 이벤트 객체를 통해 접근하는 방법이다.
function App() {
function handleClick(event) {
const id = event.currentTarget.dataset.id;
const name = event.currentTarget.dataset.name;
alert(`${id}번 ${name} 클릭됨!`);
}
return (
<div>
<button onClick={handleClick} data-id="1" data-name="첫번째">버튼 1</button>
<button onClick={handleClick} data-id="2" data-name="두번째">버튼 2</button>
</div>
);
}
장점:
단점:
React의 이벤트는 브라우저의 네이티브 이벤트를 감싼 합성 이벤트(SyntheticEvent)이다.
크로스 브라우저 호환성을 보장하며, 네이티브 이벤트와 동일한 인터페이스를 제공한다.
function App() {
function handleEvent(event) {
// 이벤트 정보
console.log(event.type); // 이벤트 타입 (예: "click")
console.log(event.target); // 이벤트가 발생한 요소
console.log(event.currentTarget); // 이벤트 핸들러가 연결된 요소
// 마우스 이벤트 정보
console.log(event.clientX); // 마우스 X 좌표
console.log(event.clientY); // 마우스 Y 좌표
// 키보드 이벤트 정보
console.log(event.key); // 눌린 키
console.log(event.code); // 키보드 코드
// 이벤트 제어
event.preventDefault(); // 기본 동작 막기
event.stopPropagation(); // 이벤트 버블링 중단
}
return <button onClick={handleEvent}>클릭</button>;
}
function App() {
function handleClick(event) {
console.log('target:', event.target); // 실제 클릭된 요소
console.log('currentTarget:', event.currentTarget); // 핸들러가 붙은 요소
}
return (
<div onClick={handleClick}>
<button>버튼</button>
</div>
);
}
<button>)<div>)합성 이벤트 객체를 통해 원본 브라우저 이벤트에 접근할 수 있다.
function App() {
function handleClick(event) {
const nativeEvent = event.nativeEvent;
console.log('React 합성 이벤트:', event);
console.log('브라우저 네이티브 이벤트:', nativeEvent);
}
return <button onClick={handleClick}>클릭</button>;
}
대부분의 경우 합성 이벤트만으로 충분하지만, 특수한 경우 네이티브 이벤트가 필요할 수 있다.
폼 제출이나 링크 클릭 같은 기본 동작을 막을 때 사용한다.
function Form() {
function handleSubmit(event) {
event.preventDefault(); // 페이지 새로고침 방지
console.log("폼 제출됨");
}
return (
<form onSubmit={handleSubmit}>
<input type="text" />
<button type="submit">제출</button>
</form>
);
}
이벤트가 부모 요소로 전파되는 것을 막을 때 사용한다.
function App() {
function handleParentClick() {
alert("부모 클릭됨");
}
function handleChildClick(event) {
event.stopPropagation(); // 부모로 이벤트 전파 중단
alert("자식 클릭됨");
}
return (
<div onClick={handleParentClick}>
부모 영역
<button onClick={handleChildClick}>자식 버튼</button>
</div>
);
}
버튼을 클릭하면 "자식 클릭됨"만 표시되고, "부모 클릭됨"은 표시되지 않는다.
이벤트 전파란 이벤트가 발생했을 때 해당 이벤트가 부모 또는 자식 요소로 전달되는 현상을 의미한다.
┌─────────────────────────────────┐
│ 1. 캡처링 (Capturing) │
│ 최상위 → 타겟 요소 │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 2. 타겟 (Target) │
│ 타겟 요소 자체 │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 3. 버블링 (Bubbling) │
│ 타겟 요소 → 최상위 │
└─────────────────────────────────┘
캡처링은 최상위 부모 요소에서 타겟 요소 방향으로 내려가며 이벤트가 전파되는 방식이다.
React에서 캡처링 단계 이벤트를 처리하려면 이벤트 이름 뒤에 Capture를 붙인다.
function App() {
function handleCaptureClick() {
console.log("캡처링 단계");
}
function handleBubbleClick() {
console.log("버블링 단계");
}
return (
<div onClickCapture={handleCaptureClick}>
<button onClick={handleBubbleClick}>클릭</button>
</div>
);
}
버튼을 클릭하면 콘솔에 다음 순서로 출력된다:
1. "캡처링 단계" (div의 onClickCapture)
2. "버블링 단계" (button의 onClick)
버블링은 타겟 요소에서 최상위 부모 요소 방향으로 올라가며 이벤트가 전파되는 방식이다.
버블링은 기본 동작이므로 별도의 키워드를 붙일 필요가 없다.
function App() {
function handleDivClick() {
console.log("div 클릭됨");
}
function handleButtonClick() {
console.log("button 클릭됨");
}
return (
<div onClick={handleDivClick}>
<button onClick={handleButtonClick}>클릭</button>
</div>
);
}
버튼을 클릭하면:
1. "button 클릭됨" (타겟 요소)
2. "div 클릭됨" (부모 요소로 버블링)
버블링을 막으려면 이벤트 핸들러 내부에서 stopPropagation() 메서드를 호출한다.
function App() {
function handleDivClick() {
console.log("div 클릭됨");
}
function handleButtonClick(e) {
e.stopPropagation(); // 버블링 중단
console.log("button 클릭됨");
}
return (
<div onClick={handleDivClick}>
<button onClick={handleButtonClick}>클릭</button>
</div>
);
}
이제 버튼을 클릭해도 "button 클릭됨"만 출력되고, div의 핸들러는 실행되지 않는다.
같은 요소에 여러 핸들러가 있을 때 나머지 핸들러 실행을 막으려면 네이티브 이벤트의 stopImmediatePropagation()을 사용한다.
function App() {
function handleClick1(e) {
e.nativeEvent.stopImmediatePropagation();
console.log("첫 번째 핸들러");
}
function handleClick2() {
console.log("두 번째 핸들러"); // 실행 안 됨
}
return (
<button
onClick={handleClick1}
onClickCapture={handleClick2}
>
클릭
</button>
);
}
function Modal({ onClose }) {
function handleOverlayClick(e) {
// 오버레이를 직접 클릭했을 때만 닫기
if (e.target === e.currentTarget) {
onClose();
}
}
function handleContentClick(e) {
// 모달 내용 클릭 시 닫히지 않도록 전파 중단
e.stopPropagation();
}
return (
<div className="overlay" onClick={handleOverlayClick}>
<div className="modal-content" onClick={handleContentClick}>
<h2>모달 제목</h2>
<p>모달 내용</p>
<button onClick={onClose}>닫기</button>
</div>
</div>
);
}
일관된 네이밍은 코드 가독성을 높인다.
function handleClick() { }
function handleSubmit() { }
function handleChange() { }
function handleMouseEnter() { }
function Button({ onClick, onSubmit }) {
return <button onClick={onClick}>제출</button>;
}
function App() {
function handleButtonClick() {
console.log("클릭됨");
}
return <Button onClick={handleButtonClick} />;
}
| 위치 | 패턴 | 예시 |
|---|---|---|
| 컴포넌트 내부 함수 | handle- | handleClick, handleSubmit |
| Props 이름 | on- | onClick, onSubmit |
| DOM 이벤트 속성 | on- (카멜케이스) | onClick, onChange |
function List({ items }) {
function handleClick(id) {
console.log(`${id} 클릭됨`);
}
return (
<ul>
{items.map(item => (
// ⚠️ 렌더링마다 새 함수 생성
<li key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
대부분의 경우 성능 문제가 되지 않지만, 아이템이 수천 개 이상이거나 자식 컴포넌트가 React.memo로 최적화된 경우 문제가 될 수 있다.
import { useCallback } from 'react';
function List({ items }) {
const handleClick = useCallback((id) => {
console.log(`${id} 클릭됨`);
}, []); // 의존성 배열
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
주의: 성능 최적화는 실제 성능 문제가 발생했을 때 적용하는 것이 좋다. 조기 최적화는 오히려 코드를 복잡하게 만들 수 있다.
onClick, onChange)onClick={handleClick}onClick={handleClick()}는 잘못된 코드onClick={() => handleClick(id)}handle-로 시작 (handleClick)on-으로 시작 (onClick)useCallback 고려import { useState } from 'react';
function LoginForm() {
const [formData, setFormData] = useState({
email: '',
password: ''
});
function handleChange(e) {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
}
function handleSubmit(e) {
e.preventDefault();
console.log('로그인 데이터:', formData);
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="이메일"
/>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="비밀번호"
/>
<button type="submit">로그인</button>
</form>
);
}
function SearchInput() {
function handleKeyDown(e) {
if (e.key === 'Enter') {
console.log('검색:', e.target.value);
} else if (e.key === 'Escape') {
e.target.value = '';
}
}
return (
<input
type="text"
placeholder="Enter로 검색, ESC로 초기화"
onKeyDown={handleKeyDown}
/>
);
}
import { useState } from 'react';
function HoverCard() {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
padding: '20px',
backgroundColor: isHovered ? 'lightblue' : 'white',
transition: 'background-color 0.3s'
}}
>
{isHovered ? '마우스가 올라와 있어요!' : '마우스를 올려보세요'}
</div>
);
}
function DragDropDemo() {
function handleDragStart(e) {
e.dataTransfer.setData('text/plain', e.target.id);
console.log('드래그 시작:', e.target.id);
}
function handleDragOver(e) {
e.preventDefault(); // 드롭 허용
}
function handleDrop(e) {
e.preventDefault();
const id = e.dataTransfer.getData('text/plain');
console.log('드롭됨:', id);
}
return (
<div>
<div
id="drag-item"
draggable
onDragStart={handleDragStart}
style={{ padding: '10px', background: 'lightgreen', cursor: 'move' }}
>
이것을 드래그하세요
</div>
<div
onDragOver={handleDragOver}
onDrop={handleDrop}
style={{
marginTop: '20px',
padding: '40px',
border: '2px dashed gray',
textAlign: 'center'
}}
>
여기에 드롭하세요
</div>
</div>
);
}
import { useState } from 'react';
function ContextMenuDemo() {
const [menuPos, setMenuPos] = useState(null);
function handleContextMenu(e) {
e.preventDefault();
setMenuPos({ x: e.clientX, y: e.clientY });
}
function handleClick() {
setMenuPos(null); // 메뉴 닫기
}
return (
<div onClick={handleClick}>
<div
onContextMenu={handleContextMenu}
style={{
padding: '40px',
background: 'lightgray',
textAlign: 'center'
}}
>
여기서 우클릭하세요
</div>
{menuPos && (
<div
style={{
position: 'fixed',
top: menuPos.y,
left: menuPos.x,
background: 'white',
border: '1px solid black',
padding: '10px',
zIndex: 1000
}}
>
<div>메뉴 항목 1</div>
<div>메뉴 항목 2</div>
<div>메뉴 항목 3</div>
</div>
)}
</div>
);
}
import { useState, useEffect } from 'react';
function InfiniteScroll() {
const [items, setItems] = useState(Array.from({ length: 20 }, (_, i) => i));
function handleScroll(e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
// 스크롤이 하단에 가까워지면
if (scrollHeight - scrollTop <= clientHeight + 100) {
console.log('더 로드!');
setItems(prev => [
...prev,
...Array.from({ length: 20 }, (_, i) => prev.length + i)
]);
}
}
return (
<div
onScroll={handleScroll}
style={{
height: '400px',
overflow: 'auto',
border: '1px solid black'
}}
>
{items.map(item => (
<div key={item} style={{ padding: '20px', borderBottom: '1px solid gray' }}>
아이템 {item}
</div>
))}
</div>
);
}
import { useRef } from 'react';
function FocusDemo() {
const inputRef = useRef(null);
function handleFocus() {
console.log('포커스 들어옴');
}
function handleBlur() {
console.log('포커스 빠져나감');
}
function focusInput() {
inputRef.current.focus();
}
return (
<div>
<input
ref={inputRef}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="포커스 테스트"
/>
<button onClick={focusInput}>인풋에 포커스</button>
</div>
);
}
// ❌ async를 직접 사용하면 안 됨
<button onClick={async () => await fetchData()}>클릭</button>
// ✅ 내부에서 async 함수 호출
function App() {
async function fetchData() {
const data = await fetch('/api/data');
return data.json();
}
function handleClick() {
fetchData().then(data => console.log(data));
}
return <button onClick={handleClick}>클릭</button>;
}
// ❌ 이전 상태를 직접 참조
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>+</button>
// ✅ 함수형 업데이트 사용 (권장)
<button onClick={() => setCount(prev => prev + 1)}>+</button>
function App() {
function handleClick(e) {
console.log('클릭됨');
}
function handleSpecialClick(e) {
if (e.shiftKey) {
console.log('Shift + 클릭');
} else if (e.ctrlKey) {
console.log('Ctrl + 클릭');
} else {
handleClick(e);
}
}
return <button onClick={handleSpecialClick}>클릭</button>;
}
function Button({ disabled, onClick }) {
function handleClick(e) {
if (disabled) {
e.preventDefault();
console.log('버튼이 비활성화됨');
return;
}
onClick(e);
}
return (
<button onClick={handleClick} disabled={disabled}>
클릭
</button>
);
}
import { useState, useCallback } from 'react';
function SearchInput() {
const [query, setQuery] = useState('');
// 디바운싱: 입력이 멈춘 후 실행
const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
};
const handleSearch = useCallback(
debounce((value) => {
console.log('검색:', value);
}, 500),
[]
);
function handleChange(e) {
const value = e.target.value;
setQuery(value);
handleSearch(value);
}
return (
<input
type="text"
value={query}
onChange={handleChange}
placeholder="검색어 입력"
/>
);
}
// ❌ 잘못된 코드
<button onClick={handleClick()}>클릭</button>
// ✅ 올바른 코드
<button onClick={handleClick}>클릭</button>
<button onClick={() => handleClick()}>클릭</button>
// ❌ 비동기에서 이벤트 객체 직접 사용
function handleClick(e) {
setTimeout(() => {
console.log(e.target); // 에러 또는 null
}, 1000);
}
// ✅ 필요한 값을 미리 저장
function handleClick(e) {
const target = e.target;
setTimeout(() => {
console.log(target); // 정상 작동
}, 1000);
}
// ❌ 조건문 밖에서 호출
function handleSubmit(e) {
e.preventDefault();
if (!isValid) return;
submitForm();
}
// ✅ 필요한 시점에 호출 (위 코드도 괜찮지만, 아래가 더 명확)
function handleSubmit(e) {
if (!isValid) return;
e.preventDefault();
submitForm();
}