이벤트
이벤트 리스너 (addEventListener, removeEventListener)
addEventListener — 이벤트 등록
const btn = document.querySelector("#btn");
btn.addEventListener("click", () => {
console.log("클릭됨");
});같은 요소에 여러 개의 리스너를 등록할 수도 있다.
btn.addEventListener("click", () => console.log("첫 번째"));
btn.addEventListener("click", () => console.log("두 번째"));
// 클릭하면 둘 다 실행됨removeEventListener — 이벤트 제거
등록할 때 사용한 것과 **동일한 함수(익명 함수 X)**를 넘겨야 제거된다.
function handleClick() {
console.log("클릭됨");
}
btn.addEventListener("click", handleClick);
btn.removeEventListener("click", handleClick); // 정상 제거
btn.addEventListener("click", () => console.log("클릭"));
btn.removeEventListener("click", () => console.log("클릭")); // 제거 안 됨 (다른 함수로 취급)옵션 — once, capture
btn.addEventListener("click", handleClick, { once: true }); // 한 번만 실행되고 자동 제거
btn.addEventListener("click", handleClick, { capture: true }); // 캡처링 단계에서 실행(`capture`는 이벤트 전파 방식과 관련된 옵션)이벤트 객체 (event.target, preventDefault, stopPropagation)
이벤트 콜백의 매개변수로 전달되는 객체에는 이벤트에 대한 다양한 정보가 담겨있다.
event.target — 실제로 이벤트가 발생한 요소
document.querySelector("#list").addEventListener("click", (event) => {
console.log(event.target); // 클릭된 실제 요소
console.log(event.target.textContent);
});preventDefault — 기본 동작 막기
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
event.preventDefault(); // 폼 제출 시 페이지가 새로고침되는 기본 동작을 막음
console.log("폼 제출 처리");
});const link = document.querySelector("a");
link.addEventListener("click", (event) => {
event.preventDefault(); // 링크 이동 막기
});stopPropagation — 이벤트 전파 막기
child.addEventListener("click", (event) => {
event.stopPropagation(); // 이 이벤트가 부모로 전파되는 것을 막음
console.log("child만 실행됨");
});버블링과 캡처링 (이벤트 전파 방식)
하나의 요소에서 이벤트가 발생하면, 그 이벤트는 DOM 트리를 따라 순서대로 전파된다.
<div id="outer">
<div id="inner">
<button id="btn">클릭</button>
</div>
</div>버블링 (Bubbling) — 기본 동작, 안쪽에서 바깥쪽으로
outer.addEventListener("click", () => console.log("outer"));
inner.addEventListener("click", () => console.log("inner"));
btn.addEventListener("click", () => console.log("btn"));
// btn 클릭 시 실행 순서: btn → inner → outer이벤트가 발생한 요소에서 시작해서 부모 방향으로 계속 전파된다.
캡처링 (Capturing) — 바깥쪽에서 안쪽으로
addEventListener의 세 번째 인자를 true로 주면 캡처링 단계에서 실행된다.
outer.addEventListener("click", () => console.log("outer"), { capture: true });
inner.addEventListener("click", () => console.log("inner"), { capture: true });
btn.addEventListener("click", () => console.log("btn"), { capture: true });
// btn 클릭 시 실행 순서: outer → inner → btn
이벤트 위임 (Event Delegation)
자식 요소마다 각각 이벤트 리스너를 등록하는 대신, 부모 요소 하나에만 리스너를 등록하고 버블링을 이용해 처리하는 방식이다.
문제 상황 — 항목마다 리스너를 등록하면
const items = document.querySelectorAll(".item");
items.forEach((item) => {
item.addEventListener("click", () => console.log(item.textContent));
});
// 나중에 새로 추가되는 .item에는 리스너가 자동으로 붙지 않음이벤트 위임으로 개선
const list = document.querySelector("#list");
list.addEventListener("click", (event) => {
if (event.target.classList.contains("item")) {
console.log(event.target.textContent);
}
});list에만 리스너를 하나 등록해두면, 이후에 .item이 새로 추가되더라도 버블링을 통해 list까지 이벤트가 전달되기 때문에 별도로 리스너를 다시 붙일 필요가 없다.
실전 예시 — 동적으로 추가되는 삭제 버튼 처리
const list = document.querySelector("#itemList");
list.addEventListener("click", (event) => {
if (event.target.matches(".delete-btn")) {
event.target.closest("li").remove();
}
});
function addItem(name) {
list.insertAdjacentHTML(
"beforeend",
`<li>${name} <button class="delete-btn">삭제</button></li>`
);
}
addItem("gori");새로 추가되는 삭제 버튼마다 리스너를 따로 등록하지 않아도, 부모(list)의 리스너 하나가 모든 클릭을 처리한다. 메모리 사용량도 줄고 코드도 간결해져서 실무에서 자주 쓰이는 패턴이다.