RATSENO

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

DEV/JS

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

RATSENO 2019. 11. 15. 16:23

HTML은 계층적이므로 이벤트를 꼭 한곳에서만 처리해야 하는 건 아닙니다. 예를 들어 버튼을 클릭했을 때 물론 버튼

자체에서 이벤트를 처리할 수 있지만, 버튼의 부모에서 처리해도 되고 그 부모의 부모에서 처리해도 되는 식입니다.

여러 요소에서 이벤트를 처리할 수 있다면, 그 이벤트에 응답할 기회는 어떤 순서로 주어지는가 하는 의문이 생깁니다.

 

기본적으로 두 가지 방법이 있습니다.

 

하나는 가장 먼 조상부터 시작하는 방법으로, 캡처링(capturing)이라 부릅니다. 밑에서 보여드릴 예제 HTML에서 버튼은

<div id="content"></div>에 들어있고 <div id="content"></div>는 <body>에 들어있습니다. 따라서 <body>도 버튼에서 일어난 이벤트를 '캡처'할 수 있습니다.

 

다른 방법은 이벤트가 일어난 요소에서 시작해 거슬러 올라가는 방법입니다. 이런 방법을 버블링(bubbling)이라 부릅니다. 

 

HTML5 이벤트 모델에서는 두 방법을 모두 지원하기 위해 먼저 해당 요소의 가장 먼 조상에서 시작해 해당요소까지 내려온 다음, 다시 해당 요소에서 시작해 가장 먼 조상까지 거슬로 올라가는 방법을 택했습니다.

 

이벤트 핸들러에는 다른 핸들러가 어떻게 호출될지 영향을 주는 세 가지 방법이 있습니다.

 

[1]

가장 많이 쓰이는 것은 preventDeatult입니다. 이 메서드는 이벤트를 취소합니다. 최소된 이벤트는 계속 전달되기는 하지만, defaultPrevented 프로퍼티가 true로 바뀐 채 전달됩니다. 브라우저의 이벤트 핸들러는 defaultPrevented 프로퍼티가 true로 바뀐 이벤트를 무시하고 아무 일도 하지 않습니다. 프로그래머가 만든 이벤트 핸들러에서는 defaultPrevented 프로퍼티를 무시한 채 동작을 수행할 수 있고, 보통 그렇게 합니다.

 

[2]

두 번째 방법은 stopPropagation입니다. 이 메서드는 이벤트를 현재 요소에서 끝내고 더는 전달되지 않게 막습니다.

즉, 해당 요소에 연결된 이벤트 핸들러는 동작하지만 다른 요소에 연결된 이벤트 핸들러는 동작하지 않습니다.

 

[3]

마지막 방법은 가장 강력한 stopImmediatePropagation입니다. 이 메서드는 다른 이벤트 핸들러, 심지어 현재요소에 연결된 이벤트 핸들러도 동작하지 않게 막습니다.

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <title>Page Title</title>
</head>
<body>
    <div>
        <button>Click Me!</button>
    </div>
    <script>
    //이벤트 핸들러를 만들어 반환합니다.
    function logEvent(handlerName, type, cancel, stop, stopImmediate){
        //실제 이벤트 핸들러
        return function(evt){
            if(cancel){
                evt.preventDefault();
            }
            if(stop){
                evt.stopPropagation();
            }
            if(stopImmediate){
                evt.stopImmediatePropagatioin();
            }
            console.log(`${type} : ${handlerName}` + (evt.defaultPrevented ? '(canceled)' : ''));
        }

    }

    function addEventLogger(elt, type, action){
        const capture = type === 'capture';
        elt.addEventListener('click', logEvent(elt.tagName, type, action === 'cancel', action === 'stop', action === 'stop!'), capture);
    }

    const body = document.querySelector('body');
    const div = document.querySelector('div');
    const button = document.querySelector('button');
    addEventLogger(body, 'capture');
    addEventLogger(body, 'bublle');
    addEventLogger(div, 'capture');
    addEventLogger(div, 'bublle');
    addEventLogger(button, 'capture');
    addEventLogger(button, 'bublle');
    </script>
</body>
</html>

테스트 예제 페이지를 브라우저로 실행 후 , 버튼 클릭시 콘솔창을 확인하게되면

 

capture : BODY
capture : DIV
capture : BUTTON
bublle : BUTTON
bublle : DIV
bublle : BODY

 

이러한 식으로 클릭한 버튼의 가장 먼 조상인 BODY, DIV, BUTTON 순으로 caputuring이 일어나고, 이벤트가 일어난 요소에서 부터 시작해서 BUTTON, DIV, BODY순으로 bubbling이 일어나는 것을 확인할 수 있습니다.

즉 버튼에서는 핸들러가 캡쳐링 다음 버블링이라는 일반적인 순서를 무시하고 추가된 순서대로 실행합니다. 예를 들어 앞의 예제에서 버튼에 핸들러를 추가한 순서를 반대로 했다면 콘솔에서도 반대로 기록됩니다.

 


다음 예제에서는 이벤트를 취소해 봅시다.

다음과 같이 예제를 수정해서 <div>의 캡처 단계에서 취소되도록 해보겠습니다.

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <title>Page Title</title>
</head>
<body>
    <div>
        <button>Click Me!</button>
    </div>
    <script>
    //이벤트 핸들러를 만들어 반환합니다.
    function logEvent(handlerName, type, cancel, stop, stopImmediate){
        //실제 이벤트 핸들러
        return function(evt){
            if(cancel){
                evt.preventDefault();
            }
            if(stop){
                evt.stopPropagation();
            }
            if(stopImmediate){
                evt.stopImmediatePropagation();
            }
            console.log(`${type} : ${handlerName}` + (evt.defaultPrevented ? '(canceled)' : ''));
        }

    }

    function addEventLogger(elt, type, action){
        const capture = type === 'capture';
        elt.addEventListener('click', logEvent(elt.tagName, type, action === 'cancel', action === 'stop', action === 'stop!'), capture);
    }

    const body = document.querySelector('body');
    const div = document.querySelector('div');
    const button = document.querySelector('button');
    
    // addEventLogger(body, 'capture');
    // addEventLogger(body, 'bublle');
    // addEventLogger(div, 'capture');
    // addEventLogger(div, 'bublle');
    // addEventLogger(button, 'capture');
    // addEventLogger(button, 'bublle');

    // 이벤트 취소
    addEventLogger(body, 'capture');
    addEventLogger(body, 'bublle');
    addEventLogger(div, 'capture', 'cancel');
    addEventLogger(div, 'bublle');
    addEventLogger(button, 'capture');
    addEventLogger(button, 'bublle');


    </script>
</body>
</html>

콘솔창을 확인해보면 이벤트 전달은 계속되지만, 이벤트가 취소됐다고 기록된 걸 볼 수 있습니다.

 

capture : BODY
capture : DIV(canceled)
capture : BUTTON(canceled)
bublle : BUTTON(canceled)
bublle : DIV(canceled)
bublle : BODY(canceled)

 


다음으로는 버튼의 캡처 단계에서 이벤트 전달을 중지해 보겠습니다. (이벤트 리스너 부분만 수정하면 됩니다.)

//버튼의 캡쳐 단계에서 이벤트 전달 중지
addEventLogger(body, 'capture');
addEventLogger(body, 'bublle');
addEventLogger(div, 'capture', 'cancel');
addEventLogger(div, 'bublle');
addEventLogger(button, 'capture', 'stop');
addEventLogger(button, 'bublle');

capture : BODY
capture : DIV(canceled)
capture : BUTTON(canceled)
bublle : BUTTON(canceled)

 

버튼의 요소에서 이벤트 전달이 멈추는 것을 볼 수 있습니다. 캡처링까지 진행하고 멈추게 했지만, 버튼의 버블링 이벤트는 여전히 발생합니다. 하지만  <div>와 <body> 요소는 이벤트 버블링을 받지 못합니다.

 


마지막으로, 버튼의 캡쳐 단계에서 즉시 멈추게 만들어 봅시다.

//버튼의 캡쳐 단계에서 즉시 멈춤
addEventLogger(body, 'capture');
addEventLogger(body, 'bublle');
addEventLogger(div, 'capture', 'cancel');
addEventLogger(div, 'bublle');
addEventLogger(button, 'capture', 'stop!');
addEventLogger(button, 'bublle');

capture : BODY
capture : DIV(canceled)
capture : BUTTON(canceled)

 

이제 버튼의 캡처 단계에서 이벤트 전달이 완전히 멈췄고, 이후로는 아무 일도 일어나지 않습니다.

'DEV > JS' 카테고리의 다른 글

[JS]Promise (프라미스) - 1  (0) 2019.11.20
[JS]Ajax  (0) 2019.11.19
[JS]moment.js를 이용한 날짜 형식 다루기  (0) 2019.11.13
[JS]날짜 데이터 전송하기  (0) 2019.11.13
[JS]moment.js를 이용한 Timezone 다루기 (Date)  (0) 2019.11.12
Comments