자바스크립트 실전 #2 이벤트 핸들링과 위임
#1 DOM 조작 기본 에서 엘리먼트를 다루는 도구를 봤다면, 이번엔 그것들로 사용자 인터랙션을 처리하는 단계로 나아갑니다.
이벤트 등록 — addEventListener
#
const btn = document.querySelector('#submit');
btn.addEventListener('click', (e) => {
console.log('클릭됨');
});addEventListener('타입', 핸들러) — 이게 모던 자바스크립트의 표준입니다.
옛 코드에는 btn.onclick = () => {...}같이 직접 할당하는 방식도 보이는데, 한 엘리먼트에 한 핸들러만 등록할 수 있어 한계가 있습니다. addEventListener는 같은 이벤트에 여러 핸들러를 등록할 수 있습니다.
자주 쓰는 이벤트 타입 #
click 마우스 클릭
input 입력 필드 값 변경 (실시간)
change 값 변경 (포커스 잃을 때)
submit 폼 제출
keydown/keyup 키 입력
focus/blur 포커스 받기/잃기
mouseenter 마우스 진입
mouseleave 마우스 떠남
scroll 스크롤
load 로딩 완료
DOMContentLoaded HTML 파싱 완료이벤트 객체 — e
#
핸들러가 받는 인자는 이벤트 객체. 자주 쓰는 속성들.
btn.addEventListener('click', (e) => {
e.target; // 이벤트가 시작된 엘리먼트
e.currentTarget; // 핸들러가 등록된 엘리먼트
e.type; // 'click'
e.preventDefault(); // 기본 동작 막기
e.stopPropagation(); // 버블링 막기
});e.target vs e.currentTarget
#
가장 헷갈리는 부분입니다.
<button class="card">
<span>클릭하세요</span>
</button>btn.addEventListener('click', (e) => {
console.log(e.target); // <span> (실제 클릭된 엘리먼트)
console.log(e.currentTarget); // <button> (핸들러 등록된 엘리먼트)
});target— 이벤트가 발생한 엘리먼트 (가장 안쪽 클릭 대상)currentTarget— 핸들러가 등록된 엘리먼트
이벤트 위임 패턴에서 둘의 차이가 중요합니다. 잠시 뒤에 살펴보겠습니다.
이벤트의 흐름 — 캡처링과 버블링 #
이벤트가 발생하면 두 단계로 전파됩니다.
1. 캡처링 (capturing) — document → 이벤트 발생 위치까지 위→아래
2. 타깃 (target) — 발생 위치 도착
3. 버블링 (bubbling) — 발생 위치 → document까지 아래→위기본은 버블링 단계에서 핸들러가 실행됩니다.
<div id="outer">
<div id="inner">
<button id="btn">클릭</button>
</div>
</div>document.querySelector('#outer').addEventListener('click', () => console.log('outer'));
document.querySelector('#inner').addEventListener('click', () => console.log('inner'));
document.querySelector('#btn').addEventListener('click', () => console.log('button'));
// 버튼 클릭 시 출력:
// button
// inner
// outer
이벤트가 버튼 → inner → outer로 위로 흘러올라가며 각 핸들러가 차례로 실행됩니다. 이게 버블링입니다.
캡처링으로 듣기 #
document.querySelector('#outer').addEventListener(
'click',
() => console.log('outer (캡처)'),
{ capture: true }
);세 번째 인자에 { capture: true }를 주면 캡처 단계에서 핸들러가 실행됩니다. 거의 안 쓰지만, “위쪽이 먼저 처리해야 하는 경우"가 있을 때 가끔 유용합니다.
stopPropagation과 preventDefault
#
stopPropagation — 전파 멈추기
#
button.addEventListener('click', (e) => {
e.stopPropagation(); // 더 위로 안 올라감
});위 핸들러에서 stopPropagation을 부르면 — outer/inner 핸들러는 호출되지 않습니다.
preventDefault — 기본 동작 막기
#
form.addEventListener('submit', (e) => {
e.preventDefault(); // 폼이 페이지를 새로고침하지 않음
// ... 직접 처리
});
link.addEventListener('click', (e) => {
e.preventDefault(); // 링크가 이동하지 않음
});브라우저 기본 동작을 막습니다. 폼 제출, 링크 이동, 우클릭 메뉴 등 자주 만나는 경우.
두 메서드는 목적이 다릅니다. stopPropagation은 자바스크립트 핸들러 전파를, preventDefault는 브라우저 기본 동작을. 헷갈리지 않게 따로 두어 사용합니다.
이벤트 위임 — 효율적인 패턴 #
리스트 안에 100개 항목이 있고 각각 클릭 핸들러가 필요하다면:
안 좋은 패턴 — 항목마다 핸들러 #
document.querySelectorAll('.item').forEach((item) => {
item.addEventListener('click', (e) => {
console.log('클릭:', item.dataset.id);
});
});100개 핸들러가 메모리에 등록됩니다. 리스트에 동적으로 항목이 추가되면 새 항목엔 핸들러가 없습니다.
좋은 패턴 — 부모에 한 핸들러 #
const list = document.querySelector('.list');
list.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (!item || !list.contains(item)) return;
console.log('클릭:', item.dataset.id);
});핸들러는 하나만. 클릭이 일어나면 버블링으로 부모까지 올라오고, 거기서 closest로 어떤 항목이 클릭됐는지 확인합니다.
위임의 이점 #
- 메모리 절약 — 핸들러가 하나
- 동적 항목 자동 처리 — 나중에 추가된 항목도 같은 핸들러로 동작
- 간결함 — 리스트가 변할 때마다 핸들러 등록/해제 안 해도 됨
리스트, 테이블, 카드 그리드 같은 경우에 거의 표준 패턴입니다.
핸들러 제거 — removeEventListener
#
function onClick() {
console.log('클릭');
}
btn.addEventListener('click', onClick);
// 나중에
btn.removeEventListener('click', onClick);핸들러 함수의 참조가 같아야 제거됩니다. 화살표 함수를 즉석에서 등록하면 제거할 수 없습니다.
btn.addEventListener('click', () => console.log('클릭'));
btn.removeEventListener('click', () => console.log('클릭')); // ✗ 다른 함수
AbortController — 모던 핸들러 정리
#
중급 #6 fetch API 에서 본 AbortController가 이벤트 정리에도 쓰입니다.
const controller = new AbortController();
btn.addEventListener('click', onClick, { signal: controller.signal });
input.addEventListener('input', onInput, { signal: controller.signal });
window.addEventListener('resize', onResize, { signal: controller.signal });
// 한 번에 모두 제거
controller.abort();여러 핸들러를 한 번에 정리할 수 있습니다. 컴포넌트 cleanup에 매우 적합합니다.
한 번만 실행 — { once: true }
#
btn.addEventListener('click', () => {
console.log('처음이자 마지막');
}, { once: true });자기 자신을 자동으로 제거합니다. 일회성 핸들러에 매우 깔끔.
Passive 이벤트 — 스크롤 성능 #
scroll, touchmove 같은 이벤트는 passive로 등록하는 게 권장됩니다.
window.addEventListener('scroll', onScroll, { passive: true });passive 핸들러는 preventDefault를 못 부른다는 약속입니다. 그 약속이 있으면 브라우저가 스크롤을 더 빠르게 처리할 수 있습니다.
폼/입력 자주 쓰는 패턴 #
input vs change
#
input.addEventListener('input', (e) => {
// 매 키 입력마다
console.log(e.target.value);
});
input.addEventListener('change', (e) => {
// 입력이 끝나고 포커스 잃을 때 (또는 select 변경 시)
console.log(e.target.value);
});검색 자동완성처럼 실시간 처리는 input. 검증/저장 같은 무거운 작업은 change 또는 디바운스된 input.
키보드 이벤트 #
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') submit();
if (e.key === 'Escape') close();
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
save();
}
});e.key가 모던 표준. 옛날의 e.keyCode는 deprecated.
커스텀 이벤트 #
자기만의 이벤트를 만들고 dispatch 할 수 있습니다.
const event = new CustomEvent('user-login', {
detail: { id: 'u1', name: '커티스' },
});
document.dispatchEvent(event);
// 다른 곳에서
document.addEventListener('user-login', (e) => {
console.log(e.detail); // { id: 'u1', name: '커티스' }
});큰 앱에서 모듈 사이 통신에 가끔 활용합니다. 다만 너무 많이 쓰면 흐름 추적이 어려워지니 신중하게.
마무리 #
이번 글에서 정리한 내용:
addEventListener가 모던 표준e.target(실제 발생) vse.currentTarget(핸들러 등록 위치)- 이벤트 흐름: 캡처링 → 타깃 → 버블링 (기본은 버블링)
stopPropagation은 전파,preventDefault는 기본 동작- 이벤트 위임 — 부모에 한 번 등록,
closest로 위치 확인 removeEventListener는 같은 함수 참조 필요AbortController로 여러 핸들러 한 번에 정리{ once: true },{ passive: true }옵션input은 실시간,change는 입력 완료 시점e.key가 키보드 이벤트 모던 표준
다음 글(#3 fetch와 비동기 UI)에서는 fetch로 데이터를 받아 DOM에 반영하는 패턴 — 로딩/에러 상태 표시, 디바운스, AbortController까지 묶어서 다룹니다.