Handling Events

김윤진·2022년 6월 28일
0

React

목록 보기
4/13
post-custom-banner

bind 를 사용해야하는 이유는 무엇일까요?

JavaScript에서 클래스 메서드는 기본적으로 바인딩 되어 있지 않다.

class 필드 내부 메서드에 바인딩하지 않는다면 내부 메서드를 호출할 때 this는 undefined가 된다. 이것은 React만의 특수 동작이 아니라 JavaScript에서의 함수가 작동하는 방식이다.

매번 사용하는 bind를 생략하는 방법이 있을까요?

  • 화살표 함수를 사용하는 방법
class SomeComponent extends React.Component {
	handleClick = () => {}

	render() {
		return(
			<button onClick={this.handleClick()}>
        Click me
      </button>
		)
	}
}
  • 콜백에 화살표 함수를 사용하는 방법
class SomeComponent extends React.Component {
	handleClick() {}

	render() {
		return(
			<button onClick={() => this.handleClick()}>
        Click me
      </button>
		)
	}
}

이 문법은 button element가 속해 있는 컴포넌트가 렌더링될 때마다 다른 콜백이 생성되는 문제가 있다. 이 콜백이 하위 컴포넌트의 props로 전달된다면 그 컴포넌트들은 추가로 다시 렌더링을 수행할 수 있다. 그래서 생성자 안에서 바인당한다.

JSX 내부에 이벤트 핸들러로 콜백 함수를 전달하는 것과 함수를 전달해서 사용하는 것은 어떤 차이가 있을까요?

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

두 경우 모두 React 이벤트를 나타내는 e 인자가 id 뒤에 두번째 인자로 전달된다.

화살표 함수를 사용하면 명시적으로 인자를 전달해야 하지만 bind를 사용하면 추가 인자가 자동으로 전달된다.

**Synthetic Event**

React는 크로스 브라우징을 위해 NativeEvent를 그대로 사용하는 것이 아니라 SyntheticEvent 객체를 이용해서 NativeEvent를 감싸는 방식으로 사용한다.

click, change 등 NativeEvent들의 목록은 React 코드 안에 저장하고 있다.

React에서 제공하는 onChange, onClick 등으로 이벤트 핸들러 property를 매핑합니다.

Discrete Event, UserBlocking Event, Continuous Event등 React에서 정의한 이벤트 타입에 따라 React만의 기준으로 우선 순위를 설정합니다.

React가 처리하는 이벤트 핸들러를 root DOM node에 붙이는 과정은 Virtual DOM(Fiber tree)이 생성되는 시점에 일어난다. Virtural DOM을 root DOM node에 _reactRootContainer 라는 key로 저장되고 생성될 때 리액트 이벤트 핸들러들은 root DOM node에 attach된다.

이 단계는 앱이 최초에 렌더링 되기전에 모두 이루어지므로 user action이 발생하는 시점에는 root DOM node에 모든 이벤트 핸들러가 등록되어 있는 상태입니다.

유저가 버튼을 클릭했을 때 실제 React 이벤트 시스템의 흐름을 살펴보자.

<div id="test-wrapper" onClick={handleClickDiv}>
    <button id="test" onClick={handleClickButton}>
      Try Me!
    </button>
</div>

Button을 클릭하면 React는 click 이벤트를 감지하고 attach한 이벤트 리스너가 트리거된다.

이 때 이벤트 리스넌는 리액트에서 정의한 dispatchEvent 함수를 호출하게 된다.

export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): void {
  if (!_enabled) {
    return;
  }
  let allowReplay = true;
  if (enableEagerRootListeners) {
    allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
  }
  if (
    allowReplay &&
    hasQueuedDiscreteEvents() &&
    isReplayableDiscreteEvent(domEventName)
  ) {
    queueDiscreteEvent(
      null, // Flags that we're not actually blocked on anything as far as we know.
      domEventName,
      eventSystemFlags,
      targetContainer,
      nativeEvent,
    );
    return;
  }

호출시 넘어온 이벤트 객체로부터 target DOM node(Button node)를 식별하며 내부적으로 사용하는 키인 internalInstanceKey 를 사용하여 이 DOM node가 어떤 Fiber node instance와 매칭되는지 확인합니다.

export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
  let targetInst = (targetNode: any)[internalInstanceKey];
  if (targetInst) {
    return targetInst;
  }
  let parentNode = targetNode.parentNode;
  ... for simplification, i've omitted below
}

해당 Fiber node instance를 찾고 나면 해당 node로부터 출발하여 root node까지 Fiber Tree를 순회합니다. 이 때 매칭되는 이벤트 property(onClick)와 매칭되는 이벤트를 가지는 Fiber Node(여기서는 onClick이 바인딩된 div node)를 발견할 때마다 이 이벤트 리스너들을 dispatchQueue 라고 불리는 Array에 저장합니다.

root node에 도착하고 나면 처음 들어간 순서대로 리스너 함수가 실행됩니다. Button의 이벤트 리스너가 먼저 실행되고 div의 이벤트 리스너가 나중에 실행됩니다. 이벤트 리스너로부터 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;
    }
  }
}

function executeDispatch(
  event: ReactSyntheticEvent,
  listener: Function,
  currentTarget: EventTarget,
): void {
  const type = event.type || 'unknown-event';
  event.currentTarget = currentTarget;
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  event.currentTarget = null;
}

**Event Delegation**

React는 17버전부터는 root element에 이벤트들이 위임되고 있어 더욱 안전하게 이벤트를 사요할수 있다. (그 전에는 document에 이벤트들이 위임되었다.)

이는 React가 강조하는 React는 라이브러리 라는 것을 확실시 하는 것이 아닐까?

라이브러리는 가져가 쓰는 느낌이라면 프레임워크는 안에 종속되는 느낌이다. 그래서 전체 앱에 영향을 미치지 않도록 root element 내부에서만 영향을 끼칠 수 있도록 만든 것이 아닐까?

그리고 규모가 큰 앱에서는 React의 새로운 버전이 나오더라도 한번에 변경 사항을 반영할 수 없기에 부분 부분 변경해나간다. 그래서 하나의 애플리케이션에 서로 다른 리액트 버전이 존재하는 경우에는 이 두 버전 모두 Document Level에서 이벤트 리스너가 부착되기에 버그가 생기기 쉬운 환경이 되는 것이다. root DOM Container에 이벤트 리스너가 부착되니 root DOM Container 하위 요소들에게만 적용이 된다. 이것은 새로운 버전이 나오더라도 점진적으로 변경할 수 있다는 것이다.

**Event Pooling**

React 17버전부터는 더이상 Event Pooling이 적용되지 않는다.

일단 Event Pooling에 대해 알아보면

Synthetic Event에서 설명했듯이 React는 Native Event를 한번 매핑해서 사용한다.

Native Event에서 매핑한 인스턴스를 사용하는 것은 이벤트가 발생할 때마다 인스턴스가 생성되는데 이 인스턴스를 저장하기 위한 메모리가 할당된다. 그리고 이벤트가 처리된 후에는 가비지 컬렉터에 의해 이 메모리를 해제해주는 작업도 필요하다.

Event Pooling은 이벤트가 발생할 때마다 Synthetic Event 객체를 생성, 제거하지 말고 Synthetic Event Pool을 만들어 이벤트가 발생할 때마다 Pool을 사용하면 어떨까하는 아이디어에서 착안하여 나온 개념이다.

Event Pool은 다음과 같은 방식으로 동작한다.

  1. Event Pool에 Synthetic Event Instance를 구비한다.
  2. 이벤트 트리거되면 해당 이벤트의 Native Event 는 Event Pool의 instance를 사용해서 Synthetic Event로 매핑한다.
  3. 이벤트 핸들러가 종료되어 콜 스택에서 빠져나오면 해당 인스턴스는 초기화되어 다시 Pool로 돌아간다.

이 방식은 메모리도 아끼고 가비지 컬렉터도 자주 동작할 필요가 없어 효율적인 시스템 같아 보인다. 하지만 이벤트가 트러거 되었을 때 인스턴스 풀을 받아오고 이벤트 핸들러가 종료되자마자 다시 인스턴스를 release하는 방식은 필연적으로 비동기 이벤트에 대한 추가적인 대응(e.persist())가 필요하다. 따라서 아무런 대응없이 동기 이벤트와 동일한 방식으로 비동기 이벤트 콜백을 작성하면 문제가 발생하고 이는 직관적이지 않은 개발 경험을 주게 된다.

성능이 좋아진 모던 브라우저에서는 이런 이벤트 폴링이 유의미한 성능 개선을 보여주지 않는다는 이유로 17버전부터는 이벤트 폴링이 사용되지 않는다. 따라서 17버전부터는 이벤트 핸들러가 동기 함수인지 비동기 함수인지를 신경쓰지 않아도 괜찮다.

post-custom-banner

0개의 댓글