리액트(React)의 이벤트 핸들러(Event Handler) SyntheticEvent — nativeEvent

한영재
Tapjoy Korea
Published in
5 min readMar 19, 2019

작성배경

프론트개발을 하다보면 누구나 한번쯤 event-bubbling(이벤트 버블링) 을 방지하기 위해 event.stopPropagation 를 사용해본 경험이 있을 것이다. 필자의 경우에 이를 사용하다가, 리액트에 의해 래핑되지 않은 브라우저의 기본 이벤트(이하 native event) 가 필요한 경우가 있었다. 이로 말미암아 리액트의 이벤트 핸들러가 어떻게 구성되어 있고 동작하는지 궁금해졌고 짧게나마 공부한 지식을 정리해보려고 한다.

본 글은 리액트와 이벤트 핸들러에 대한 기본적인 지식이 있다는 가정하게 작성 되었습니다.

SyntheticEvent 그리고 nativeEvent

리액트에서 이벤트가 발생할 시, 이벤트 핸들러는 SyntheticEvent의 인스턴스를 전달한다. 즉 리액트로 개발 시 우리는 native event가 아니라 래핑된 이벤트(SyntheticEvent)를 사용 하게 되는데, 이것은 우리가 흔히 사용하는 stopPropagation preventDefault 를 포함하여 브라우저의 기본 이벤트(nativeEvent)와 동일한 인터페이스를 가지고 있다. 이 말인 즉슨 당신이 리액트에서사용하던 이벤트 핸들러는 S급 짝퉁 이라는 것이다. 하지만 친절히도 우리가 리액트를 개발 도중 어떤 이유로 native event 가 필요할 때를 위해, 이벤트에 nativeEvent 속성을 넣어놓았다. (다음과 같이 접근 할 수 있다 — event.nativeEvent.stopImmediatePropagtion())

그렇다면 이 짝퉁 SyntheticEvent 내부적으로 어떻게 이루어져있을까?

EventPlugin 그리고 EventPluginHub

— Event Plugin
이해에 필요한 부분만 간략하게 설명하자면, 네이티브 이벤트에 대응하여 SyntheticEvents 를 생성하고, 이에 관련된 모든 dispatch를 수집하여 array(큐)로 반환한다. 중요한 것은 실행은 하지 않는다.(지극히 예외적으로 일부 플러그인은 수집 단계에서 특정 디스패치를 실행하기도 한다.) 이 때 관련된 모든 dispatch를 수집하기 위해 리액트는 capture and bubbling phase를 참조하면서 컴포넌트(DOM) 트리를 두 번 탐색 하며 순서대로 큐에 넣는다.

이 때 주의할 것은 수집단계에서 즉시 실행되는 것을 제외한 dispatch (즉, 대부분의 dispatch)에 대해서만 이중 탐색이 온전하게 진행 되어 큐에 들어가는데, stopPropagation 과 같은 인터럽션은 동일한 SyntheticEvent 에 속한 함수의 실행만 방지한다. 한마디로 같은 큐에 속한 dispatch 들의 실행만 방지하는 것이다. (결론 참조)

— Event Plugin Hub
모든 이벤트 플러그인은 웹 또는 앱이 실행되자마자 Event Plugin Hub 에 주입되며, configuration file 에 따라 플러그인이 정렬된다. 그런 다음, 위에서 언급했듯 런타임에 Event Plugin Hubnative event를 받을 때마다 다음을 수행한다.

각 플러그인 마다 순서대로 모든 SyntheticEvents과 관련된 dispatch를 수집하고 그들을 큐에 저장한다. 이후 큐에 있는 이벤트와 관련된 모든 dispatch 들을 실행하고 큐에서 삭제한다.

이런 과정을 통해 우리의 예상대로 이벤트 핸들링이 진행된다.

결론 (예시)

위에서 언급했듯, stopPropagation은 오직 동일한 SyntheticEvent(동일한 큐)에 속한 dispatch 들의 실행만 방지한다. 즉 이벤트 플러그인 하나에서만 작동하는데 아래와 같은 흥미로운 결과를 야기할 수 있다.

class App extends React.Component {  
render() {
return(
<div
onEventA={(evt) => console.log('onEventA')}
onEventB={(evt) => console.log('onEventB')}>
<div onEventAFmaily={(evt) => evt.stopPropagation()} />
</div>
)
}
}

이 코드에서 onEventAonEventB가 다른 SyntheticEvent에 속해 있고 onEventAFamilyonEventA와 같은 SyntheticEvent 라고 생각해보자. 만약 onEventAFamily를 클릭하면 이벤트 버블링으로 인해 부모 div의 이벤트도 발생할 것이다. 하지만 evt.stopPropagation() 메서드 호출로 전파를 막고(해당 dispatch 뒤에 있는 것들은 실행되지 않을 것이다) onEventAonEventB모두 실행되지 않아야 하지만. 안타깝게도 다른 SyntheticEvent(다른 큐)에 속한 ‘onEventB’는 콘솔에 출력된다. 이는 분명히 edge-case이다.

맺으며

글을 작성하게 된 연유는 evt.stopPropagation() 으로도 막을 수 없는 이벤트들이 있어서 였다. 나 같은 경우에 evt.nativeEvent.stopImmediatePropagation() 을 사용해야 했다. 플러그 인(큐)의 종류에 관계없이 동작하는 것이 필요 했기 때문이다. 때문에 SyntheticEvent에 존재할 수 없어, nativeEvent에 존재 하는 것이고 이를 위해 nativeEvent 에 접근하는 것을 남겨둔 것이 아닐까 추론해 본다.

참고 자료

  1. The React and React Native Event System Explained: A Harmonious Coexistence [ https://levelup.gitconnected.com/how-exactly-does-react-handles-events-71e8b5e359f2 ]

2. 리액트 공식 문서

--

--