블로그 내 검색

2011. 12. 7.

HTML 이벤트 버블링(Event Bubbling) 에 대해서

자바스크립트의 이벤트 처리는 아주 직관적이다.
그 만큼 사용이 간단한 지식만으로 쉽게 사용이 가능하다.

그런데 간혹 이벤트의 발생이 원하지 않는 방향으로 흐를 때가 있다.
특히, 마우스 이벤트 처리시에 자주 발생되는데, 메뉴 드랍다운 UI를 자바스크립트로 구현하면서 많이 겪어봤을 거라고 생각한다.
드랍메뉴 영역에 mouseout을 걸어서 영역을 벗어날 때 사라지게 했는데, 결과는 메뉴 영역의 아이템에 마우스를 갖다대도 사라져 버린다든가...

골치아프다. 이벤트가 왜 이러는거야!

이벤트 버블링에 대해 예제와 함께 좀더 알아보자.
아래와 같은 예제를 준비했다.

div 를 여러개 계층 구조로 겹쳐 두었다.

<!DOCTYPE html>
<html>
<head>
<style>
div { margin: 10px; padding: 10px; background-color: red; }
div div { background-color: yellow;
div div div { background-color: blue; }
textarea { width: 90%; height: 200px; }
</style>
</head>
<body>

<div id="depth1">
<div id="depth2">
<div id="depth3">
</div>
</div>
</div>
<textarea></textarea>
</body>
</html> 

그다음 이벤트 바인딩이다.
div영역에 마우스를 올릴 때, 해당아이디를 텍스트에리어에 출력한다.

window.onload = function(e) {

    var logger = document.getElementsByTagName("textarea")[0];
    function log(newtext) {
        logger.value += newtext + "\n";
        logger.scrollTop = logger.scrollHeight; 
    }
    var divs = document.getElementsByTagName("div");
    for(var i=0; i < divs.length; i++) {
        (function(){
            var div = divs[i];
            div.onmouseover = function(e) {
                if(div.id === "depth1") {
                    log(div.id);
                }
                else if(div.id === "depth2")  {                    
                    log(div.id);
                }
                else if(div.id === "depth3") {
                    log(div.id);
                }
            }
        })();
    }
}


가장 안쪽의 depth3에 마우스를 올려보자.
실행 결과를 보면 아래 영역의 아이디부터 textarea에 출력되는것을 볼 수 있다.

depth1
depth2
depth1
depth3
depth2
depth1

JS Bin on jsbin.com
이제 어떤 일이 일어났는지 확인해보자.
먼저 앞으로 쓰일 용어 정의부터 하자

이벤트 핸들러 : 요소에 어떠한 이벤트가 일어날 때 실행되는 함수. 캡처 핸들러와 버블 핸들러로 구분할 수 있다.
요소 : HTML Element

HTML 이벤트 모델에서 이벤트가 실행되는 것은

캡처 (Capture)
버블 (Bubble)

이라는 두 단계가 있다.

캡처는 말 그대로 캡처이다.
이벤트가 뭔가에 의해 발생하였다면 그 이벤트를 캡처하기 위해 이벤트가 발생한 요소를 포함하는 부모 HTML부터 이벤트의 근원지인 자식 요소까지 이벤트를 검사한다. 이때, 캡처 속성의 이벤트 핸들러가 있다면 실행시키면서 이벤트 요소로 접근한다.

이렇게 이벤트의 근원을 아래로 내려가며 찾아가는 단계를 이벤트 캡처링(Event Capturing)이라고 부른다.

이제 캡처가 끝났으니 버블이 발생한다.

이벤트 요소에 도달했다면 이제 다시 이벤트 요소로부터 이벤트 요소를 포함하고 있는 부모 요소까지 올라가며 이벤트를 검사한다.
이때 버블 속성의 이벤트 핸들러가 있다면 실행시킨다.

마우스를 영역에 갖다대면 이벤트는 하나에서만 발생하는것이 아니라 모든 영역에서 발생한다. 가장 안쪽의 div에 마우스를 갖다대면 이벤트의 발생 순서는 안쪽 div부터 바깥쪽의 부모 div로 전파되며 발생한다.

여기서는 모든 div에 이벤트 핸들러를 할당했지만, 이벤트 핸들러가 있든 없든 상관없이 이벤트 처리기는 이벤트를 체크하며 핸들러가 있을 경우 실행시키면서 차례로 상위 요소로 이벤트를 전파시킨다.

사이다등의 탄산 음료를 컵에 담아두면 탄산 기포가 아래에서 위로 올라오는 것을 봤을 것이다. 그것과 같이 이벤트도 자식 요소로부터 부모 요소로 올라오며 실행된다고 하여 이벤트 버블링(Event Bubbling)이라고 부른다.

보통 기본 이벤트 핸들러는 버블 속성이며 W3C 표준에서는 이벤트를 묶을 때 캡처 핸들러인지 버블 핸들러인지 지정할 수 있게 되어 있다.
하지만 인터넷 익스플로러 계열은 캡처 이벤트를 지원하지 않는다.

위의 예제에서도 모든 이벤트 핸들러는 버블 이벤트 핸들러이다.

그럼 위의 예제의 결과를 이해할 수 있을것이다.
  • depth1에 진입하여 아이디를 출력
  • depth2에 진입하여 아이디를 출력하고 이벤트는 버블되어 depth1의 아이디를 출력
  • depth3에 진입하여 아이디를 출력하고 이벤트는 버블되어 depth2, 또 버블되어 depth1 의 아이디를 순서대로 출력
이제 한번 캡처링 이벤트 핸들러를 등록하고 실행 순서가 어떻게 되는지 알아보자.
주의할 점은 아래 예제는 인터넷 익스플로러에서는 동작하지 않는다.

캡처 이벤트를 지원하려면 다음 표준 메서드가 필요하다.


element.addEventListener
target.addEventListener(이벤트타입, 핸들러, 캡처여부);

위의 예제에서 이벤트를 등록하는 부분을 아래와 같이 변경한다.

var divs = document.getElementsByTagName("div");
if(document.addEventListener) {
    for(var i=0; i < divs.length; i++) {
        (function(){
            var div = divs[i];
            if(div.id === "depth1") {
                div.addEventListener(
                    "mouseover", 
                    function(evt) {
                        log(div.id);
                    },
                    true // 이벤트 캡처로 등록
                )
            }
            else if(div.id === "depth2") {
                div.addEventListener(
                    "mouseover", 
                    function(evt) {
                        log(div.id);
                    },
                    false // 이벤트 버블로 등록
                )
            }
            else if(div.id === "depth3") {
                div.addEventListener(
                    "mouseover", 
                    function(evt) {
                        log(div.id);
                    },
                    false // 이벤트 버블로 등록
                )
            }
        })();
    }
}
코드가 복잡해 보이지만 사실 단순한 반복 코드이다.


실행 순서를 예측한 사람이라면 캡처와 버블에 대해 정확하게 이해하고 있는 것이다.
실행 순서는 다음과 같다

depth1
depth1
depth2
depth1
depth3
depth2
  • depth1에 캡처하면서 캡처 이벤트 핸들러 동작, 아이디를 출력
  • depth2에 캡처하면서 depth1 아이디를 출력하고 이벤트는 버블되며 depth2의 아이디를 출력
  • depth3에 캡처하면서 depth1 아이디를 출력하고 이벤트는 버블되며 depth3, depth2의 아이디를 순서대로 출력
JS Bin on jsbin.com 이러한 이벤트 동작은 때로 원치 않는 결과를 가져온다.
그러나 이벤트 버블 동작을 막는 방법도 지원한다.
물론 인터넷 익스플로러는 이번에도 따로 논다. 익스폴로러를 죽입시다 익스플로러는 나의 원수

eventObject.stopPropagation() // W3C방식
eventObject.cancelBubble = true; // 인터넷 익스플로러 방식


이벤트 핸들러에서 이벤트 객체의 특정 메서드를 샐행하거나 속성을 수정하면 버블링을 막을 수 있다.

우리가 원하는 이벤트 버블 막기 작업을 위해서는 먼저 이벤트 객가 필요하다. 이벤트 객체는 어디 있을까?

W3C 표준에 따르면 이벤트핸들러에는 첫번째 인자로 이벤트 객체가 전달된다.
그러나 인터넷 익스플로러는 전역 객체의 속성에 이벤트 객체가 바인딩된다.

이벤트 객체를 확인하는 간단한 함수 예제이다.

function alertEventObject(e) {
    var objEvent = e || window.event;
    alert(objEvent + " is Event Object");
}


첫번째 예제의 depth2 이벤트 핸들러 지정 부분을 아래와 같이 수정한다.

else if(div.id === "depth2") {
    var evt = e || window.event;
    if(evt.stopPropagation) {
        evt.stopPropagation();  // W3C 표준
    }
    else { 
        evt.cancelBubble = true; // 인터넷 익스플로러 방식
    }
    log(div.id);
}

그렇다면 실행 결과는 다음과 같이 될 것이다.

depth1
depth2
depth3
depth2
  • depth1에 진입, 아이디를 출력
  • depth2에 진입, depth2의 아이디를 출력하고 이벤트를 버블시키려 하지만 이베트 버블이 막아져 있어 이벤트 전파 종료
  • depth3에 진입, depth3 아이디를 출력하고 이벤트는 버블되어 depth2 의 아이디를 출력, 그뒤 버블이 막혀 이벤트 전파 종료.

이벤트 버블링을 정확하게 이해하고 있어야 복잡한 자바스크립트 어플리케이션을 구현시에 알 수 없는 오류를 줄일 수 있을 것이다.

댓글 3개:

  1. .......아 진짜 익스플로러 없애버리고 싶어요ㅠㅜ 지금까지 멘붕당했어요ㅠㅜ 정말 감사합니다 너무 감사합니다ㅠ
    진짜 뭣때문에 안되는지 계속 고민했어요ㅠㅜ

    답글삭제