3. 이벤트 핸들링 (Event Handling)
개념
이벤트는 웹페이지에서 발생하는 사용자의 행동이나 브라우저의 동작임
클릭, 키보드 입력, 스크롤, 폼 제출 등이 모두 이벤트임. 이벤트 핸들링은 이런 이벤트가 발생했을 때 실행할 코드(함수)를 연결하는 것!
1) 이벤트 리스너 등록 방법
const button = document.getElementById('myBtn');
// 방법 1: addEventListener (권장!)
button.addEventListener('click', function() {
console.log('버튼이 클릭되었습니다!');
});
설명:
- `addEventListener`(이벤트타입, 콜백함수)
- 첫 번째 인자: 이벤트 종류 (문자열) - 'click', 'keydown', 'submit' 등
- 두 번째 인자: 이벤트 발생 시 실행할 함수 (이벤트 핸들러)
// 화살표 함수로 더 간결하게
button.addEventListener('click', () => {
console.log('클릭!');
});
// 별도 함수로 분리 (재사용, 테스트 용이)
function handleClick() {
console.log('클릭!');
}
button.addEventListener('click', handleClick);
// 주의: 함수를 "호출"하면 안 됨!
button.addEventListener('click', handleClick()); // 잘못됨
button.addEventListener('click', handleClick); // 올바름
설명:
- `handleClick()`: 함수를 즉시 실행하고 그 반환값을 전달 (의도한 게 아님)
- `handleClick`: 함수 자체를 전달 (이벤트 발생 시 호출됨)
이벤트 리스너 제거
// 이벤트 리스너 제거
function handleClick() {
console.log('클릭!');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick); // 동일한 함수 참조여야 제거됨
// 이건 제거 안 됨! (다른 함수 객체이므로)
button.addEventListener('click', function() { console.log('클릭!'); });
button.removeEventListener('click', function() { console.log('클릭!'); }); //
여러 이벤트 리스너 등록
// 같은 요소에 여러 이벤트 리스너 등록 가능
button.addEventListener('click', () => console.log('핸들러 1'));
button.addEventListener('click', () => console.log('핸들러 2'));
// 클릭 시 둘 다 실행됨
// 다른 이벤트 타입도 등록 가능
button.addEventListener('mouseenter', () => console.log('마우스 진입'));
button.addEventListener('mouseleave', () => console.log('마우스 이탈'));
3-2) 주요 이벤트 종류
마우스 이벤트
const box = document.getElementById('box');
// 클릭 이벤트
box.addEventListener('click', () => {
console.log('클릭됨');
});
// 더블클릭
box.addEventListener('dblclick', () => {
console.log('더블클릭됨');
});
// 마우스 진입/이탈 (자식 요소 무시)
box.addEventListener('mouseenter', () => {
console.log('마우스가 들어왔습니다');
box.style.backgroundColor = 'yellow';
});
box.addEventListener('mouseleave', () => {
console.log('마우스가 나갔습니다');
box.style.backgroundColor = '';
});
// 마우스 이동
box.addEventListener('mousemove', (e) => {
console.log(`마우스 위치: ${e.clientX}, ${e.clientY}`);
});
// 마우스 버튼 누름/뗌
box.addEventListener('mousedown', () => console.log('마우스 버튼 누름'));
box.addEventListener('mouseup', () => console.log('마우스 버튼 뗌'));
키보드 이벤트
const input = document.getElementById('myInput');
// 키를 누를 때 (키가 눌려있는 동안 반복 발생 가능)
input.addEventListener('keydown', (e) => {
console.log('keydown:', e.key);
});
// 키를 뗄 때
input.addEventListener('keyup', (e) => {
console.log('keyup:', e.key);
});
// 실용 예시: Enter 키 감지
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
console.log('엔터가 눌렸습니다!');
console.log('입력값:', input.value);
}
});
// Escape 키로 입력 취소
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
input.value = '';
input.blur(); // 포커스 해제
}
});
폼 이벤트
const form = document.getElementById('myForm');
const input = document.getElementById('myInput');
const select = document.getElementById('mySelect');
// 폼 제출
form.addEventListener('submit', (e) => {
e.preventDefault(); // 페이지 새로고침 방지 (매우 중요!)
console.log('폼이 제출되었습니다');
// 여기서 데이터 처리...
});
// input 이벤트: 값이 변경될 때마다 실시간으로 발생
input.addEventListener('input', (e) => {
console.log('현재 입력값:', e.target.value);
});
// change 이벤트: 값이 변경되고 포커스를 잃을 때 발생
input.addEventListener('change', (e) => {
console.log('최종 값:', e.target.value);
});
// select, checkbox, radio는 change 이벤트가 더 적합
select.addEventListener('change', (e) => {
console.log('선택된 값:', e.target.value);
});
// 포커스 이벤트
input.addEventListener('focus', () => {
console.log('입력 필드에 포커스됨');
input.style.borderColor = 'blue';
});
input.addEventListener('blur', () => {
console.log('입력 필드에서 포커스 해제됨');
input.style.borderColor = '';
});
input vs change 차이
- `input`: 타이핑할 때마다 실시간 발생 (검색 자동완성 등에 사용)
- `change`: 입력을 완료하고 포커스를 잃을 때 한 번 발생 (폼 저장 등에 사용)
윈도우/문서 이벤트
// 페이지 로드 완료 (이미지 등 모든 리소스 포함)
window.addEventListener('load', () => {
console.log('페이지 완전히 로드됨');
});
// DOM만 로드 완료 (더 빠름, 보통 이걸 사용)
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 로드 완료');
// 여기서 DOM 조작 시작
});
// 스크롤
window.addEventListener('scroll', () => {
console.log('스크롤 위치:', window.scrollY);
});
// 창 크기 변경
window.addEventListener('resize', () => {
console.log('창 크기:', window.innerWidth, window.innerHeight);
});
3-3) 이벤트 객체 (Event Object)
이벤트가 발생하면 브라우저가 자동으로 이벤트에 대한 정보를 담은 객체를 핸들러에 전달
button.addEventListener('click', function(event) {
// event (또는 e로 줄여서 씀)에 이벤트 정보가 담겨 있음
console.log(event);
});
이벤트 객체의 주요 속성
element.addEventListener('click', function(e) {
// 이벤트 종류
console.log(e.type); // 'click'
// 이벤트가 발생한 요소 (실제로 클릭된 요소)
console.log(e.target); // 클릭된 요소
// 이벤트 리스너가 등록된 요소
console.log(e.currentTarget); // 핸들러가 붙은 요소
// 마우스 좌표
console.log(e.clientX); // 뷰포트 기준 X 좌표
console.log(e.clientY); // 뷰포트 기준 Y 좌표
console.log(e.pageX); // 문서 기준 X 좌표 (스크롤 포함)
console.log(e.pageY); // 문서 기준 Y 좌표
// 이벤트 발생 시간
console.log(e.timeStamp); // 밀리초 단위 타임스탬프
});
target vs currentTarget:
<div id="outer">
<button id="inner">클릭</button>
</div>
document.getElementById('outer').addEventListener('click', function(e) {
console.log('target:', e.target.id); // 'inner' (실제 클릭된 요소)
console.log('currentTarget:', e.currentTarget.id); // 'outer' (핸들러가 붙은 요소)
});
설명:
- 버튼을 클릭하면 버블링으로 인해 div의 핸들러도 실행됨
- `target`: 실제로 클릭한 요소 (button)
- `currentTarget`: 이 핸들러가 등록된 요소 (div)
- 이벤트 위임 패턴에서 이 차이가 중요함
키보드 이벤트 객체
input.addEventListener('keydown', function(e) {
console.log(e.key); // 눌린 키: 'a', 'Enter', 'Escape', 'ArrowUp' 등
console.log(e.code); // 물리적 키 코드: 'KeyA', 'Enter', 'Escape' 등
console.log(e.shiftKey); // Shift 키 눌림 여부 (true/false)
console.log(e.ctrlKey); // Ctrl 키 눌림 여부
console.log(e.altKey); // Alt 키 눌림 여부
console.log(e.metaKey); // Meta 키 (Mac의 Cmd) 눌림 여부
// 단축키 구현 예시: Ctrl + S
if (e.ctrlKey && e.key === 's') {
e.preventDefault(); // 브라우저 기본 저장 대화상자 방지
console.log('저장 단축키!');
}
});
key vs code 차이:
- `key`: 입력된 문자 ('a', 'A', 'ㄱ' 등 - 언어/Shift 영향 받음)
- `code`: 물리적 키 위치 ('KeyA' - 어떤 키보드든 동일)
4) 기본 동작 방지 & 이벤트 전파 중단
preventDefault() - 기본 동작 방지
// 1. 폼 제출 시 페이지 새로고침 방지
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
e.preventDefault(); // 폼의 기본 동작(페이지 이동) 막기
console.log('폼 데이터를 JavaScript로 처리합니다');
// fetch로 서버에 데이터 전송 등...
});
설명
- `<form>`의 기본 동작: 제출 시 지정된 URL로 이동 또는 새로고침
- SPA(Single Page Application)에서는 이걸 막고 JavaScript로 처리
- React에서 폼 다룰 때도 항상 `e.preventDefault()` 사용
// 2. 링크 클릭 시 페이지 이동 방지
const link = document.querySelector('a');
link.addEventListener('click', function(e) {
e.preventDefault(); // 링크 이동 막기
console.log('링크 클릭됨, 하지만 이동 안 함');
// 대신 모달을 띄우거나 다른 작업 수행
});
// 3. 우클릭 메뉴 막기
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
console.log('커스텀 메뉴를 띄울 수 있음');
});
// 4. 특정 키 입력 막기 (숫자만 입력 가능하게)
input.addEventListener('keydown', function(e) {
// 숫자가 아니면 입력 막기
if (!/[0-9]/.test(e.key) && e.key !== 'Backspace') {
e.preventDefault();
}
});
stopPropagation() - 이벤트 버블링 중단
<div id="outer">
<div id="inner">
<button id="btn">클릭</button>
</div>
</div>
document.getElementById('outer').addEventListener('click', () => {
console.log('outer 클릭!');
});
document.getElementById('inner').addEventListener('click', () => {
console.log('inner 클릭!');
});
document.getElementById('btn').addEventListener('click', (e) => {
e.stopPropagation(); // 버블링 중단!
console.log('button 클릭!');
});
// 버튼 클릭 시 출력:
// "button 클릭!" 만 출력됨
// inner, outer로는 이벤트가 전파되지 않음
설명
- `stopPropagation()`을 호출하면 이벤트가 부모로 전파되지 않음
- 특정 요소에서만 이벤트를 처리하고 싶을 때 사용
- 남용하면 디버깅이 어려워지므로 꼭 필요할 때만 사용
5) 이벤트 버블링 & 캡처링
이벤트 버블링 (Bubbling): 이벤트가 발생하면 해당 요소에서 시작해서 부모 요소들로 전파됨
<div id="grandparent">
<div id="parent">
<button id="child">클릭</button>
</div>
</div>
document.getElementById('grandparent').addEventListener('click', () => {
console.log('grandparent');
});
document.getElementById('parent').addEventListener('click', () => {
console.log('parent');
});
document.getElementById('child').addEventListener('click', () => {
console.log('child');
});
// 버튼 클릭 시 출력 순서:
// child
// parent
// grandparent
설명
- 버튼을 클릭하면 버튼 → 부모 div → 조부모 div 순서로 이벤트 전파
- 마치 거품이 올라가는 것처럼 아래에서 위로 전파 (그래서 "버블링")
- 이게 기본 동작이고, 이벤트 위임 패턴의 핵심 원리
이벤트 캡처링 (Capturing): 버블링과 반대로 위에서 아래로 전파되는 단계
// 세 번째 인자를 true로 설정하면 캡처링 단계에서 실행
document.getElementById('grandparent').addEventListener('click', () => {
console.log('grandparent (캡처링)');
}, true); // true = 캡처링 단계에서 실행
document.getElementById('parent').addEventListener('click', () => {
console.log('parent (캡처링)');
}, true);
document.getElementById('child').addEventListener('click', () => {
console.log('child');
}); // 기본값 false = 버블링 단계에서 실행
// 버튼 클릭 시 출력 순서:
// grandparent (캡처링) - 캡처링: 위에서 아래로
// parent (캡처링)
// child - 타겟 단계
// (버블링 핸들러가 있다면 여기서 아래→위로)
이벤트 전파 3단계
- 캡처링 단계: window → document → ... → 타겟의 부모 (위→아래)
- 타겟 단계: 실제 이벤트가 발생한 요소
- 버블링 단계: 타겟의 부모 → ... → document → window (아래→위)
실무에서는 캡처링을 거의 사용하지 않고, 대부분 버블링만 사용.
3-6) 이벤트 위임 (Event Delegation)
개념: 이벤트 버블링을 활용해서 부모 요소 하나에만 이벤트 리스너를 등록하고, 자식 요소들의 이벤트를 처리하는 패턴.
왜 중요한가?
- 성능: 리스너 개수를 줄여 메모리 절약
- 동적 요소: 나중에 추가되는 요소도 자동으로 처리됨
- 코드 간결: 하나의 핸들러로 여러 요소 처리
<ul id="todoList">
<li>할 일 1 <button class="delete">삭제</button></li>
<li>할 일 2 <button class="delete">삭제</button></li>
<li>할 일 3 <button class="delete">삭제</button></li>
</ul>
안 좋은 방법 (각 버튼에 개별 리스너)
const deleteButtons = document.querySelectorAll('.delete');
deleteButtons.forEach(btn => {
btn.addEventListener('click', function() {
this.parentElement.remove();
});
});
// 문제점:
// 1. 버튼 100개면 리스너도 100개 (메모리 낭비)
// 2. 나중에 동적으로 추가된 li에는 이벤트가 안 붙음!
좋은 방법 (이벤트 위임)
const todoList = document.getElementById('todoList');
todoList.addEventListener('click', function(e) {
// 클릭된 요소가 delete 버튼인지 확인
if (e.target.classList.contains('delete')) {
// 버튼의 부모(li)를 삭제
e.target.parentElement.remove();
}
});
동작 설명
- 삭제 버튼을 클릭함
- 이벤트가 버블링되어 ul까지 올라감
- ul에 등록된 핸들러가 실행됨
- `e.target`으로 실제 클릭된 요소(버튼)를 확인
- 버튼이 맞으면 해당 작업 수행
장점
- 리스너가 1개뿐 (버튼이 몇 개든 상관없음)
- 나중에 li가 추가되어도 자동으로 동작함!
// 나중에 추가된 요소도 잘 동작함
const newLi = document.createElement('li');
newLi.innerHTML = '새 할 일 <button class="delete">삭제</button>';
todoList.appendChild(newLi);
// 새 삭제 버튼도 클릭하면 잘 삭제됨!
더 복잡한 이벤트 위임 예시
<ul id="todoList">
<li data-id="1">
<span class="todo-text">할 일 1</span>
<button class="edit">수정</button>
<button class="delete">삭제</button>
<button class="complete">완료</button>
</li>
<!-- ... -->
</ul>
const todoList = document.getElementById('todoList');
todoList.addEventListener('click', function(e) {
const target = e.target;
const li = target.closest('li'); // 가장 가까운 li 조상 찾기
if (!li) return; // li 안의 요소가 아니면 무시
const todoId = li.dataset.id; // data-id 값 가져오기
if (target.classList.contains('delete')) {
console.log(`${todoId}번 삭제`);
li.remove();
}
if (target.classList.contains('edit')) {
console.log(`${todoId}번 수정`);
// 수정 로직...
}
if (target.classList.contains('complete')) {
console.log(`${todoId}번 완료`);
li.querySelector('.todo-text').classList.toggle('completed');
}
});
closest() 메서드
- `e.target.closest('li')`: target에서 시작해서 위로 올라가며 'li'를 찾음
- 버튼 안에 아이콘이 있어도 안전하게 li를 찾을 수 있음