2019-09-28 TIL

2019-09-28

https://levelup.gitconnected.com/how-exactly-does-react-handles-events-71e8b5e359f2

이어서..

The React and React Native Event System Explained: A Harmonious Coexistence (2 / 2)

An overview of React’s event handling flow (내용에 언급돼서 그림 다시가져옴)

Receiving (listening to) events

모든 경우의 이벤트 핸들링은 listening 페이즈로 시작한다는걸 위 그림을 통해 볼 수 있다. 이건 조금 놀랍다. 우리는 대부분 커스텀 리스너를 정의하는데 익숙한데 왜냐하면 예를 들어 mousescroll 말고 click에만 반응하길 원하기 때문임. 그러나 왜 리액트가 직접 모든 이벤트를 리스닝 할 필요가 있는걸까? 이건 이벤트들은 그들의 “natural” 환경에서 나타나기 때문임 : 웹이면 DOM, 모바일이면 native. 리액트는, 웹이든 네이티브든, 그들의 기초적인 환경의 위에 만들어진 툴이다. 그 결과, 이벤트들은 리액트를 통과하지 않으며, 리액트가 이벤트를 열심히 리스닝해야 하는 것이다.

Receiving events: React web

리액트 웹에서는, 과정은 아주 간단하며 top-level delegation(위임)을 활용한다. 이것은 리액트가 document레벨의 모든 이벤트를 리스닝한다는 뜻인데, 이것은 리액트 관련 코드가 실행되는 시점에는 이미 이벤트가 DOM 트리를 따라 첫번째 캡쳐링/버블링 사이클을 마쳤음을 의미한다.

브라우저로부터 이벤트를 받은 다음에, 리액트는 추가적인 cross-browsing harmonization 단계를 수행한다. 같은 효과를 갖는 이벤트가 브라우저 마다 이름이 다르 경우의 예비 수단으로, 리액트는 topLevelTypes를 정의하는데 이건 브라우저 특정적인 이벤트의 wrapper다. 예를 들어, transitionEnd, webkitTransitionEnd, MozTransitionEnd, oTransitionEnd는 모두 topAnimationEnd가 된다.

Receiving events: React Native

리액트 네이티브에서는, 이벤트들은 리액트와 네이티브 코드를 연결하는 브릿지를 통해 수신된다. 간단히 말해, View가 생성될 때 마다 리액트는 네이티브에게 ID 번호를 전달하고, 이 엘리먼트에 관련된 모든 이벤트를 받을 수 있게 된다. 이번에도 (터치)이벤트 downstream을 전달하기 전에 약간의 변형이 가해지는데, toucheschangedTouches 배열을 이벤트에 추가하는 것을 포함하며 이건 W3 표준을 따르게 하기 위함이다.

지금부터는, 나중에 소개할 SyntheticEvents와 구분짓기 위해 지금까지 “events”(즉, 네이티브나 브라우저로부터 가져와 약간 수정한 이벤트 오브젝트)라고 부르던 것을 “native events”라고 부르도록 하겠다.

The innards of React’s event management system

이제 진짜 작업을 시작할 준비가 됐다 : 이벤트를 적절한 콜백(들)에 전달하는 것이다. 이건 리액트 이벤트 시스템의 의무다. 자세히 살펴보자.

Flow of events inside React’s event system

뭐가 많다. 그래도 EventPluginHub와 이것의 이벤트 플러그인들은 눈에 띈다. EventPluginHub는 사실상 전체 시스템의 주축이다. 왜냐면 EventPluginHub는, :

  • 주입될 이벤트 플러그인들의 통합된 인터페이스를 제공하고
  • 새로운 네이티브 이벤트가 수신될 때 마다 주입된 플러그인들을 통해 실행되며 이벤트들을 dispatch하기 전에 반환된 SyntheticEvents를 수집한다.

한 편, 이벤트 플러그인들은 모두 유사한 구조를 가지고 있고, 네이티브 이벤트를 입력으로 받아 하나 이상의 SyntheticEvents를 출력한다. 이벤트 플러그인은 최종적으로 어레이를 내보낸다. (어레이는 나중 단계에서 실행되어야 할 dispatch들(함수들)을 모은 어레이) SyntheticEvents는 네이티브 이벤트를 감싸는 리액트만의 wrapper인데, stopPropagation()preventDefault()를 포함해 여러분이 이미 사용하고있는 브라우저 이벤트와 동일한 인터페이스를 가지고 있다. (더 많은 정보를 원한다면 SyntheticEvents에 대한 리액트 공식문서를 참조하기 바람)

Event Plugins

SimpleEventPlugin(onClick, onTouch등을 다룸)과, 유명한 ResponderEventPlugin을 포함해 이벤트에 관련한 아주 다양한 플러그인들이 있지만 그것들은 모두 같은 패턴을 따름.

  1. 네이티브 이벤트에 대해 하나 혹은 그보다 많은 SyntheticEvents를 생성함
  2. SyntheticEvents에 관련된 모든 dispatch들(여러분이 작성, 제공한 함수들)을 수집함 (예를 들어 dispatch는 onTouchStart={doStuff}에서 doStuff를 말하는것임)
  3. 모든 SyntheticEvents를 그들의 dispatch와 함께 반환함

여기서 주목할 것은 플러그인에서 어떤 dispatch도 실행되지 않는다는 것인데, 플러그인들은 함수들을 수집할 뿐이기 때문임. (어떤 플러그인들은 수집단계에 특정 dispatch를 실행시키기도 하지만 예외적이다.) SyntheticEvents는 간단히 네이티브 이벤트(click이나 drag같은 것)를 미러링할 수 있고 또는 더 복잡한 touchTap같은 이벤트가 될 수도 있지만 모든 경우에 처리할 준비가 되도록 dispatch 어레이가 붙은 상태로 리턴된다.

주석: 한마디로 SyntheticEvents엔 dispatch들을 담은 어레이가 꼭 포함된다는 얘기?

dispatch들을 수집하기 위해서, 리액트는 컴포넌트 트리를 캡쳐링, 버블링 페이즈로 두번 순회한다. root부터 안쪽의 타겟으로 가는게 캡쳐링 다시 root로 돌아가는게 버블링.

Double traversal

서로 다른 모든 dispatch들(플러그인의 바깥에서 실행된 것들, 즉 대부분의 dispatch)에 대해서, 이중 순회(double traversal)가 완전하게 일어난다. stopPropagation()같은 인터럽트는 dispatch 시점에 효과를 발휘하는데, 이 SyntheticEvent에 해당되는 이어지는 함수들의 실행을 효과적으로 막는다. (결론 참조)

The EventPluginHub

모든 이벤트 플러그인들은 앱 실행시 EventPluginHub에 주입되고, 플러그인들은 설정 파일을 따라 정렬됨. 그리고, 실행시점(runtime)에, EventPluginHub은 네이티브 이벤트를 받을 때 마다 다음을 수행한다.

  1. 각 플러그인에 대해(순서대로), 모든 SyntheticEvents와 그것들의 dispatch configuration을 모으고 큐에 쌓는다.
  2. 큐에 있는, 모든 이벤트들에 대한 dispatch를 실행하고 효과적으로 clear함.

여기까지임! 여러분의 콜백들은 올바른 이벤트에 맞게 실행된다.

Consequences & conclusion

이 시스템의 흥미로운 결론은 하나의 네이티브 이벤트가 다수의 SyntheticEvent들을 만들어낼 수 있고, 각각은 그걸 만들어낸 플러그인에 한정된 스코프를 갖는다는 것이다. 다음과 같은 의미를 갖는다 :

  • SyntheticEventnativeEvent부분만이 플러그인과 플러그인 간에 전달된다. 따라서 nativeEvent의 변경은 다음에 이어지는 플러그인들의 실행 전반에 영향을 주지만, SyntheticEvent의 변경은 그렇지 않다.
  • SyntheticEvent의 제한된 스코프 때문에, stopPropagation()같은 메서드의 호출은 단 하나의 이벤트 플러그인에 대해서만 작동한다.

두번째에 대한 예시로, 우리가 두 개의 플러그인 AB을 갖고있다고 상상해보자. 이것은 각각 eventAeventB라는 synthetic event를 정의한다. 우리는 이 이벤트들이 다음과 같은 이름을 갖는다고 가정 할 것이다 : 버블링 페이즈의 onEventA, onEventB 그리고 캡쳐링 페이즈의 onEventACapure, onEventBCapture. 마침내, 그 둘이 똑같은 최상위 타입(topClick)에 의해 트리거되고 [A, B]로 정렬되었다. 이제 다음 RN 코드를 살펴보자 :

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

어떤 클릭 이벤트가 eventA를 캡쳐링 페이즈에 트리거하고, 중첩된 컴포넌트 안의 stopPropagation()를 호출해 이어지는 버블링 페이즈를 효과적으로 막는다. 기대한 대로, 'onEventA'는 나타나지 않는다. 그러나, eventB는 다른 플러그인에 정의되었기 때문에 다른 SyntheticEvent에 의존하고, 'onEventB'는 마침내 콘솔에 출력될 것이다. 이것이 꽤나 엣지 케이스에 해당함에도 불구하고, 필자는 이것이 예기치 않은 동작을 유발하리라는 것을 알 수 있다.

SyntheticEvent공동 사용된다는 사실 등 리액트의 이벤트 핸들링 시스템에 대해 말할 것이 더 있지만 과한 감이 있어 그러지 않았다.

////// 바로위 링크 내용인데 이건 읽어야한다

Event Pooling

The SyntheticEvent is pooled. This means that the SyntheticEvent object will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. As such, you cannot access the event in an asynchronous way.

function onClick(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // Won't work. this.state.clickEvent will only contain null values.
  this.setState({clickEvent: event});

  // You can still export event properties.
  this.setState({eventType: event.type});
}

Note:

If you want to access the event properties in an asynchronous way, you should call event.persist() on the event, which will remove the synthetic event from the pool and allow references to the event to be retained by user code.

////// 요약하면, synthetic event 오브젝트는 콜백이 실행된 후에 내용을 비워 재사용되기 때문에 이벤트에 비동기적으로 접근할 수 없다는 것. 다만 persist()로 이벤트를 보존하는 방법이 있다.

주석: 이어서. 뒤는 별내용 없어서 대충 링크만 남김

굉장한 영상 by Kent C. Dodds, Dan Abramov & Ben Alpert 보길 추천한다.


Minchang Kim
Minchang Kim
웹/앱 개발자 김민창입니다! 좋은 하루 되세요!