🫧 이벤트 흐름
표준에서 정의한 DOM 이벤트의 흐름에는 3단계가 존재합니다.
- 캡처링(capturing): 이벤트가 하위 요소로 전파되는 단계
- 타깃(target): 이벤트가 실제로 의도한 타깃 요소에 전달되는 단계
- 버블링(bubbling): 이벤트가 상위 요소로 전파되는 단계
예를 들어, 아래와 같은 코드가 있고 테이블의 세 번째 요소에 클릭 이벤트를 할당한 경우
<html>
<head>
...
</head>
<body>
<table>
<tbody>
<tr>
<td>apple</td>
<td>banana</td>
</tr>
<tr>
<td>cherry</td> <!-- 클릭 이벤트 할당! -->
<td>durian</td>
</tr>
</tbody>
</table>
</body>
</html>
세 번째 '<td>cherry</td>'를 클릭하면 이벤트 흐름에 따라
이벤트가 최상위 요소부터 타깃 요소까지 전달되면(캡처링) -> 이벤트가 전달된 타깃 요소는 이벤트 리스너를 실행하며(타깃) -> 이벤트는 다시 최상위 요소로 전파되는 과정(버블링)을 거칩니다.
헷갈리지 말아야 할 것은 이벤트 핸들러가 아닌 '이벤트' 자체가 전파된다는 점입니다.
🫧 버블링(Bubbling)
어떤 요소에 이벤트가 발생하면, 해당 이벤트가 전파되어 그 조상 요소들의 이벤트 핸들러도 함께 동작하는 현상
버블링으로 인해 이벤트가 전파된 요소에도 이벤트 핸들러가 있다면 해당 핸들러 또한 차례대로 동작합니다.
이벤트가 최상단에 있는 윈도우 객체를 만날 때까지 부모 요소를 거슬러 올라가는 현상이 마치 물 속에서 생겨난 거품이 수면 위로 올라가는 것과 비슷하여 붙여진 이름입니다.
버블링이 일어나도 타깃 객체(처음 특정한 '그' 요소)는 변하지 않습니다.
따라서 상위 요소들의 핸들러를 통해 최초의 이벤트가 발생한 위치를 알 수 있습니다.
- 원하는_이벤트.target: 사용자가 실제로 클릭한 요소를 반환, 이벤트가 발생한 요소
- 원하는_이벤트.currentTarget: 이벤트 핸들러가 동작하는 요소를 반환, 이벤트 핸들러가 할당된 요소
버블링 멈추려면 아래 코드를 써주면 됩니다.
원하는_이벤트.stopPropagation();
참고로 event.stopPropagation()은 위쪽으로 일어나는 버블링은 막아주지만, 다른 핸들러들이 동작하는 건 막지 못합니다.
버블링을 멈추고, 요소에 할당된 다른 핸들러의 동작도 막으려면 event.stopImmediatePropagation()을 사용해야 합니다.
이 메서드를 사용하면 요소에 할당된 특정 이벤트를 처리하는 핸들러 모두가 동작하지 않습니다.
그러나 버블링을 막는 것은 권장하지 않습니다.
페이지 전체를 관통하는 이벤트를 발생시키고 싶을 때 특정 요소만 버블링을 막아 놓으면 그 요소만 이벤트가 적용되지 않아 해당 영역은 '죽은 영역'이 될 수 있기 때문입니다.
버블링 중단에 대한 더 자세한 내용은 다음 링크를 참고해주세요.
🫧 캡쳐링(Capturing)
어떤 요소에 이벤트가 발생하면, 해당 이벤트가 전파되어 그 자손 요소들의 이벤트 핸들러도 함께 동작하는 현상
캡처링은 버블링과 반대되는 과정이라고 볼 수 있습니다. 실무에서 자주 쓰이지는 않지만 가끔 유용한 경우가 있습니다.
보통 타깃에서 이벤트가 발생하면 해당 이벤트가 상위 요소로 버블링되며, 버블링 과정에서 만나는 이벤트 핸들러를 동작시킵니다.
하지만 상황에 따라 이벤트가 발생한 요소를 찾아 DOM 트리 객체를 타고 하위 요소로 내려오는 과정(캡처링)에서 부모 요소의 이벤트 핸들러를 동작시켜야 할 수도 있습니다.
위와 같이 캡처링이 필요한 상황이라면 addEventListener() 메서드의 capture 프로퍼티를 true로 설정하면 됩니다.
// 방법 1
원하는_요소.addEventListener('이벤트_종류', 호출될_함수, {capture: true})
// 방법 2
원하는_요소.addEventListener('이벤트_종류', 호출될_함수, true)
🫧 동작 순서 살펴보기
간단한 예시 코드로 이벤트 흐름을 살펴보겠습니다.
<html>
<head>
...
</head>
<body>
<form>
<div>
<p> <!-- 클릭 이벤트 할당! -->
...
</p>
</div>
</form>
<script>
for(let element of document.querySelectorAll('*')) {
element.addEventListener("click", e => alert(`캡쳐링: ${element.tagName}`), true);
element.addEventListener("click", e => alert(`버블링: ${element.tagName}`));
}
</script>
</body>
</html>
<p>를 클릭하면
- 캡처링 단계: HTML -> BODY -> DIV ->
- 캡처링 타깃 단계: P ->
- 버블링 타깃 단계: P ->
- 버블링 단계: DIV -> FORM -> BODY -> HTML
의 순서로 이벤트 흐름이 발생합니다.
위의 코드에서는 캡처링과 버블링 모두에 addEventListener()를 설정해주었기 때문에 타깃인 <p>가 두 번 호출됩니다.
🫧 이벤트 위임(Event delegation)
하위 요소마다 이벤트를 할당하는 대신, 공통된 상위 요소에서 하위 요소의 이벤트를 제어하는 방식
이벤트 위임의 토대가 되는 것이 바로 앞서 살펴본 버블링과 캡처링입니다.
이벤트 위임을 이용하면, 모든 요소마다 핸들러를 할당하지 않아 초기화가 단순해지고 메모리가 절약됩니다.
요소의 공통 조상에 이벤트 핸들러가 할당되기 때문에 핸들러의 추가 및 삭제가 간단해집니다.
아래의 html 코드에 이벤트 핸들러를 할당한다고 가정해봅시다.
<html>
<head>
...
</head>
<body>
<div id="parent">
<button id="child-1">버튼 1</button>
<button id="child-2">버튼 2</button>
<button id="child-3">버튼 3</button>
</div>
</body>
</html>
먼저 이벤트 위임을 사용하지 않고, 하위 요소 모두에게 이벤트 핸들러를 할당한 코드입니다.
document.getElementById('child-1').addEventListener('click',
function() { console.log('버튼 1이 클릭됨'); }
);
document.getElementById('child-2').addEventListener('click',
function() { console.log('버튼 2가 클릭됨'); }
);
document.getElementById('child-3').addEventListener('click',
function() { console.log('버튼 3이 클릭됨'); }
);
위의 함수는 간단하지만 복잡하고 긴 함수를 해당 방식으로 작성할 경우, 중복 코드가 많아지고 가독성이 떨어지며 유지보수가 어려워집니다.
다음은 이벤트 위임을 사용하여, 공통된 하나의 상위 요소에게 이벤트 핸들러를 할당한 코드입니다.
document.getElementById('parent').addEventListener('click',
function(event) { console.log(`${event.target.textContent}이(가) 클릭됨`); }
);
해당 방식으로 작성할 경우, 중복되는 코드가 없어져 한결 간결해진 모습을 볼 수 있습니다.
더 나아가 자식 요소 중에서 타깃이 아닌 부분에 이벤트가 발생해도 이벤트 핸들러가 동작할 수 있기 때문에 명확한 타깃에서만 핸들러가 동작하도록 제한해주는 과정이 필요합니다.
읽어주셔서 감사합니다:)