본문 바로가기

프로그래밍/웹

[번역글] 자바스크립트의 이벤트 처리 순서 (Event order in Javascript)

원문 : https://www.quirksmode.org/js/events_order.html


첫 번역 글이라 뭔가 매끄럽지 못하네요. 궁금하신 점이 있거나 번역 수정에 대한 의견은 댓글로 주시면 감사하겠습니다.



자바스크립트 이벤트 순서

See section 7D of the book.

Netscape 4 only supports event capturing, Explorer only supports event bubbling. Netscape 6 and Konqueror support both, while Opera and iCab support neither.

 자바스크립트 이벤트 소개 에서 처음에 제가 이해가 잘 안되는 질문을 했었습니다. “만약 어떤 엘리먼트와 그 상위 엘리먼트가 같은 이벤트에 대한 핸들러를 가지고 있다면 어떤 핸들러가 먼저 호출될까요?” 당연히 브라우저가 어떻게 처리하는지에 따라 다릅니다.


문제는 간단합니다. 엘리먼트 안에 엘리먼트가 있다고 생각해보죠.

-----------------------------------
| element1                        |
|   -------------------------     |
|   |element2               |     |
|   -------------------------     |
|                                 |
-----------------------------------

그리고 두 엘리먼트 모두 'onClick' 이벤트 핸들러를 가지고 있습니다. 만약 사용자가 엘리먼트2를 클릭한다면 엘리먼트1과 엘리먼트2 모두에게 클릭 이벤트가 발생되겠죠. 그런데 어떤 엘리먼트의 이벤트가 먼저 발생할까요? 어떤 이벤트 핸들러가 먼저 실행되죠? 다른말로 하자면, 이 상황에서 이벤트 순서는 어떻게 되나요?


두 가지 모델

당연하게도(?), 아주 먼 옛날 넷스케이프와 마이크로소프트는 다른 답을 내렸습니다.

  • 넷스케이프는 상위 요소인 엘리먼트1의 이벤트를 먼저 처리했습니다. 이를 이벤트 캡쳐링(Event Capturing)이라 합니다.
  • 마이크로소프트는 자식 요소인 엘리먼트2의 이벤트를 먼저 처리했습니다. 이를 이벤트 버블링(Event Bubbling)이라 합니다.

이 두 가지 모델은 완전히 서로의 반대 방식으로 이벤트를 처리합니다. Explorer는 오직 이벤트 버블링만 지원합니다. Mozilla, Opera 7 그리고 Konqueror는 두 가지 전부 지원합니다. 구버전 Opera와 iCab은 둘 다 지원하지 않습니다.

이벤트 캡쳐링(Event capturing)

이벤트 캡쳐링을 사용한다면

               | |
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  \ /          |     |
|   -------------------------     |
|        Event CAPTURING          |
-----------------------------------

엘리먼트1의 이벤트 핸들러가 먼저 실행되고 그 다음 엘리먼트2의 이벤트 핸들러가 실행됩니다.

이벤트 버블링(Event bubbling)

이벤트 버블링을 사용한다면

               / \
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  | |          |     |
|   -------------------------     |
|        Event BUBBLING           |
----------------------------------- 

엘리먼트2의 이벤트 핸들러가 먼저 실행되고 그 다음 엘리먼트1의 이벤트 핸들러가 실행됩니다.


W3C 이벤트 모델

W3C는 이 두 모델 가운데 있는 아주 현명한 방식을 사용하기로 했습니다. W3C 이벤트 모델에서 발생하는 모든 이벤트는 타겟 엘리먼트에 도달할 때까지 이벤트 캡쳐링이 발생하고 그 이후에 타겟 엘리먼트로부터 이벤트 버블링이 발생합니다.

                 | |  / \
-----------------| |--| |-----------------
| element1       | |  | |                |
|   -------------| |--| |-----------     |
|   |element2    \ /  | |          |     |
|   --------------------------------     |
|        W3C event model                 |
------------------------------------------

웹 개발자들은 이벤트 핸들러를 캡쳐링 단계에 등록할지 버블링 단계에 등록할지 선택할 수 있습니다.  이는 고급 이벤트 모델 페이지에서 설명한  addEventListener() 함수를 통해 가능합니다. 이 함수의 마지막 인자가 true라면 이벤트 핸들러가 캡쳐링 단계에 등록되고,  false의 경우에는 버블링 단계에 이벤트 핸들러가 등록됩니다.


아래와 같은 코드에서

element1.addEventListener('click',doSomething2,true)
element2.addEventListener('click',doSomething,false)

만약 사용자가 엘리먼트2를 클릭한다면 다음과 같은 순서로 이벤트 처리가 진행됩니다.

  1. click 이벤트는 캡쳐링 단계를 시작합니다. 엘리먼트2의 상위 엘리먼트 중에 캡쳐링 단계로 등록되어 있는 onclick 이벤트 핸들러가 있는지 찾습니다.
  2. 이벤트는 상위 엘리먼트인 엘리먼트1에 캡쳐링 단계로 등록되어있는 doSomething2() 함수를 찾아서 실행합니다.
  3. 이벤트는 이제 타겟 엘리먼트인 엘리먼트2에 도달합니다. 캡쳐링 단계에 등록된 다른 이벤트 핸들러가 없기 때문에 이벤트는 이벤트 버블링 단계로 진입하여 엘리먼트2 자신의 버블링 단계에 등록된 이벤트 핸들러 doSomething()함수를 실행합니다.
  4. 이벤트는 이제 계속해서 상위로 이동하여 버블링 단계에 등록된 상위 엘리먼트의 이벤트 핸들러를 찾습니다. 현재 상위 엘리먼트의 버블링 단계에 등록된 이벤트 핸들러가 없으므로 아무일도 일어나지 않습니다.


반대의 경우를 생각해본다면

element1.addEventListener('click',doSomething2,false)
element2.addEventListener('click',doSomething,false)

이 경우 엘리먼트2를 클릭한다면:

  1. click 이벤트는 캡쳐링 단계를 시작합니다. 엘리먼트2의 상위 엘리먼트 중에 캡쳐링 단계로 등록되어 있는 onclick 이벤트 핸들러가 있는지 찾지만 캡쳐링 단계에는 등록된 이벤트 핸들러가 없습니다.
  2. 이벤트는 이제 타겟 엘리먼트인 엘리먼트2에 도달합니다. 이벤트는 이제 버블링 단계로 진입하여 엘리먼트2 자신의 버블링 단계에 등록된 이벤트 핸들러 doSomething()함수를 실행합니다.
  3. 이벤트는 이제 계속해서 상위로 이동하여 버블링 단계에 등록된 상위 엘리먼트의 이벤트 핸들러를 찾습니다.
  4. 이벤트는 엘리먼트1의 버블링 단계에 등록된 doSomething2() 함수를 찾아서 실행합니다.

이전 모델과의 호환성

W3C DOM을 지원하는 브라우저에서, 이전 이벤트 핸들러 등록 방식인 아래의 코드는

element1.onclick = doSomething2;

W3C 모델의 버블링 단계로 등록 됩니다.


이벤트 버블링 사용

이벤트 캡쳐링 또는 버블링을 의식적으로 사용하는 웹 개발자는 거의 없습니다. 요즘 웹페이지에서는 여러 이벤트 핸들러에서 버블링 이벤트를 처리할 필요가 없습니다. 사용자들은 한 번의 마우스 클릭으로 인해 여러가지 일들이 발생하는 것들을 혼란스러워 할 것이고, 아마 당신도 특정 이벤트에 대한 핸들링을 분리시키고 싶을 테니까요. 사용자가 어떤 엘리먼트를 클릭 했을 때 어떠한 일이 일어나고, 다른 엘리먼트를 클릭 했을 때에는 다른 일이 일어나도록 말이죠. 물론 이것들은 미래에 바뀔수도 있고, 미래에도 계속 사용 가능하도록 만드는 것은 중요합니다. 하지만 지금 이벤트 캡쳐링과 버블링의 가장 핵심적이고 실용적인 용도는 '기본 함수'의 등록입니다.


이벤트 캡쳐링과 버블링은 언제나 발생합니다.

가장 먼저 알아야하는 것은 이벤트 캡쳐링과 버블링이 항상 일어난다는 것입니다. 전체 도큐먼트에 대한 onclick 이벤트 핸들러를 정의한다고 가정해봅시다.

document.onclick = doSomething;
if (document.captureEvents) document.captureEvents(Event.CLICK);

이 도큐먼트안에 있는 모든 엘리먼트의 click 이벤트는 결국 이벤트 버블링을 통해 도큐먼트에 도착할 것이고, 도큐먼트에 등록되어있는 이벤트 핸들러를 실행시킬 것입니다. 오직 이전 이벤트 핸들러에서 명시적으로 버블링을 멈추도록 하는 경우에만 도큐먼트까지 버블링을 통해 이벤트가 전달되지 않습니다.


실제 사용 예제

모든 이벤트가 결국 도큐먼트까지 전달 되기 때문에 '기본 함수'를 만들 수 있습니다. 다음 페이지를 가정해봅시다:

------------------------------------
| document                         |
|   ---------------  ------------  |
|   | element1    |  | element2 |  |
|   ---------------  ------------  |
|                                  |
------------------------------------

element1.onclick = doSomething;
element2.onclick = doSomething;
document.onclick = defaultFunction;

여기서 사용자가 만약 엘리먼트1이나 엘리먼트2를 클릭한다면 doSomething()함수가 실행됩니다. 만약 당신이 이벤트의 전파를 막고 싶다면 이 함수에서 명시적으로 막을 수 있습니다. 그렇지 않으면 이벤트는 버블링되어 defaultFunction()함수까지 실행시킵니다. 만약 사용자가 엘리먼트1과 엘리먼트2가 아닌 다른 곳을 클릭한다면 defaultFunction()함수는 여전히 호출됩니다. 이렇게 기본 함수를 만드는 것은 가끔 쓸모가 있습니다.

드래그 앤 드롭을 처리하는 스크립트에서는 반드시 도큐먼트 전체에 대한 이벤트 핸들러를 설정해야 합니다. 보통은 어떤 레이어에 대한 mouseup 이벤트를 설정하기 위해서 해당 레이어를 선택하고 mousemove 이벤트에 반응시킵니다. mousedown 이벤트는 보통 브라우저 버그를 피하기 위해 레이어에 등록되어 있지만, 다른 두 이벤트 핸들러는는 모두 도큐먼트 전체에 대해 등록되어야 합니다.


브라우저학(Browserology)의 첫 번째 법칙을 기억하세요: 미리 대응하지 않은 모든 일이 발생할 수 있다는 것. 그러므로 사용자가 마우스를 굉장히 넓게 움직여서 이벤트가 등록된 레이어를 마우스 포인터가 벗어날 경우 스크립트는 제대로 동작하지 않게 될 것입니다.

  • 만약 onmousemove 이벤트 핸들러가 특정 레이어에 등록된다면, 그 레이어는 더 이상 마우스의 움직임에 반응하지 않게 될 것이고 이는 사용자를 혼란스럽게 할 것입니다.
  • 만약 onmouseup 이벤트 핸들러가 어떤 레이어에 등록된다면, 이벤트 핸들러는 사용자가 드래그해서 드롭한 이벤트를 처리하지 못할 것이고 이는 더욱 사용자를 혼란스럽게 할 것입니다.

이렇게 도큐먼트 전체에 등록되어 항상 실행되어야하는 이벤트 핸들러가 필요한 경우에 이벤트 버블링은 아주 유용합니다.


버블링 끄기

하지만 당신은 함수들이 서로 간섭되지 않도록 이벤트 캡쳐링과 버블링을 끄고 싶을것입니다. 게다가 도큐먼트 구조가 아주 복잡하다면 이벤트 버블링을 꺼서 시스템 리소스를 아낄 수도 있죠. 브라우저는 모든 이벤트에 대해 최상위 엘리먼트까지 이벤트 핸들러 등록여부를 확인해야합니다. 아무것도 등록이 되어있지 않아도 탐색하는데에 시간이 소요되죠.

마이크로소프트 모델에서는 cancelBubble 속성을 true로 설정하여 버블링을 끌 수 있습니다.

window.event.cancelBubble = true

W3C 모델에서는 이벤트의 stopPropagation() 함수를 실행해야합니다.

e.stopPropagation()

이 함수는 버블링 단계의 모든 이벤트 전파를 막습니다. 크로스 브라우져를 지원하기 위해 아래와 같이 사용합니다.

function doSomething(e)
{
	if (!e) var e = window.event;
	e.cancelBubble = true;
	if (e.stopPropagation) e.stopPropagation();
}

cancelBubble 속성을 지원하지 않는 브라우져라고 해도 이 코드는 별 문제가 없습니다. 아마 이 속성이 뭔가 싶어서 그냥 속성을 하나 만들어 버리겠죠. 물론 이게 실제로 버블링을 종료하는 기능을 하지 않겠지만 이렇게 값을 할당하는 것은 문제가 없습니다.


현재 타겟 엘리먼트

위에서 본 것처럼 이벤트는 어떤 엘리먼트에서 일어날지에 대한 target 또는 srcElement 를 가지고 있습니다. 우리가 본 예제에서는 사용자가 클릭한 엘리먼트2를 말하는 것이죠.

중요한 것은 이벤트 캡쳐링과 버블링 단계에서 이 타겟 엘리먼트는 바뀌지 않는다는 것입니다. 타겟은 항상 엘리먼트2로 고정되어 있습니다.

하지만 다음과 같은 경우는 가정해봅시다.

element1.onclick = doSomething;
element2.onclick = doSomething;

만약 사용자가 엘리먼트2를 클릭한다면 doSomething() 함수는 두 번 호출 될 것입니다. 그런데 지금 이 함수를 호출하는 이벤트가 어떤 HTML 엘리먼트에서 처리되는지 어떻게 알 수 있을까요? target/srcElement 는 별 도움이 안됩니다. 이것들은 항상 최초의 이벤트 타겟만을 바라보고 있습니다.

T이 문제를 해결하기 위해서 W3C currentTarget 이라는 속성을 추가했습니다. 이 속성은 현재 이벤트가 처리되고 있는 HTML 엘리먼트를 알려줍니다. 안타깝게도 마이크로소프트의 이벤트 모델에는 비슷한 속성이 없습니다.

여러분은 또한 this 키워드를 사용할 수도 있습니다. currentTarget과 같이 위와 같은 상황에서 현재 이벤트를 처리하고 있는 HTML 엘리먼트를 가리킵니다.


마이크로소프트 이벤트 모델의 문제점

만약 당신이 마이크로소프트 이벤트 모델을 사용한다면 this 키워드는 HTML 엘리먼트를 가리키지 않습니다. currentTarget과 같은 속성도 없기 때문에  다음과 같은 코드에서

element1.attachEvent('onclick',doSomething)
element2.attachEvent('onclick',doSomething)

현재 이벤트를 처리하고 있는 HTML엘리먼트가 어떤 것인지 알 수가 없습니다. 이것은 마이크로소프트 이벤트 등록 모델의 아주 심각한 문제이고 이 때문에 저는 마이크로소프트의 이벤트 등록 모델을 사용하고있지 않습니다. 심지어 익스플로러/윈도우 에서만 사용하는 어플리케이션에서조차도 말이죠.

저는 마이크로소프트에서 currentTarget과 같은 속성을 제공하거나 현재 사용되는 표준을 지켜주었으면 좋겠군요. 웹 개발자들은 이 정보가 필요합니다.


계속해서

모든 이벤트 관련 페이지를 순서대로 살펴보고 싶다면, 다음 순서인 마우스 이벤트 페이지로 넘어가세요.

'프로그래밍 > ' 카테고리의 다른 글

Chart.js 버그 픽스 컨트리뷰션  (0) 2024.01.15
혜움 레포트 프론트 개선 - 2  (1) 2024.01.08
혜움 레포트 프론트 개선 - 1  (1) 2024.01.05
타입스크립트가 싫다  (0) 2021.04.15
웹 푸시 구현에 대한 고민  (0) 2019.08.01