본문 바로가기

개발공부/Javascript

[JS] 이벤트 버블링과 캡처링

웹 브라우저는 사용자와 상호작용이나 브라우저 자체 상태 변화 감지를 위해 다양한 이벤트를 제공한다. 키보드에서 글자 입력, 페이지 스크롤 이러한 이벤트 들은 웹페이지에 사용자 동작을 감지하여 적절한 반응을 생성한다, 웹 브라우저에서는 이벤트를 효율적으로 처리하기 위해 이벤트 버블링과 캡처링 2가지 메커니즘을 사용한다. 두 개념의 정의와 작동 원리에 대해 알아본다.

 

웹 브라우저에서 이벤트 처리는 웹앱의 ui와 상호작용을 관리하는 데 있어 아주 중요하다. 이벤트 버블링과 캡처링 두 가지 이벤트 전파 메커니즘이 존재한다. 이들 메커니즘은 개발자가 이벤트를 효과적으로 처리하고 관리하는데 중요하다. 여러 자식 요소들을 거처 발생하는 이벤트를 효율적으로 처리할 수 있다. 예를 들어 하나의 부모 요소에서 자식 요소들의 이벤트를 한 곳에서 처리할 수 있어서 이벤트 리스너를 각각의 자식 요소에 개별적으로 할당하는 것보다 효율적으로 관리할 수 있다. 개별적인 할당은 성능에 영향을 줄 수 있다. 이벤트 위임 기법을 사용하면 이런 부담을 줄일 수 있다 이벤트 위임은 버블링을 활용하여 상위 요소에 이벤트 리스너를 단 한번만 할당해서 하위 요소들의 이벤트를 처리한다. 어떤 경우에는 이벤트 버블링과 캡처링을 중단 해야 할 때도 있다. 예를 들어, 특정 이벤트가 부모 요소로 전파되는 것을 원하지 않는 경우 stopPropagation 메소드를 사용하여 이벤트를 중단할 수 있다. 이를 통해 불필요한 이벤트 처리를 방지하고 예상치 못한 동작을 막을 수 있. 복잡한 ui에서 다양한 요소들 간에 상호작용이 복잡하게 얽혀 있을 수 있다. 이벤트 버블링과 캡처링의 원리를 이해하면 잘 제어 할 수 있다.

 

이벤트 버블링과 캡처링은 개발자에게 DOM 요소 간의 상호작용을 관리하는 도구를 제공한다. 이런 이벤트 전파 메커니즘을 이해하고 적절히 사용하면 복잡한 이벤트 흐름을 효과적으로 제어하고 사용자에게 직관적이고 부드러운 ux를 제공할 수 있다.

 

이벤트 종류

마우스 - click, mouseover, mouseout, mouseup

키보드 - keydown, keyup, keypress(누리고 있을떄 연속적으로)

폼 이벤트 - submit, change, focus, blur

문서 이벤트 - load, resize, scroll

 

이벤트에 따라 다양한 객체와 데이터를 제공해서 사용자 상호작용을 구현할 수 있다.

 

이벤트 버블링 (Event Bubbling)

이벤트가 가장 깊은 요소에서 시작하여 점차 상위 요소로 전파되는 과정이다. 물속에서 거품이 밑에서 올라오듯 이벤트가 DOM tree를 따라 하위에서 상위로 이동하는 것을 시각적으로 표현한 것이다. 버튼이 있고 버튼 위에 div가 있다. 버튼 부터 div까지 이벤트가 순차적으로 이동한다. 이벤트 버블링은 웹 브라우저에서 이벤트를 처리하는 가장 기본적인 메커니즘이다. 이벤트 버블링에서는 특정 이벤트가 발생한 가장 깊은 요소에서 시작하여 그 이벤트가 DOM tree를 따라 상위 요소들로 전파가 된다. 예를 들어 여러 자식 요소에 공통적으로 적용되어야 하는 이벤트 핸들러를 부모 요소에 한번만 할당하여 코드의 중복을 줄이고 성능을 개선할 수 있다.

 

 

버튼을 클릭하면, 버튼 > div2 > div1 까지 이벤트 리스너가 차례대로 실행된다. 이것이 이벤트가 하위 요소에서 발생하여 상위 요소로 버블링 되기 때문이다. 이벤트 버블링은 가장 안쪽에 있는 요소 (실제로 이벤트가 발생한 요소)에서 시작하여 트리를 따라 상위 요소로 전파가 된다. 이 과정은 해당 요소의 부모 요소에서 시작하여 최상위 요소인 body → html 태그에 도달할 때까지 계속 된다.

// Button Clicked!' => DIV1 Clicked! => DIV2 Clicked!
// 순서로 alert 노출
document.getElementById('div2').addEventListener('click'
, function () {
    alert('DIV2 Clicked!');
});

document.getElementById('div1').addEventListener('click'
, function () {
    alert('DIV1 Clicked!');
});

document.getElementById('btn').addEventListener('click'
, function () {
    alert('Button Clicked!');
});

 

이벤트 버블링의 장점

1) 중복 감소

하위 요소에서 발생하는 이벤트를 상위 요소에서 한번에 처리할 수 있다. 같은 이벤트를 여러 요소에 각각 할당하는 것보다 효율적으로 처리할 수 있기 때문에 중복이 감소 된다.

 

2) 동적인 요소 처리

페이지가 로드된 이후 추가되는 요소에 대해서도 이벤트 리스너를 별도로 추가할 필요 없이 기존 상위 요소 이벤트 리스너가 이벤트를 처리할 수 있기 때문에 동적인 요소 처리가 가능하다.

 

3) 메모리 사용 최적화

각 요소의 개별적인 리스너를 붙이는 것보다 상위 요소의 이벤트 리스너를 설정할 수 있다.

 

4) 이벤트 관리의 단순화

이벤트 흐름을 쉽게 추적할 수 있다. 이벤트 관리를 단순화 할 수 있다.

 

이벤트 위임 (Event Delegation)

이벤트 버블링은 이벤트 위임을 구현하기 위해 사용된다. 이벤트 위임이란 한 개의 부모 요소에 이벤트 리스너를 할당하고 그 부모의 자식 요소들에서 발생하는 이벤트를 처리하는 방식이다. 이런 이벤트 버블링은 이벤트 위임을 가능하게 하는 메커니즘이다. 코드를 간결하게 만들 수 있고, 메모리 사용을 줄이고, 동적으로 요소가 추가 되어도 유연하게 대응이 가능하다.

 

위 코드에서 event 객체의 targetid를 검사해서 클릭 요소의 target id일 때만 alert이 노출되도록 변경 (index2.js)

document.getElementById('div2').addEventListener('click'
, function (event) {
    if (event.target.id === 'div2') {
        alert('DIV2 Clicked!');
    }

});

document.getElementById('div1').addEventListener('click'
, function (event) {
    if (event.target.id === 'div1') {
        alert('DIV1 Clicked!');
    }
});

document.getElementById('btn').addEventListener('click'
, function (event) {
    if (event.target.id === 'btn') {
        alert('Button Clicked!');
    }
});

 

이벤트가 발생할 때 event.target.id를 이용하여 실제로 클릭된 요소를 참조하여이벤트가 실제 클릭된 요소와 일치할 때만 알림을 표시하도록 했기 때문이다. 이벤트 버블링을 고려하여 실제 이벤트가 발생한 요소가 특정 조건에 맞는지 확인하고 만족 시에만 이벤트를 처리하는 방식이다.

 

이벤트 위임을 이용하여 리팩토링(index3.js)

document.getElementById('div1').addEventListener('click'
, function (event) {
    if (event.target.id === 'btn') {
        alert('button Clicked!');
    } else if (event.target.id === 'div2') {
        alert('div2 Clicked!')
    } else if (event.target.id === 'div1') {
        alert('div1 Clicked!')
    }
});

 

각각 선언되었던 이벤트 리스너를 상위의 요소인 div1에 단 한번만 선언하면 된다. 이벤트가 버블링 되기 떄문에 해당 이벤트가 발생한 이후에 상위 요소로 이동 한다. 따라서 이러한 버블링의 특성을 이용하여 개별적인 자식 요소에 각각 이벤트 리스너를 할당할 필요없이 공통된 상위요소 단 하나에만 이벤트 리스너를 추가하면 된다. 효율적이다. 이벤트 위임은 다른 장점을 가진다. 바로 동적으로 돔요소가 추가되거나 삭제될 때 이다.

 

동적으로 요소가 추가될 때, 어떤 돔에 이벤트 리스너를 추가하면 좋을까?

이 요소들 중 list item을 클릭했을 때, alert를 출력하게 하고 싶다.

document.getElementById('addBtn').addEventListener('click'
, function () {
    const list = document.getElementById('list');
    const newItem = document.createElement('li');
    newItem.textContent = 'List Item' + (list.children.length + 1);
    list.appendChild(newItem);
});

 

이벤트 위임이 유용하게 사용된다. 어차피 상위 요소 하나에 이벤트를 할당하면 되기 때문에 동적으로 추가 되는 각각의 list item에 대한 개별적인 이벤트 리스너를 부여할 필요가 없다. list item을 포함하는 상위 요소에 단 하나의 이벤트 리스너를 설정하여 동적으로 추가되는 모든 리스트에 클릭 이벤트를 할당할 수 있다.

// 이벤트 버블링과 이벤트 위임이 있기 때문에 이 코드를 실행할 수 있다.
document.getElementById('list').addEventListener('click'
, function (event) {
    if (event.target.tagName === 'LI') {
        alert('Clicked on  ' + event.target.textContent);
    }
})

 

이벤트 캡처링

버블링 과는 반대로 최상위 요소에서 시작하여 발생한 요소로 내려가는 과정이다. document 객체에서 시작해서 div → button으로 내려간다. addEventListener 함수의 세 번째 인자로 true를 설정하여 이벤트 캡처링을 활성화하도록 한다. 이벤트가 하위 요소로 도달하기 전에 이벤트를 캡처하기 위해 사용한다. 복잡한 이벤트 처리 시나리오에서 유용할 수도 있지만, 드물게 사용이 된다. 이벤트 캡처링은 하위 요소에서 처리되기 전에 상위 요소에서 먼저 잡아야 할 때 유용하다. 특정 조건에서만 하위 요소로 전파되게 하거나 이벤트 전파를 조절하는 데 사용할 수 있다.

document.getElementById('div1').addEventListener('click', function () {
    alert('DIV1 Clicked!');
}, true);

document.getElementById('div2').addEventListener('click', function () {
    alert('DIV2 Clicked!');
});

document.getElementById('btn').addEventListener('click', function () {
    alert('Button Clicked!');
});

 

이벤트 캡처링 활성화 후Alert 순서 : DIV1 → Button → DIV2

캡처링은 상위 요소에서 시작하여 하위 요소로 점점 전파가 된다. 상위 요소에서 이벤트를 캡쳐할 수 있게 되고, 캡처링의 기능으로 이벤트의 흐름을 제어하고 특정 동작을 세밀하게 관리할 수 있는 기능을 제공한다.

로그인을 한 사용자만 버튼을 클릭할 수 있게 하고 로그인을 하지 않은 사람은 로그인을 하라는 alert을 띄우는 캡처링을 구현할 것이다. 캡처링은 상위 요소부터 하위 요소로 이동한다. 버튼을 감싸는 parent div에 이벤트 리스너를 할당한다.

 

isUserLoggedIn을 만들어서 false일 때 alert 띄우는 걸 캡쳐링으로 구현할 것이다.

let isUserLoggedIn = false;
document.getElementById("parentDiv").addEventListener('click', function (event) {
    if (!isUserLoggedIn) {
        alert('로그인을 먼저 해주세요.');
        event.stopPropagation();
    }
}, false or true)

document.getElementById("childButton").addEventListener('click',
    function () {
        alert('Button Clicked');
    }
)

 

addEventListener의 세번째 인자에 true가 들어갔을 때 alert 순서

⇒ 로그인을 먼저 해주세요. → Button Clicked

addEventListener의 세번째 인자에 false가 들어갔을 때 alert 순서

⇒ Button Clicked → 로그인을 먼저 해주세요.

 

이벤트 캡처링은 상위 이벤트 요소 부터 실행이 되기 때문에 Button Clicked! alert 나오지 않는다. 만약 캡처링을 false로 바꾸면 어떻게 될까? 이벤트는 기본적으로 버블링으로 실행이 되기 때문에 캡처링이 되기 저에 Button Clicked alert이 나오고 그 이후에 로그인 해달라는 alert이 나온다.

 

이처럼 이벤트 캡처링은 상위 요소에서 이벤트를 먼저 처리하고 필요에 따라 하위 요소의 이벤트 처리를 방지할 수 있다. 이처럼 복잡한 상호작용이 있는 ui에서 유용할 수 있다.

 

캡처링이 자주 사용되지 않는 이유?

  1. 복잡성 증가 : 이벤트 흐름을 더 복잡하게 만들수도 있다. 캡처링과 버블링을 모두 고려해야 한다.
  2. 불필요한 사용 사례: 대부분의 이벤트 처리는 이벤트 버블링 만으로도 충분히 처리가 가능하다.
  3. 이벤트 흐름의 예측 가능성: 이벤트의 흐름이 덜 직관적
  4. 특정 상황에 대해 이해: 캡처링을 사용해야 할 상황이 자주 발생하지 않는다.
  5. 일반적인 웹 개발 시나리오에서는 매우 제한적이다.

코드 단순성과 유지보수를 위해 버블링에 의존하는 편이 낫다.

 

 

이벤트 전파 방지 (Event stopPropagation)

이벤트의 흐름을 제어하는 메서드이다. stopPropagation 메서드를 통해 구현할 수 있다. 이 메서드는 DOM 이벤트가 발생했을 때, 이벤트가 현재 요소에서 상위 요소로 전파되는 것을 방지할 수 있다. 또한, 캡처링에서도 전파 방지가 되기 때문에 상위 요소에서 하위 요소로 전달되는 것도 방지할 수 있다. stopPropagation 은 이벤트가 발생한 요소에서 시작하여 돔트리를 따라 이벤트 전파를 중단 시킨다. stopPropagation 의 주요 사례는 특정 이벤트가 상위 요소로 전파되는 것을 방지하고자 할 때이다. 예를 들어 하위 요소에서 이벤트가 발생했는데, 이 이벤트가 상위 요소의 이벤트 리스너에 영향을 주고 싶지 않을 때 사용할 수 있다. 이벤트 전파 방지는 주의 사항이 있다. 상위 요소에 이벤트 리스너가 실행되지 않기 때문에, 즉 버블링이 중단되기 때문에 의도치 않은 부작용이 발생할 수도 있다. 따라서 이 메서드를 사용할 때는 side effect를 충분히 고려해야 한다. stopPropagation 는 이벤트의 기본 동작을 중단하지는 않는다. 이벤트의 기본 동작을 중단하지는 않는다.

document.getElementById('div1').addEventListener('click', function () {
    alert('DIV1 Clicked!');
});

document.getElementById('div2').addEventListener('click', function () {
    alert('DIV2 Clicked!');
});

// 버튼의 이벤트 전파 방지
document.getElementById('btn').addEventListener('click', function (event) {
    alert('Button Clicked!');
    event.stopPropagation();
});

 

'Button Clicked!'만 노출

 

 

모달에서 예시

모달 배경을 클릭했을 때 모달이 닫히도록 & 모달 내부의 컨텐츠를 클릭했을 때는 닫히지 않도록 구현해본다.

document.getElementById('modalBackground')
.addEventListener('click', function() {
    this.style.display = 'none';
});

 

모달 내부 버튼을 클릭 했을 때 모달이닫힌다. 이벤트 버블링 특성 때문에 버튼 이벤트가 버블링을 타고 올라가서 모달의 백그라운드 까지 전달이 된다.

⇒ 이러한 현상을 방지하기 위해서는 이벤트 버블링 중지시키면 된다. (이벤트 전파 방지) 버튼을 클릭했을 때, stopPropagation 메소드를 실행 시키면 이벤트가 더 이상 모달 백그라운드로 전파가 되지 않기 때문에 창이 닫히지 않게 된다.

document.getElementById('modalContent')
.addEventListener('click', function (event) {
    event.stopPropagation();
})

 

이 코드를 추가하면 버튼을 클릭했을 때 모달이 닫히지 않고, 배경을 클릭했을 때만 닫힌다.

 

 

이벤트 기본 동작 방지 (event preventDefault)

웹브라우저는 많은 HTML 요소들에 대해 기본적인 동작을 가지고 있는데, 이 메서드를 사용하면 기본 동작을 취소할 수 있다. submit 태그를 누르면 기본적으로 새로 고침이 되는데, 서버 처리에 대한 응답 메시지를 보여줄 수 없다. 이런 동작을 원하지 않을 때, 새로 고침을 방지할 수 있다. 이를 통해 사용자에 더 나은 상호작용과 ux를 제공할 수 있다. 이 메서드는 이벤트의 전파를 중단하는 것은 아니다.

document.querySelector('form')
    .addEventListener('submit', function (event) {
        event.preventDefault();
})

 

 

 

[참고]

패스트캠퍼스 강의 (내 생에 마지막 JavaScript : 기초 문법부터 실무 웹 개발까지 한 번에 끝내기 초격차 패키지 Online) 를 기반으로 작성하였고, 추가 서치 내용을 정리하였습니다.