시작하면서
몇 가지 질문으로 시작해볼까요? 다음과 같은 상황을 가정해 봅시다.
---------------------
| parent |
| --------------- |
| | child | |
| --------------- |
| |
---------------------
그리고 parent
와 child
엘리먼트 둘 다 click
이벤트에 대한 핸들러를 가지고 있다고 합시다. 만약 사용자가 child
엘리먼트를 클릭한다면 parent
와 child
모두에게 클릭 이벤트가 발생하겠죠. 그렇다면 어떤 엘리먼트의 이벤트 핸들러가 먼저 실행될까요? 다른 말로 하자면, 이 상황에서 이벤트 처리 순서는 어떻게 될까요?
(고대의) 두 가지 이벤트 처리 모델
너무나 당연하게도(?), 아주 먼 옛날 넷스케이프와 마이크로소프트는 다른 답을 내렸습니다. 만악의 근원
- 넷스케이프 모델: 상위 엘리먼트인
parent
의 이벤트부터 처리해야지! => 이벤트 캡쳐링(Event Capturing)- 마이크로소프트 모델: 자식 엘리먼트인
child
의 이벤트부터 처리해야지! => 이벤트 버블링(Event Bubbling)
이 두 모델은 이벤트를 완전히 반대 방식으로 처리합니다. Explorer는 오직 이벤트 버블링만 지원합니다. Mozilla, Opera 7 그리고 Konqueror는 두 가지 전부 지원합니다. 구버전 Opera와 iCab은 둘 다 지원하지 않습니다.
이벤트 캡쳐링 by 넷스케이프
||
-------------||------
| parent || |
| ----------||--- |
| | child \/ | |
| --------------- |
| |
---------------------
이벤트 캡쳐링을 사용한다면 parent
의 이벤트 핸들러가 먼저 실행되고 그 다음 child
의 이벤트 핸들러가 실행됩니다.
이벤트 버블링 by 마이크로소프트
/\
-------------||------
| parent || |
| ----------||--- |
| | child || | |
| --------------- |
| |
---------------------
이벤트 버블을 사용한다면 child
의 이벤트 핸들러가 먼저 실행되고 그 다음 parent
의 이벤트 핸들러가 실행됩니다.
(현대의) W3C 이벤트 모델
W3C는 이 두 모델을 모두 포용하는 아주 현명한 방식을 사용하기로 했습니다. W3C 이벤트 모델은 타깃 엘리먼트에 도달할 때까지 이벤트 캡쳐링이 발생하고 그 이후 타깃 엘리먼트로부터 이벤트 버블링이 발생합니다.
|| /\
-----------||-||-----
| parent ||-|| |
| --------||-||-- |
| | child \/ || | |
| --------------- |
| |
---------------------
개발자들은 이벤트 핸들러를 캡쳐링 단계에 등록할지 버블링 단계에 등록할지 선택할 수 있습니다. addEventListener()
함수 마지막 인자를 true
로 전달하면 이벤트 핸들러가 캡쳐링 단계에 등록되고, false
인 경우에는 버블링 단계에 이벤트 핸들러가 등록됩니다. 아무 값도 전달하지 않는다면 기본적으로 false
에 해당하는 이벤트 버블링 단계에 이벤트 핸들러가 등록됩니다.
W3C 모델 실제 코드로 살펴보기
Case 1
parent.addEventListener('click', doSomethingParent, true); // 캡쳐링 단계 등록
child.addEventListener('click', doSomethingChild, false); // 버블링 단계 등록
위와 같은 코드에서, 만약 사용자가 child 엘리먼트를 클릭한다면 이벤트 처리 순서는 다음과 같습니다.
이벤트 처리 순서 |
---|
1. click 이벤트는 캡쳐링 단계를 시작합니다. 도큐먼트의 최상위 엘리먼트부터 child 엘리먼트까지 캡쳐링 단계에 등록되어 있는 onclick 이벤트 핸들러가 있는지 찾습니다. |
2. 이벤트는 child 의 상위 엘리먼트인 parent 에 캡쳐링 단계로 등록된 doSomethingParent 함수를 찾아서 실행합니다. |
3. 이벤트 캡쳐링 단계가 타깃 엘리먼트인 child 에 도달합니다. 캡쳐링 단계에 등록된 다른 이벤트 핸들러가 없으므로 다음 단계인 이벤트 버블링 단계로 진입하여 child 엘리먼트부터 도큐먼트의 최상위 엘리먼트까지 버블링 단계에 등록된 이벤트 핸들러를 찾습니다. |
4. child 엘리먼트의 버블링 단계로 등록된 doSomethingChild 함수를 찾아서 실행합니다. |
5. 이벤트 버블링을 통해 계속해서 상위 엘리먼트의 버블링 단계에 등록된 이벤트 핸들러를 찾습니다. 현재는 상위 엘리먼트의 버블링 단계에 등록된 이벤트 핸들러가 없으므로 아무일도 일어나지 않습니다. |
Case 2
parent.addEventListener('click', doSomethingParent, false); // 버블링 단계 등록
child.addEventListener('click', doSomethingChild, false); // 버블링 단계 등록
위와 같은 코드에서, 만약 사용자가 child 엘리먼트를 클릭한다면 이벤트 처리 순서는 다음과 같습니다.
이벤트 처리 순서 |
---|
1. click 이벤트는 캡쳐링 단계를 시작합니다. 도큐먼트의 최상위 엘리먼트부터 child 엘리먼트까지 캡쳐링 단계에 등록되어 있는 onclick 이벤트 핸들러가 있는지 찾습니다. |
2. 이벤트 캡쳐링 단계에 등록된 이벤트 핸들러가 없으므로 아무 일도 일어나지 않고 이벤트 캡쳐링 단계가 타깃 엘리먼트인 child 에 도달합니다. |
3. 다음 단계인 이벤트 버블링 단계로 진입하여 child 엘리먼트부터 도큐먼트의 최상위 엘리먼트까지 버블링 단계에 등록된 이벤트 핸들러를 찾습니다. |
4. child 엘리먼트의 버블링 단계로 등록된 doSomethingChild 함수를 찾아서 실행합니다. |
5. 이벤트 버블링을 통해 상위 엘리먼트인 parent 의 버블링 단계에 등록된 doSomethingParent 함수를 찾아서 실행합니다. |
참고할만한 것들
고대의 이벤트 처리 모델과의 호환성
parent.onclick = doSomethingParent;
W3C DOM을 지원하는 브라우저에서, 이전 이벤트 핸들러 등록방식인 위 코드는 W3C 모델의 버블링 단계로 등록됩니다.
이벤트 버블링 중단하기
개발을 하다보면 더 이상 이벤트 버블링으로 이벤트가 전파되는 것을 막고싶을 때가 생깁니다. 예를 들면 parent
와 child
모두의 버블링 단계에 이벤트 핸들러가 등록되어 있고, child
를 클릭했을 때 parent
의 클릭 이벤트 핸들러를 실행하지 않고 child
의 클릭 이벤트 핸들러만 실행하고 싶은 경우겠죠.
마이크로소프트 모델에서는 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();
}
}
근래에는 Explorer 호환을 고려하지 않는 웹 어플리케이션이 많아져서 쓸모 없는 정보일 수도 있지만 참고로 알아두시면 될 것 같습니다. 쓸모가 없기를 진심으로 바랍니다
현재 타깃 엘리먼트 파악하기
위에서 살펴본 것 처럼 이벤트는 어떤 엘리먼트에서 발생했는지에 대한 정보인 target
혹은 srcElement
를 가지고 있습니다. 우리가 본 예제에서는 사용자가 클릭한 child
엘리먼트를 말하는 것이죠. 이 타겟 엘리먼트는 이벤트가 어떤 엘리먼트에서 처리되든지 바뀌지 않습니다.
parent.addEventListener('click', doSomething);
child.addEventListener('click', doSomething);
위와 같이 parent
와 child
에 클릭 이벤트 핸들러로 같은 이벤트가 등록되어 있다면, child
를 클릭했을 때 doSomething
함수는 두 번 호출 됩니다. 문제는 이 함수가 두 번 실행될 때 어떤 HTML 엘리먼트에서 처리되고 있는지 알 수 있는 방법이 없다는 것입니다. 이벤트의 target/srcElement
속성은 계속 child
엘리먼트를 가르키고 있기 때문입니다. 최초의 이벤트 타겟을 계속 바라보고 있는 것이죠.
이 문제를 해결하기 위해 W3C는 이벤트에 currentTarget
이라는 속성을 추가했습니다. 이 속성은 현재 이벤트가 처리되고 있는 HTML 엘리먼트를 알려줍니다. 물론 이벤트 핸들러 안에 this
를 사용할 수도 있지만, 되도록 javascript에서 this
는 사용하지 않기를 권장합니다. (왜 그런지는 다음에 기회가 있다면 포스트를 작성하도록 하겠습니다.)
참고로 마이크로소프트 이벤트 모델에서는 currentTarget
속성이 존재하지 않아서 위와 같은 경우에 어떤 엘리먼트에서 이벤트를 처리하고 있는지 알 수 없습니다. 부디 이게 문제가 되지 않기를 바랍니다. Explorer 호환을 위해 개발하고 있다는 뜻이므로…
React.js 에서의 이벤트
리액트에서는 크로스 브라우저 지원을 위해 SyntheticEvent
라는 래퍼(Wrapper)를 통해 이벤트 관련 정보를 전달합니다. 이 객체는 각 브라우저의 native 이벤트 객채와 같은 인터페이스를 가지고 있습니다.
이벤트 캡쳐링과 버블링
리액트에서는 jsx를 사용하여 html 태그 안에 이벤트 핸들러 함수를 등록할 수 있습니다. 기본적으로 모든 이벤트 핸들러 속성은 버블링 단계에 등록이 됩니다. 이벤트 핸들러를 캡쳐링 단계에 등록하려면 핸들러 속성 이름에 Capture를 뒤에 붙여서 등록하면 됩니다.
const SampleComponent = (props) => {
const handleClick = (e) => {
console.log('clicked!');
};
return (
<div
onClick={handleClick}
onClickCapture={handleClick}
/>
);
};
리액트에서도 동일하게 e.stopPropagation()
함수를 통해 이벤트 전파를 중단할 수 있습니다.
리액트에서 지원하는 이벤트 핸들러 속성은 여기를 참고해주세요.
마치며
원티드 기술 블로그에 올리는 첫 글이네요. 부족한 부분이 많네요. 제가 잘못 쓴 부분이나 의견은 댓글로 적어주시면 감사하겠습니다. :)
참고 문서
- React.js 공식홈페이지 https://reactjs.org/
- https://www.quirksmode.org/js/events_order.html