최근에 공부를 하면서 백엔드 지식뿐만 아니라 프론트의 지식도 알고 있어야 프로젝트를 하는데 도움도 되고 취업에도 유리하겠다란 생각이 들어 공부를 시작하게 되었다.
프론트를 배우면서 면접에서 가장 많이 질문받는 것 중 하나가 캡처링과 버블링의 차이라고 한다.
간단하게 설명하자면 캡처링과 버블링은 이벤트가 DOM에서 전파되는 방식을 설명하는 용어이다.
버블링(Bubbling): 이벤트가 발생한 요소에서 시작하여 최상위 요소까지 전파되는 과정이다. 이벤트가 발생한 요소에서 시작해서 부모요소, 그 부모의 부모요소까지 전달되어 올라가는 방식이다.
(제일 깊은 곳에 있는 요소에서 시작해 부모 요소를 거슬러 올라가며 발생하는 모양이 마치 물속 거품(bubble)과 닮았기 때문에 명명 지어졌다.)
캡처링(Cpaturing): 이벤트가 최상위 요소에서부터 시작하여 실제 대상 요소까지 전파되는 과정이다. 이벤트가 더 깊은 요소로 전달되기 전에 이벤트가 캡처 단계를 거치게 된다.
그럼 조금 더 자세히 살펴보자
이벤트
앞에서 간단하게 설명하고 왔지만 여기서 계속 이벤트라는 단어가 있다. 여기서 이벤트는 뭘까?
**이벤트**란 사용자나 브라우저에서 발생하는 특정한 동작을 가리킨다. 예를 들어, 클릭, 마우스를 움직임, 키보드 입력 등이 모두 이벤트에 해당한다.
그럼 이제 HTML 이벤트의 흐름을 살펴보자.
HTML 이벤트의 흐름은 일반적으로 캡처링과 버블링 두 가지 단계로 나뉜다.
- 캡처링 단계: 이벤트가 최상위 요소에서부터 이벤트가 발생한 요소까지 전파되는 단계이다. 최상위 요소에서 시작해서 이벤트 타깃 요소로 이벤트가 전달되기 전에 발생하는 단계이다.
- 타겟 단계: 이벤트가 실제로 발생한 요소에 도달한 단계이다. 이벤트가 발생한 요소에서 해당 이벤트가 처리된다. 위의 사진을 예시로 들면 button에 해당한다.
- 버블링 단계: 이벤트가 발생한 요소에서 시작하여 최상위 요소까지 거슬러 올라가는 단계이다. 이벤트 타겟요소에서 시작해서 최상위 요소까지 이벤트가 전파되며, 이 단계에서도 이벤트 핸들러가 동작할 수 있다.
버블링
브라우저의 이벤트는 기본적으로 **버블링 방식**으로 이벤트가 전파된다.
자바스크립트 `addEventListener()` 함수로 요소의 이벤트를 등록하면 기본적으로 버블링 전파 방식으로 이벤트가 흐르게 되어, 만일 자식 요소에 이벤트가 발생하면 부모 요소도 버블링 통해 이벤트가 전파되어 리스너가 호출되게 된다.
<body>
<div class="one">
<div class="two">
<div class="three">
</div>
</div>
</div>
</body>
var divs = document.querySelectorAll('div');
divs.forEach(function(div) {
div.addEventListener('click', logEvent);
});
function logEvent(event) {
console.log(event.currentTarget.className);
}
위 코드는 세 개의 div 태그에 모두 클릭 이벤트를 등록하고 클릭 했을 때 logEvent 함수를 실행시키는 코드이다. 여기서 위 그림대로 최하위 div 태그 `<div class="three"></div>` 를 클릭하면 아래와 같은 결과가 실행된다.
div 태그 한 개만 클릭했지만 3개의 이벤트가 발생된다.
이렇게 브라우저가 이벤트를 감지하는 이유가 버블링 방식이기 때문이다.
브라우저는 특정 화면 요소에서 이벤트가 발생했을 때 그 이벤트를 최상위에 있는 화면 요소까지 이벤트를 전파시킨다. 따라서 그림과 같이 실행이 되는 것이다.
여기서 주의해야 할 점은 각 태그마다 이벤트가 등록되어 있기 때문에 상위 요소로 이벤트가 전달되는 것을 확인할 수 있다. 만약 이벤트가 특정 div 태그에만 달려 있다면 위와 같은 동작 결과는 확인할 수 없다.
캡처링
버블링과는 반대 방향으로 진행되는 이벤트 전파 방식이다. 위의 그림처럼 특정 이벤트가 발생했을 때 최상위 요소에서부터 해당 태그를 찾아 내려간다.
<body>
<div class="one">
<div class="two">
<div class="three">
</div>
</div>
</div>
</body>
var divs = document.querySelectorAll('div');
divs.forEach(function(div) {
div.addEventListener('click', logEvent, {
capture: true // default 값은 false입니다.
});
});
function logEvent(event) {
console.log(event.currentTarget.className);
}
기본적으로 `addEventListner()` 에서는 버블링 방식이기 떄문에 옵션 객체에서 `capture:true`를 설정해줘야 한다. 그러면 해당 이벤트를 감지하기 위해 이벤트 버블링과 반대 방향으로 탐색한다.
그럼 이벤트의 전파를 중지시키는 방법이 있나?
만약 부모와 자식 둘다 이벤트를 등록한 상태에서, 자식 요소만 클릭했을 때만 이벤트를 발생하고 부모 요소는 이벤트를 발생시키고 싶지 않은 상황이 있을 수 있다.
하지만 브라우저는 기본적으로 캡처링 - 버블링으로 동작되기 때문에 이벤트 동작 자체를 바꿀 순 없다. HTML 구조에서 자식 요소가 부모 요소의 영역 안에 위치한다면, 자식 요소를 클릭했더라도 브라우저는 그것을 부모 요소도 함께 클릭한 것으로 해석된다. 이것은 브라우저의 이벤트 처리 방식으로, HTML 요소의 구조와 일관성이 유지되는 동작이다.
따라서 브라우저의 이벤트 구조를 바꿀수는 없고, 엘리먼트의 이벤트 전파를 방지 처리하는 식으로 해결해야 한다.
event.stopPropagation()
function logEvent(event) {
event.stopPropagation();
}
이 메서드를 사용하면 전파를 막을 수 있다. 따라서, 이벤트 버블링의 경우에는 클릭한 요소의 이벤트만 발생시키고 상위 요소로 이벤트를 전달하는 것을 방해한다. 그리고 이벤트 캡처의 경우에는 클릭한 요소의 최상위 요소의 이벤트만 동작시키고 하위 요소들로 이벤트를 전달하지 않는다.
// 이벤트 버블링 예제
divs.forEach(function(div) {
div.addEventListener('click', logEvent);
});
function logEvent(event) {
event.stopPropagation();
console.log(event.currentTarget.className); // three
}
// 이벤트 캡쳐 예제
divs.forEach(function(div) {
div.addEventListener('click', logEvent, {
capture: true // default 값은 false입니다.
});
});
function logEvent(event) {
event.stopPropagation();
console.log(event.currentTarget.className); // one
}
참고 문헌: