[React] React 에서는 이벤트를 어떻게 처리할까 ?

한호수 (The Lake)·2023년 4월 14일
0
post-thumbnail

Synthetic Event 키워드를 발견했을때 낯익음과 낯설음이 공존했다. React에서 버튼을 클릭하는 이벤트를 만들었을때 넘어오는 객체를 콘솔로 찍어보면 나왔던 객체였다. 슬며시 보았을때는 왜 이런 객체가 나오는지 이해가 가지 않았는데 오랜만에 만난김에 정리하기로 하였다.

요약

  1. React는 기존 이벤트를 포함한 SyntheticEvent 객체를 사용한다.
  2. React는 root Node에 모든 이벤트 핸들러를 연결시켜 이벤트 위임으로 이벤트를 처리한다.
  3. 이벤트가 트리거 되면 Fiber Tree를 따라서 위로 올라가면서 같은 이벤트 핸들러를 트리거 시키면서 DispatchQueue에 추가한다.
  4. root 에 도착하고 나면, DispatchQueue를 반복문으로 순회하면서 캡쳐링 또는 버블링 여부에 따라 순서대로 이벤트를 실행시킨다.

자바스크립트에서는 어떻게 이벤트를 감지할 수 있을까?

HTML 요소의 속성에 직접 inline event를 추가하는 방법이 있다. 하지만 코드가 복잡해지면 유지보수와 관리가 어려워지기 때문에 권장하지 않는다.

<button onclick="alert('Hello, this is my old-fashioned event handler!');">Press me</button>

JavaScript를 사용하여 DOM 요소에 직접 이벤트 핸들러를 추가하여 감지하는 방법이며 주로 사용하는 방법이다.

const bgChange = () => { console.log("클릭") };
const btn = document.querySelector('button');
btn.addEventListener('click', bgChange); // 버튼을 클릭하면 "클릭"이 출력된다.

그렇다면 리액트는 어떻게 이벤트를 처리할까?

리액트에서는 합성 이벤트(SyntheticEvent) 라는 객체를 이용하여 이벤트를 처리한다.

합성 이벤트(SyntheticEvent)
DOM의 Event() 생성자로 생성한 이벤트는 브라우저가 생성하는 이벤트와 구분하기 위해 합성 이벤트(synthetic event)라고 부릅니다.
하지만 React에서는 DOM Event(Native Event)를 포함한 래퍼객체 SyntheticEvent 를 의미합니다.

왜 합성이벤트 객체를 만들어서 사용할까?

브라우저별 호환성(크로스 브라우징)과 사용자의 편의성을 위해서 합성 이벤트를 사용한다.
기존 Event를 감싼 SyntheticEvent로 사용되며 일관적으로 이벤트를 처리할 수 있도록 만들었다.

합성이벤트는 어떻게 확인 할 수 있을까?

간단하게 jsx 요소 중 DOM 요소에 이벤트핸들러를 추가하고 넘어오는 객체를 확인하면 된다.

	<input onChange={(e) => { console.log(e) }} />

React에서 내부적으로 이벤트 핸들러가 추가되는 방식

결론부터 말하자면 React는 root DOM node에 모든 이벤트 핸들러들을 부착하여 이벤트 위임을 통해 모든 이벤트를 제어한다.

  1. 기존 DOM Event는 "mousedown"등 단어와 단어가 소문자로만 이루어져 있기 때문에 카멜케이스로 변경한 후 "on"을 붙여 React에서 제공하는 이벤트 핸들러 네이밍으로 변경한다.

  2. 그 후 이벤트마다 0부터 2까지 우선순위를 부여한다. 낮을 수록 높은 우선순위를 가진다.

  1. 마지막으로 Virtual DOM(root Fiber Node)에 전체 NativeEvent에 대한 Loop을 돌면서 해당 이벤트에 리액트 이벤트 핸들러를 등록하는 과정을 거친다. 특정 컴포넌트에 "onClick" 이벤트 핸들러를 주었더라도 root DOM Node에 등록된다.
    ( react에서 default root Container는<div id="root"> 이다. )

이 단계들은 앱이 최초로 렌더링 되기 전에 모두 이루어진다.

모든 이벤트를 감시하는 Root Container

React에서 실제 이벤트가 발생했을 때 일어나는 일

button 요소에 이벤트 핸들러를 추가하고 클릭되었을때를 가정해보자

	<button onClick={(e) => { console.log(e) }} > 버튼 </button>
  1. button을 클릭하면 리액트에서 ‘click’ 이벤트를 감지하고, 해당 이벤트 리스너가 트리거 된다. 이때, 이 이벤트 리스너는 리액트에서 정의한 dispatchEvent 함수를 호출하게 된다.

  2. 호출 시 넘어온 이벤트 객체로 부터 event.target (여기서는 button Node)를 식별하여, 내부적으로 사용하는 키인 internalInstanceKey를 이용하여 해당 DOM Node와 매칭되는 Fiber node를 확인한다.

  3. Fiber node가 확인되면, 해당 Fiber node 로부터 출발해서 root node까지 Fiber Tree를 순회하며 마치 이벤트 버블링 처럼 매칭되는 이벤트를 가지고있는 Fiber Node를 발견할때마다 이벤트 리스너가 실행 할 함수들을 DispatchQueue 배열로 저장한다.

type DispatchListener = {|
  instance: null | Fiber,
  listener: Function,
  currentTarget: EventTarget,
|};

type DispatchEntry = {|
  event: ReactSyntheticEvent,
  listeners: Array<DispatchListener>,
|};

export type DispatchQueue = Array<DispatchEntry>;
  1. root 에 도착하고 나면, DispatchQueue를 반복문으로 순회하면서 eventlisteners, inCapturePhase (이벤트 캡쳐링 여부) 를 추출해 processDispatchQueueItemsInOrder 함수를 실행시킨다.
export function processDispatchQueue(
  dispatchQueue: DispatchQueue,
  eventSystemFlags: EventSystemFlags,
): void {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  for (let i = 0; i < dispatchQueue.length; i++) {
    const {event, listeners} = dispatchQueue[i];
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
    //  event system doesn't use pooling.
  }
  // This would be a good time to rethrow if any of the event handlers threw.
  rethrowCaughtError();
}
  1. processDispatchQueueItemsInOrder 함수에서는 fiberNode instance, currentTarget, listener함수를 추출하여 이벤트 캡쳐링 여부에 따라 실행 순서를 역전시키고 propagation 여부를 검사 및 이벤트 중복 여부를 확인한 이후에 executeDispatch 함수를 실행시킨다. 즉 이벤트를 실행한다.
function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  if (inCapturePhase) {
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    for (let i = 0; i < dispatchListeners.length; i++) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}

내용이 너무 어려워서 정리하다가 실수하거나 틀린부분이 있을 수 있는데, 댓글로 알려주시면 감사하겠습니다.

Reference
https://blog.mathpresso.com/react-deep-dive-react-event-system-1-759523d90341
https://gist.github.com/romain-trotard/76313af8170809970daa7ff9d87b0dd5

profile
항상 근거를 찾는 사람이 되자

0개의 댓글