mimizae 님의 블로그
JavaScript의 Event (2) 본문
이전 글을 보고 와 주시면 감사하겠습니다! 🤩

이벤트 전파란?
DOM 트리 상에 존재하는 DOM 요소 노드에서 발생한 이벤트는 이 트리를 타고 전파된다!
<!D0CTYPE html>
<html>
<body>
<ul id="fruits">
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">0range</li>
</ul>
</body>
</html>
이 예제를 살펴 보며 빠르게 이해해 보자!
ul 요소의 두 번째 자식 요소인 li 요소를 클릭해 이벤트를 발생시키면 이벤트 객체가 생성이 된다.
이렇게 생성된 이벤트 객체는 이벤트를 발생시킨 DOM 요소인 이벤트 타깃을 중심으로 DOM 트리를 통해 전파된다.
이벤트 전파는 객체가 전파되는 방향!!에 따라 아래의 그림과 같이 3단계로 구분할 수 있다.

- 캡처링 단계: 이벤트가 상위 요소에서 하위 요소 방향으로 전파
- 타깃 단계: 이벤트가 이벤트 타깃에 도달
- 버블링 단계: 이벤트가 하위 요소에서 상위 요소 방향으로 전파
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<script>
const $fruit = document.getElementById('fruits');
// $fruit 요소의 하위 요소인 li를 클릭한 경우
$fruit.addEventListener('click', e => {
console.log(`이벤트 단계: ${e.eventPhase}`); // 3: 버블링 단계
console.log(`이벤트 단계: ${e.target}`); // [object HTMLElement]
console.log(`이벤트 단계: ${e.eventPhase}`); // [object HTMLUListElement]
});
</script>
</body>
</html>

li 요소를 클릭하면 클릭 이벤트가 발생하여 클릭 이벤트 객체가 생성되고 클릭된 li 요소가 이벤트 타깃이 된다.
이렇게 생성된 클릭 이벤트 객체는 window에서 이벤트 타깃 방향으로 전파된다. 이것이 캡처링 단계이다.
전파되던 이벤트 객체는 이벤트를 발생시킨 이벤트 타깃에 도달한다. 이것이 타깃 단계이다.
마지막으로 타깃에서 시작해 다시 window 방향으로 전파된다. 이것이 버블링 단계이다.
오 근데 왜 3이 계속 뜰까? 왜 버블링 단계만 콘솔에 뜨는 거지?
addEventListener 메서드 방식으로 등록한 이벤트 핸들러는 타깃 단계와 버블링 단계뿐만 아니라 캡처링 단계의 이벤트도 선별적으로 캐치할 수 있다.
캡처링 단계의 이벤트를 캐치하려면 addEventListener 메서드의 3번째 인수로 true를 전달해야 한다.
3번째 인수를 생략하거나 false를 전달하면 타깃 단계와 버블링 단계의 이벤트만 캐치할 수 있다!!!!!!
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<script>
const $fruits = document.getElementById('fruits');
const $banana = document.getElementById('banana');
// fruits의 하위 li 클릭 시 캡처링 단계에서 이벤트를 잡는다 (capture: true)
$fruits.addEventListener('click', e => {
console.log(`이벤트 단계: ${e.eventPhase}`); // 1 = 캡처링
console.log('이벤트 타깃:', e.target); // 실제 클릭된 요소 (li)
console.log('커런트 타깃:', e.currentTarget); // 리스너가 붙은 요소 (ul)
}, true);
// 타깃 단계(실제 타깃)에 붙인 리스너
$banana.addEventListener('click', e => {
console.log(`이벤트 단계: ${e.eventPhase}`); // 2 = 타깃 단계
console.log('이벤트 타깃:', e.target);
console.log('커런트 타깃:', e.currentTarget); // 이 경우 currentTarget === target (li)
});
// fruits의 버블링 단계에서 이벤트를 잡는다 (capture: false — 기본값)
$fruits.addEventListener('click', e => {
console.log(`이벤트 단계: ${e.eventPhase}`); // 3 = 버블링
console.log('이벤트 타깃:', e.target);
console.log('커런트 타깃:', e.currentTarget); // ul
});
</script>
</body>
</html>


이 예제에서 알 수 있는 것은,
이벤트를 발생시킨 이벤트 타깃과 이벤트 핸들러가 바인딩된 커런트 타깃이 같은 DOM 요소라면 이벤트 핸들러는 타깃 단계의 이벤트 객체를 캐치한다는 것이다!!
😅 이게 무슨 말이냐면...
- event.target → 실제로 이벤트가 발생한 요소 (사용자가 클릭한 요소)
- event.currentTarget → 이벤트 핸들러가 바인딩된 요소
<ul id="fruits">
<li id="banana">Banana</li>
</ul>
const $banana = document.getElementById('banana');
$banana.addEventListener('click', e => {
console.log(e.eventPhase);
});
이렇게 이벤트 핸들러를 banana li에 등록했다고 가정하고
화면에서 Banana를 클릭하면!!!!
→ event.target === event.currentTarget === li#banana
핸들러가 바인딩 된 요소도, 실제로 클릭이 된 요소도 모두 banana li이기 때문이다.
즉, 이벤트 핸들러는 타깃 단계에서 실행 되어 event.eventPhase === 2 가 출력된다!
반대로 아래와 같이 상위 요소인 ul에 핸들러를 등록하고
$fruits.addEventListener('click', e => {
console.log(e.eventPhase);
});
li 요소를 클릭하면,
- event.target = li (실제 클릭된 건 li 요소)
- event.currentTarget = ul (그러나 핸들러가 바인딩 된 요소는 상위 요소인 ul!!!)
같은 DOM 요소가 아니므로 이벤트는 캡처 또는 버블링 단계에서 실행된다.
이벤트 위임
사용자가 네비게이션 아이템(li 요소)를 클릭해 선택하면 현재 선택된 내비게이션 아이템에 active 클래스를 추가하고 그 외의 모든 내비게이션 아이템의 active 클래스는 제거하는 예제를 살펴보자!!
<!DOCTYPE html>
<html>
<head>
<style>
#fruits {
display: flex;
list-style-type: none;
padding: 0;
}
#fruits li {
width: 100px;
cursor: pointer;
}
#fruits .active {
color: red;
text-decoration: underline;
}
</style>
</head>
<body>
<nav>
<ul id="fruits">
<li id="apple" class="active">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
</nav>
<div>선택된 내비게이션 아이템: <em class="msg">apple</em></div>
<script>
const $fruits = document.getElementById('fruits');
const $message = document.querySelector('.msg');
// 사용자의 클릭에 의해 선택된 내비게이션 아이템(li 요소)에 active 클래스를 추가하고
// 그 외의 모든 내비게이션 아이템의 active 클래스를 제거한다.
function activate({target}){
[...$fruits.children].forEach($fruit => {
$fruits.classList.toggle('active', $fruit === target);
$message.textContent = target.id;
});
}
// 모든 내비게이션 아이템에 이벤트 핸들러를 등록
document.getElementById('apple').onclick = activate;
document.getElementById('banana').onclick = activate;
document.getElementById('oragne').onclick = activate;
</script>
</body>

이 예제는 모든 내비게이션 아이템(li 요소)이 클릭 이벤트에 반응하도록 모든 내비게이션 아이템에 이벤트 핸들러인 activate를 등록했다.
만일 내비게이션 아이템이 100개라면 100개의 이벤트 핸들러를 등록해야 한다.
너무 복잡하고 유지보수에도 안 좋지 않을까??
바로 이 지점에서 활용되는 개념이 이벤트 위임이다!!
여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신!!!! 하나의 상위 DOM 요소에 이벤트 핸들러를 등록하는 방법을 말한다.
이벤트 위임을 통해 상위 DOM 요소에 이벤트 핸들러를 등록하면 여러 개의 하위 DOM 요소에 이벤트 핸들러를 등록할 필요가 없다.
또한 동적으로 하위 DOM 요소를 추가하더라도 일일이 추가된 DOM 요소에 이벤트 핸들러를 등록할 필요가 없다!
이건 이벤트 전파(캡처링 → 타깃 → 버블링) 덕분에 가능한 일인데!
하위 요소에서 발생한 이벤트가 상위 요소까지 버블링 되기 때문에!!!!! 상위 요소에 이벤트 핸들러를 등록해도 하위 요소의 이벤트를 감지할 수 있다.
이벤트 위임을 사용하여 위 예제를 수정해 보자.
//... 동일
<script>
const $fruits = document.getElementById('fruits');
const $msg = document.querySelector('.msg');
function activate({ target }) {
// 이벤트를 발생시킨 요소(target)가 ul#fruits의 자식 요소가 아니라면 무시한다.
if (!target.matches('#fruits > li')) return;
[...$fruits.children].forEach($fruit => {
$fruit.classList.toggle('active', $fruit === target);
});
$msg.textContent = target.id;
}
// 이벤트 위임: 상위 요소(ul#fruits)는 하위 요소의 이벤트를 캐치!!!!할 수 있다.
$fruits.onclick = activate;
</script>
이벤트 위임을 통해 하위 DOM 요소에서 발생한 이벤트를 처리할 때 주의할 점은 상위 요소에 이벤트 핸들러를 등록하기 때문에
이벤트 타깃, 즉 이벤트를 실제로 발생시킨 DOM 요소가 개발자가 기대한 DOM 요소가 아닐 수도 있다는 것이다.
위 예제의 경우 ul#fruits 요소에 바인딩된 이벤트 핸들러는 자기 자신은 물론 ul#fruits 요소의 하위 요소 중에서 클릭 이벤트를 발생시킨 모든 DOM 요소에 반응한다.
따라서 이벤트에 반응이 필요한 특정 DOM 요소 (위 예제의 경우, ’#fruits > li, 선택자에 의해 선택되는 DOM 요소)에 한정하여 이벤트 핸들러가 실행되도록 이벤트 타깃을 검사할 필요가 있다.
예를 들어서 좀 더 쉽게 말해 보자면!!!!
ul#fruits에 클릭 이벤트 핸들러를 등록했을 때 버블링의 특성 때문에...
- li를 클릭했을 때도 실행 되고
- li 안의 텍스트(텍스트 노드) 같은 걸 클릭해도 실행 되고
- 심지어 li 안에 <span> <strong> <img> 같은 요소가 추가되면 그것을 클릭해도 실행된다.
즉, 이벤트 타깃(e.target)이 항상 li 라는 보장이 없다!!
<li id="apple"><span>Apple</span></li>
여기서 <span>을 클릭하면?
- e.target → span (li 아님!!!!!!)
- e.currentTarget → ul (이벤트 핸들러가 바인딩된 요소)
이렇게 되면 activate 함수에서 $fruit === target 비교가 깨진다...!!!!
DOM 요소의 기본 동작 조작
- preventDefault
DOM 요소는 저마다 기본 동작이 있다.
예를 들어, a 요소를 클릭하면 href 어트리뷰트에 지정된 링크로 이동하고, checkbox 또는 radio 요소를 클릭하면 체크 또는 해제된다.
이벤트 객체의 preventDefault 메서드는 이러한 DOM 요소의 기본 동작을 중단시킨다.
- stoppropagation
이벤트 객체의 stoppropagation 메서드는 이벤트 전파를 중지시킨다.
<!DOCTYPE html>
<html>
<body>
<div class="container">
<button class="btn1">Button 1</button>
<button class="btn2">Button 2</button>
<button class="btn3">Button 3</button>
</div>
<script>
// 이벤트 위임: 클릭된 하위 버튼 요소의 color를 변경한다.
document.querySelector('.container').onclick = ({ target }) => {
if (!target.matches('.container > button')) return;
target.style.color = 'red';
};
// .btn2 요소는 이벤트 전파를 중단하므로 상위 요소에서 이벤트를 캐치할 수 없다.
document.querySelector('.btn2').onclick = e => {
e.stopPropagation(); // 이벤트 전파 중단
e.target.style.color = 'blue';
};
</script>
</body>
</html>
상위 DOM 요소인 container 요소에 이벤트를 위임했다!! 따라서
하위 버튼 요소에서 발생한 클릭 이벤트를 상위 DOM 요소인 container 요소가 캐치하여 이벤트를 처리할 수 있다.
하지만 하위 요소 중에서 btn2 요소는 자체적으로 이벤트를 처리한다.
이벤트 전파를 중단하기 때문에 btn2 요소에서 발생한 이벤트는 상위로 이동하지 않으며 자신에게 직접 바인딩된 이벤트 핸들러만 실행되도록 한다!!!!

'JS' 카테고리의 다른 글
| Promise란? (0) | 2025.12.02 |
|---|---|
| JavaScript의 Event (1) (0) | 2025.12.01 |
| DOM과 브라우저 (0) | 2025.11.18 |
| 클로저란(Closure)? (2) (0) | 2025.11.11 |
| 클로저(Closure)란? (1) (0) | 2025.11.11 |