본문 바로가기

리액트 네이티브

리액트 네이티브 터치 이벤트 흐름

리액트 네이티브의 터치 반응 속도가 너무 느려서 원인이 무엇인지 파악하기 위해 내부 코드를 살펴보게 되었고, 이전 포스트 "리액트 네이티브에서 JS와 Java가 통신하는 방법"의 내용을 기반으로 안드로이드에서 발생한 터치 이벤트가 JS로 전송되서 처리되는 과정 을 정리했습니다.

목차

  1. Java에서 JS로 이벤트 전송
    1-1. ReactRootView
    1-2. JSTouchDispatcher
      1-2-1. Leaf View 찾기
      1-2-2. 리액트 네이티브가 생성한 View 찾기
      1-2-3. UIManagerModule로 부터 EventDispatcher 얻어오기
      1-2-4. EventDispatcher
      1-2-5. Proxy 객체
  2. JS에서 Java 이벤트 수신 모듈 등록
    2-1. RCTEventEmitter
    2-2. ReactNativeRenderer
    2-3. 플러그인
       2-3-1. 플러그인 등록
       2-3-2. ReactSyntheticEvent
       2-3-3. ResponderEventPlugin
       2-3-4. ReactNativeBridgeEventPlugin
       2-3-5. ResponderEventPlugin과 ReactNativeBridgeEventPlugin 비교
  3. 느낀점

1. Java에서 JS로 이벤트 전송

1-1. ReactRootView

리액트 네이티브의 모든 뷰는 ReactRootView 객체의 자식으로 화면에 표시됩니다.

 

그리고 ReactRootView는 자식 뷰에서 발생한 모든 터치 이벤트를 가로채서 JS로 보냅니다.

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
  if (shouldDispatchJSTouchEvent(ev)) {
    dispatchJSTouchEvent(ev); // 요기 !!
  }
  dispatchJSPointerEvent(ev, true); // 미사용
  return super.onInterceptTouchEvent(ev);
}

https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java#L249-L256

 

참고 : 안드로이드 터치 시스템 - seong-hwan Kim

protected void dispatchJSTouchEvent(MotionEvent event) {
  if (!hasActiveReactContext() || !isViewAttachedToReactInstance()) {
    return;
  }
  if (mJSTouchDispatcher == null) {
    return;
  }

  // UIManager- 1-2-3에서 설명
  EventDispatcher eventDispatcher =
      UIManagerHelper.getEventDispatcher(getCurrentReactContext(), getUIManagerType());

  // 1-2
  if (eventDispatcher != null) {
    mJSTouchDispatcher.handleTouchEvent(event, eventDispatcher);
  }
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java#L350-L365

1-2. JSTouchDispatcher

mJSTouchDispathcer는 RootView 객체가 그려지고(onMeasure), ReactInstanceManager 객체에 연결되었을 때 생성됩니다.

public void onAttachedToReactInstance() {
  mJSTouchDispatcher = new JSTouchDispatcher(this); // ReactRootView instance
  // ...
}

@Override
public void onStage(@ReactStage int stage) {
  switch (stage) {
    case ReactStage.ON_ATTACH_TO_INSTANCE:
      onAttachedToReactInstance(); // 여기서 쭉쭉 따라가면 JSTouchDispatcher 객체 생성 !!
      break;
    default:
      break;
  }
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java#L604-L617

 

JSTouchDispatcher 객체는 안드로이드에서 전달된 MotionEvent 객체를 가공하여 TouchEvent 객체를 생성하고, 입력받은 eventDispatcher를 사용하여 JS측으로 전달합니다.

public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) {
  int action = ev.getAction() & MotionEvent.ACTION_MASK;
  if (action == MotionEvent.ACTION_DOWN) {
    // ...
    mTargetTag = findTargetTagAndSetCoordinates(ev); // 요기 !!

    eventDispatcher.dispatchEvent(
        TouchEvent.obtain(
            UIManagerHelper.getSurfaceId(mRootViewGroup),
            mTargetTag,
            TouchEventType.START, // "topTouchStart"
            ev,
            mGestureStartTime,
            mTargetCoordinates[0],
            mTargetCoordinates[1],
            mTouchEventCoalescingKeyHelper));
  } else if (mChildIsHandlingNativeGesture) {
    // ...
  } else if (mTargetTag == -1) {
    // ...
  } else if (action == MotionEvent.ACTION_MOVE) {
    // ...
  } else if (action == MotionEvent.ACTION_POINTER_DOWN) {
    // ...
  } else if (action == MotionEvent.ACTION_CANCEL) {
    // ...
  } else {
    // ...
  }
}

private int findTargetTagAndSetCoordinates(MotionEvent ev) {
  return TouchTargetHelper.findTargetTagAndCoordinatesForTouch(
      ev.getX(), ev.getY(), mRootViewGroup, mTargetCoordinates, null);
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java#L93

 

1-2-1. Leaf View 찾기

브라우저와 달리, MotionEvent 객체에는 실제 사용자와 상호작용한 자식 뷰에 대한 정보가 없습니다.

 

그래서 findTargetTagAndSetCoordinates 메서드를 사용하여 뷰 트리에서 사용자가 터치한 좌표에 위치하는 Leaf 뷰를 찾습니다.

public class TouchTargetHelper {
  public static int findTargetTagAndCoordinatesForTouch(
      float eventX,
      float eventY,
      ViewGroup viewGroup,
      float[] viewCoords,
      @Nullable int[] nativeViewTag) {
    int targetTag = viewGroup.getId();

    viewCoords[0] = eventX;
    viewCoords[1] = eventY;

    // 1-2-1. Leaf View 찾기 (DFS 탐색)
    View nativeTargetView = findTouchTargetViewWithPointerEvents(viewCoords, viewGroup, null);

    if (nativeTargetView != null) {
      // 1-2-2. 리액트 네이티브에서 생성한 View 찾기
      View reactTargetView = findClosestReactAncestor(nativeTargetView);
      if (reactTargetView != null) {
        if (nativeViewTag != null) {
          nativeViewTag[0] = reactTargetView.getId();
        }
        // ReactTextView같은 ReactCompoundView에 대한 처리
        targetTag = getTouchTargetForView(reactTargetView, viewCoords[0], viewCoords[1]);
      }
    }
    return targetTag;
  }
}

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java#L81-L103

 

findTouchTargetViewWithPointerEvents는 ReactRootView 객체부터 재귀적으로 DFS 탐색을하여 터치 좌표가 포함된 Leaf 뷰를 찾습니다.

private static @Nullable View findTouchTargetViewWithPointerEvents(
    float eventCoords[], View view, @Nullable List<ViewTarget> pathAccumulator) {
  PointerEvents pointerEvents =
      view instanceof ReactPointerEventsView
          ? ((ReactPointerEventsView) view).getPointerEvents()
          : PointerEvents.AUTO;
  // ...
  if (pointerEvents == PointerEvents.NONE) {
    // ...
  } else if (pointerEvents == PointerEvents.BOX_ONLY) {
    // ...
  } else if (pointerEvents == PointerEvents.BOX_NONE) {
    // ...
  } else {
    // ...
    View result =
        findTouchTargetView( // 요기 !!
            eventCoords,
            view,
            EnumSet.of(TouchTargetReturnType.SELF, TouchTargetReturnType.CHILD),
            pathAccumulator);
    if (result != null && pathAccumulator != null) {
      pathAccumulator.add(new ViewTarget(view.getId(), view));
    }
    return result;
  }
}

private static View findTouchTargetView(
    float[] eventCoords,
    View view,
    EnumSet<TouchTargetReturnType> allowReturnTouchTargetTypes,
    List<ViewTarget> pathAccumulator) {
  // We prefer returning a child, so we check for a child that can handle the touch first
  if (allowReturnTouchTargetTypes.contains(TouchTargetReturnType.CHILD) && view instanceof ViewGroup) {
    // ...
    ViewGroup viewGroup = (ViewGroup) view;

    // 뷰가 터치 좌표 안에 없는 경우 처리 ... 대부분 null 반환
    if (!isTouchPointInView(eventCoords[0], eventCoords[1], view)) {
      // ...
    }

    int childrenCount = viewGroup.getChildCount();

    // Consider z-index when determining the touch target.
    ReactZIndexedViewGroup zIndexedViewGroup =
        viewGroup instanceof ReactZIndexedViewGroup ? (ReactZIndexedViewGroup) viewGroup : null;

    for (int i = childrenCount - 1; i >= 0; i--) {
      int childIndex =
          zIndexedViewGroup != null ? zIndexedViewGroup.getZIndexMappedChildIndex(i) : i;
      View child = viewGroup.getChildAt(childIndex);
      PointF childPoint = mTempPoint;
      getChildPoint(eventCoords[0], eventCoords[1], viewGroup, child, childPoint);
      // The childPoint value will contain the view coordinates relative to the child.
      // We need to store the existing X,Y for the viewGroup away as it is possible this child
      // will not actually be the target and so we restore them if not
      float restoreX = eventCoords[0];
      float restoreY = eventCoords[1];
      eventCoords[0] = childPoint.x;
      eventCoords[1] = childPoint.y;
      
      // 재귀
      View targetView = findTouchTargetViewWithPointerEvents(eventCoords, child, pathAccumulator);
      if (targetView != null) {
        return targetView;
      }
      eventCoords[0] = restoreX;
      eventCoords[1] = restoreY;
    }
  }

  // for문에서 targetView가 반환되지 않았다면 자기 자신 반환
  if (allowReturnTouchTargetTypes.contains(TouchTargetReturnType.SELF)
      && isTouchPointInView(eventCoords[0], eventCoords[1], view)) {
    return view;
  }
  return null;
}

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java#L306-L395

 

즉, 다음과 같이 2번 뷰에 연결된 fiber에 터치 핸들러가 없더라도, 뷰 트리에서 터치 좌표의 Leaf 뷰인 2번 뷰를 반환합니다.

react-native android find leaf view

1-2-2. 리액트 네이티브가 생성한 View 찾기

findClosestReactAncestor는 리액트 네이티브에 의해 생성된 가장 가까운 조상 뷰를 찾습니다. 

 

기본적으론 자기 자신이 될 것이다. 리액트 네이티브에 의해 생성된 뷰인지는 뷰 ID가 있는지 여부로 확인합니다.

 

리액트 네이티브에서 생성한 View는 생성될 때 ID가 설정됩니다.

protected @NonNull T createViewInstance(
    int reactTag,
    @NonNull ThemedReactContext reactContext,
    @Nullable ReactStylesDiffMap initialProps,
    @Nullable StateWrapper stateWrapper) {
  T view = null;

  @Nullable Stack<T> recyclableViews = getRecyclableViewStack(reactContext.getSurfaceId());
  if (recyclableViews != null && !recyclableViews.empty()) {
    view = recycleView(reactContext, recyclableViews.pop());
  } else {
    view = createViewInstance(reactContext);
  }

  view.setId(reactTag);
  // ...
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java#L192

 

reactTag는 호출 순서에 따라 순차적으로 증가하는 숫자입니다.

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/ReactNativeHostConfig.js#L70

 

그리고 findClosestReactAncestor는 ID가 있는(리액트 네이티브에서 생성한) 조상 뷰를 찾는습니다. (대부분 자기 자신)

@SuppressLint("ResourceType")
private static View findClosestReactAncestor(View view) {
  while (view != null && view.getId() <= 0) {
    view = (View) view.getParent();
  }
  return view;
}

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java#L152-L158

1-2-3. UIManagerModule로 부터 EventDispatcher 얻어오기

이렇게 Leaf 뷰의 ID가 포함된 TouchEvent 객체가 eventDispatcher를 통해 JS로 전달됩니다.

protected void dispatchJSTouchEvent(MotionEvent event) {
  if (!hasActiveReactContext() || !isViewAttachedToReactInstance()) {
    return;
  }
  if (mJSTouchDispatcher == null) {
    return;
  }

  // ReactContext 객체로 부터 UIManagerModule을 얻어오고, UIManagerModule로 부터 EventDispatcher를 얻어옵니다.
  EventDispatcher eventDispatcher =
      UIManagerHelper.getEventDispatcher(getCurrentReactContext(), getUIManagerType());

  if (eventDispatcher != null) {
    mJSTouchDispatcher.handleTouchEvent(event, eventDispatcher);
  }
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java#L350-L365

 

eventDispatcher는 UIManagerModule 객체에 포함되어 있습니다.

public UIManagerModule(
    ReactApplicationContext reactContext,
    ViewManagerResolver viewManagerResolver,
    int minTimeLeftInFrameForNonBatchedOperationMs) {
  // ...
  mEventDispatcher = new EventDispatcherImpl(reactContext);
  // ...
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java#L119

 

UIManagerModule은 CoreModulesPackage에 포함되어 ReactInstanceManager 생성시 ReactPackage 리스트에 포함되고, ReactContext 객체를 생성할 때, NativeModuleRegistry 객체에 포함되어 CatalystInstance 객체에 전달됩니다.

 

UIManagerHelper는 ReactContext 객체의 CatalystInstance 객체에 포함된 NativeModuleRegistry 객체를 통해 UIManagerModule을 얻을 수 있습니다.

https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.java#L95

 

UIManagerHelper.getEventDispatcher는 ReactContext 객체로 부터 UIManagerModule 객체을 얻어오고, UIManagerModule 객체로 부터 EventDispatcher 객체를 얻어와 반환합니다.

public class UIManagerHelper {
  public static EventDispatcher getEventDispatcher(
      ReactContext context, @UIManagerType int uiManagerType) {
    UIManager uiManager = getUIManager(context, uiManagerType, false); // from context

    // ...
    EventDispatcher eventDispatcher = (EventDispatcher) uiManager.getEventDispatcher();
    // ...
    return eventDispatcher;
  }
}

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.java#L142

1-2-4. EventDispatcher

EventDispatcherImpl 클래스는 내부적으로 ReactEventEmitter 객체를 사용하여 JS로 이벤트를 전달합니다.

public class EventDispatcherImpl implements EventDispatcher, LifecycleEventListener {
  private volatile ReactEventEmitter mReactEventEmitter;
  // ...
  public EventDispatcherImpl(ReactApplicationContext reactContext) {
    mReactContext = reactContext;
    mReactEventEmitter = new ReactEventEmitter(mReactContext);
  }
}

public void dispatchEvent(Event event) {
  synchronized (mEventsStagingLock) {
    mEventStaging.add(event); // 이벤트를 모아둡니다.
  }

  // mEventStaging에 모다운 이벤트를 UI 스래드 외부에서 mReactEventEmitter를 사용하여 JS로 전송
  maybePostFrameCallbackFromNonUI();
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.java#L111

 

ReactEventEmitter Proxy 프록시 객체의 receiveEvent 메서드를 사용하여 JS 함수 호출합니다.

 

이 메서드 구현은 자바측에 없고, 프록시를 통해 JS 함수를 호출하는 코드로 변환하는데 이 부분은 아래 1-2-5. 프록시 객체에서 설명하도록 하겠습니다.

class ReactEventEmitter implements RCTModernEventEmitter {
  @Override
  public void receiveEvent(
      int surfaceId,
      int targetReactTag,
      String eventName,
      boolean canCoalesceEvent,
      int customCoalesceKey,
      @Nullable WritableMap event,
      @EventCategoryDef int category) {
    @UIManagerType int uiManagerType = ViewUtil.getUIManagerType(targetReactTag, surfaceId);
    if (uiManagerType == UIManagerType.FABRIC && mFabricEventEmitter != null) {
      // ...
      
    } else if (uiManagerType == UIManagerType.DEFAULT) {
      RCTEventEmitter defaultEmitter = getDefaultEventEmitter();
      if (defaultEmitter != null) {
        defaultEmitter.receiveEvent(targetReactTag, eventName, event); // 요기 !! RCTEventEmitter Proxy
      }
    } else {
      // ...
    }
  }

  @Nullable
  private RCTEventEmitter getDefaultEventEmitter() {
    if (mDefaultEventEmitter == null) {
      if (mReactContext.hasActiveReactInstance()) {
        // 요기 !! RCTEventEmitter Proxy 객체를 얻어옵니다.
        mDefaultEventEmitter = mReactContext.getJSModule(RCTEventEmitter.class);
      } else {
        ReactSoftExceptionLogger.logSoftException(
            TAG,
            new ReactNoCrashSoftException(
                "Cannot get RCTEventEmitter from Context, no active Catalyst instance!"));
      }
    }
    return mDefaultEventEmitter;
  }
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/ReactEventEmitter.java#L115

1-2-5. Proxy 객체

ReactContext 객체는 catalystInatance 객체의 getJSModule 모듈에게 동작을 위임합니다.

mJSModuleRegistry = new JavaScriptModuleRegistry();

@Override
public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
  return mJSModuleRegistry.getJavaScriptModule(this, jsInterface);
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java#L443-L445

 

mJSModuleRegistry.getJavaScriptModule 메서드는 입력된 Class 객체의 프록시 객체를 반환합니다.

public synchronized <T extends JavaScriptModule> T getJavaScriptModule(
    CatalystInstance instance, Class<T> moduleInterface) {
  JavaScriptModule module = mModuleInstances.get(moduleInterface);    
  if (module != null) {
    return (T) module;
  }
  JavaScriptModule interfaceProxy =
      (JavaScriptModule)
          Proxy.newProxyInstance(
              moduleInterface.getClassLoader(),
              new Class[] {moduleInterface},
              new JavaScriptModuleInvocationHandler(instance, moduleInterface)); // 요기 !!

  return (T) interfaceProxy;
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java#L38-L45

 

마치 moudleInterface 추상 클래스의 구현 객체를 생성하는 것 같습니다. (참고 : Proxy)

 

프록시 객체의 메서드를 호출하면, JavaScriptModuleInvocationHandler 객체의 invoke 메서드가 해당 호출을 가로챕니다.

 

invoke 메서드에는 프록시 객체의 메서드에 대한 정보가 함께 넘어옵니다.

private static class JavaScriptModuleInvocationHandler implements InvocationHandler {
  private final CatalystInstance mCatalystInstance;
  private final Class<? extends JavaScriptModule> mModuleInterface;
  private @Nullable String mName;
  public JavaScriptModuleInvocationHandler(
      CatalystInstance catalystInstance, Class<? extends JavaScriptModule> moduleInterface) {

  @Override
  public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
      throws Throwable {
    NativeArray jsArgs = args != null ? Arguments.fromJavaArgs(args) : new WritableNativeArray();
    mCatalystInstance.callFunction(getJSModuleName(), method.getName(), jsArgs);
    return null;
  }
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java#L81-L86

 

모듈 이름은 클래스 객체(moduleInterface)로 부터 얻어올 수 있고, 메서드 이름은 Mothod 객체로 부터 얻어올 수 있습니다. 

 

catalystInstance 객체의 callFunction 메서드에 모듈 이름과 메서드 이름을 호출하면, JS측의 해당 모듈의 메서드를 호출할 수 있습니다.

 

참고: 리액트 네이티브에서 JS와 Java가 통신하는 방법 > Java에서 JS까지

 

즉, ReactEventEmmiter 객체의 receiveEvent 메서드 호출은 CatalystInstance 객체에게 JS의 "RCTEventEmitter" 모듈의 "receiveEvent" 메서드 호출을 문자열을 사용하여 요청하는 것과 같습니다.

 

지금까지 흐름을 그림으로 표현해 봤습니다.

react-native android event flow

2. JS에서 Java 이벤트 수신 모듈 등록

2-1. RCTEventEmitter

네이티브측에서 전송된 이벤트를 수신하기 위한 JS 모듈입니다.

 

외부(ReactNativeRenderer)에서 네이티브 측에서 전송된 이벤트를 처리하기 위한 모듈(eventEmitter)을 받아 BatchedBridge에 등록합니다.

const BatchedBridge = require('../BatchedBridge/BatchedBridge');

const RCTEventEmitter = {
  register(eventEmitter: any) {
    BatchedBridge.registerCallableModule('RCTEventEmitter', eventEmitter);
  },
};

module.exports = RCTEventEmitter;

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js#L20

 

RCTEventEmitter는 ReactNativePrivateInterface를 통해 ReactNativeRenderer에 노출됩니다.

module.exports = {
  // ...
  get RCTEventEmitter() {
    return require('./RCTEventEmitter');
  },
  // ...
}

https://github.com/facebook/react-native/blob/7d7f94cc98203162772f4df57560f5ff90aab237/packages/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js#L49-L51

 

리액트 트리를 네이티브에 그리기 위한 ReactNativeRenderer에서 RCTEventEmitter 모듈을 등록합니다.

2-2. ReactNativeRenderer

리액트 트리를 커밋 단계에서 네이티브 뷰에 반영하는 모듈입니다.


참고로 ReactNativeRenderer는 react-native가 아닌, react 프로젝트의 일부입니다.

 

ReactNativeRenderer (react > react-native-renderer)

import './ReactNativeInjection';

// ...

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/ReactNativeRenderer.js#L14

 

ReactNativeInjection (react > react-native-renderer)

import './ReactNativeInjectionShared'; // 2-3-1. 플리그인 등록 
import {receiveEvent, receiveTouches} from './ReactNativeEventEmitter';
 
// Module provided by RN:
import {RCTEventEmitter} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';

RCTEventEmitter.register({
  receiveEvent,
  receiveTouches,
});

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/ReactNativeInjection.js#L28-L31

 

즉, 자바의 ReactEventEmmiter 객체의 receiveEvent 메서드를 호출할 때 이를 수신하는 JS측의 receiveEvent 함수를 여기서 등록합니다. 

 

rootNodeId는 주석엔 이벤트가 발생한 View라고 설명되어 있는데, 사실 터치가 발생한 좌표의 Leaf View의 ID입니다. (리액트 네이티브 내부에서 임의로 할당)

 

리액트 네이티브에서 할달할 때, 대응하는 fiber도 같이 캐시해 놓기 때문에, ID로 fiber를 찾을 수 있습니다.

export function receiveEvent(
  rootNodeID: number,
  topLevelType: TopLevelType,
  nativeEventParam: AnyNativeEvent, // target view ID 포함
) {
  _receiveRootNodeIDEvent(rootNodeID, topLevelType, nativeEventParam);
}

function _receiveRootNodeIDEvent(
  rootNodeID: number,
  topLevelType: TopLevelType,
  nativeEventParam: ?AnyNativeEvent,
) {
  var inst = getInstanceFromTag(rootNodeID); // 요기 !!

  // ... 
  batchedUpdates(function () {
    runExtractedPluginEventsInBatch(topLevelType, inst, nativeEvent, target);
  });
}

function runExtractedPluginEventsInBatch(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
) {
  const events = extractPluginEvents( // 2-3. 플러그인
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  );
  runEventsInBatch(events);
}

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/ReactNativeEventEmitter.js#L175

2-3. 플러그인

각 플러그인마다 nativeEvent로 부터 다수의 ReactSyntheticEvent 객체를 추출합니다.

 

플러그인들이 생성한 모든 ReactSyntheticEvent 객체를 모읍니다. (ReactSyntheticEvent 객체는 2-3-2에서 설명)

import { plugins } from './legacy-events/EventPluginRegistry';

function extractPluginEvents(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {

  let events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null = null;

  const legacyPlugins = plugins;

  for (let i = 0; i < legacyPlugins.length; i++) {
    // Not every plugin in the ordering may be loaded at runtime.
    const possiblePlugin: LegacyPluginModule<AnyNativeEvent> = legacyPlugins[i];
    if (possiblePlugin) {
      const extractedEvents = possiblePlugin.extractEvents(
        topLevelType,
        targetInst,
        nativeEvent,
        nativeEventTarget,
      );
      if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
      }
    }
  }
  return events;
}

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/ReactNativeEventEmitter.js#L129-L134

 

그림으로 표현하면 다음과 같습니다.

react-native touch event

 

2-3-1. 플러그인 등록

플러그인은 어디서 등록되는지, 그리고 왜 한 개의 nativeEvent로 부터 다수의 다른 이벤트 객체가 생성되는지 알아보겠습니다.

 

ReactNativeRender에서 ReactNativeInjecion을 import했었습니다. 

 

그리고 RecatNativeIjection은 ReactNativeInjectionShared를 import합니다.

import './ReactNativeInjectionShared';

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/ReactNativeInjection.js#L10

 

ReactNativeInjectionShared에서 EventPluginRegistry의 plugins에 플러그인을 등록합니다.

import {
  injectEventPluginOrder,
  injectEventPluginsByName,
} from './legacy-events/EventPluginRegistry';

// ['ResponderEventPlugin', 'ReactNativeBridgeEventPlugin']; 
injectEventPluginOrder(ReactNativeEventPluginOrder);

injectEventPluginsByName({
  ResponderEventPlugin: ResponderEventPlugin,
  ReactNativeBridgeEventPlugin: ReactNativeBridgeEventPlugin,
});

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/ReactNativeInjectionShared.js#L20-L40

 

리액트 네이티브에선 ResponderEventPlugin, ReactnativeBridgeEventPlugin 두 개의 플러그인이 사용됩니다. 

 

플러그인은 AnyNativeEvent 객체에서 ReactSyntheticEvent 객체를 추출하는 extractEvents 메서드를 정의해야 합니다. 

2-3-2. ReactSyntheticEvent

플러그인이 반환하는 ReactSyntheticEvent 객체는 시작 fiber(targetFiber)로 부터 추출할 이벤드 핸들러 정보(dispatchConfig)와 추출한 조상 fiber들(_dispatchInstances), 해당 fiber들의 리스너(_dispatchListeners)에 대한 정보를 가지고 있습니다.

export function executeDispatchesInOrder(event) {
  const dispatchListeners = event._dispatchListeners;
  const dispatchInstances = event._dispatchInstances;

  if (isArray(dispatchListeners)) {
    for (let i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }

      const listener = dispatchListeners[i];
      const instance = dispatchInstances[i];
      executeDispatch(event, listener, instance);
    }
  } else if (dispatchListeners) {
    const listener = dispatchListeners;
    const instance = dispatchInstances;
    executeDispatch(event, listener, instance);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/legacy-events/EventPluginUtils.js#L75-L94

 

최종적으로 executeDispatch 함수를 사용하여 리스너에 ReactSyntheticEvent 객체를 넣어 실행시킵니다.

export function executeDispatch(event, listener, inst) {
  event.currentTarget = getNodeFromInstance(inst);
  try {
    listener(event);
  } catch (error) {
    if (!hasError) {
      hasError = true;
      caughtError = error;
    } else {
      // TODO: Make sure this error gets logged somehow.
    }
  }
  event.currentTarget = null;
}

https://github.com/facebook/react/blob/1b7478246d05b030a2ae7a8bb07aea8c7df7ef27/packages/react-native-renderer/src/legacy-events/EventPluginUtils.js#L73

 

사용자가 뷰를 누르면 안드로이드에서 TouchEventType.START("topTouchStart") 이벤트를 JS로 전달합니다.

 

JS에선 ResponderEventPlugin, ReactnativeBridgeEventPlugin 플러그인이 네이티브에서 전달된 이벤트를 각각 다르게 처리합니다. 

 

이 중에서 TouchEventType.START("topTouchStart") 이벤트만 어떻게 처리되는지 알아보겠습니다.

2-3-3. ResponderEventPlugin

Gesture Responder System을 구현합니다.

 

이 중에서 Leaf fiber(targetInst)로 부터 root fiber까지 onStartShouldSetResponder 이벤트 핸들러를 처리하기 위한 ReactSyntheticEvent 객체(shouldSetEvent)를 생성하고, targetInst부터 root까지 올라가면서 onStartShouldSetResponder 이벤트 핸들러를 구현한 fiber를 모옵니다.

 

이렇게 모은 fiber를 순회하면서 onStartShouldSetResponder 이벤트 핸들러를 실행시키고, 가장 먼저 true를 반환한 fiber를 Gesture Responder System의 Responder로 설정하고, 다음 단계로 넘어갑니다.

const eventTypes = {
  startShouldSetResponder: { // 요기 !!
    phasedRegistrationNames: {
      bubbled: 'onStartShouldSetResponder',
      captured: 'onStartShouldSetResponderCapture',
    },
    dependencies: startDependencies,
  },
  // ...
}

const ResponderEventPlugin = {
  extractEvents: function (
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
  ) {
    // ...
    let extracted = canTriggerTransfer(topLevelType, targetInst, nativeEvent)
      ? setResponderAndExtractTransfer(
          topLevelType,
          targetInst,
          nativeEvent,
          nativeEventTarget,
        )
      : null;
    // ...
};

function setResponderAndExtractTransfer(
  topLevelType,
  targetInst,
  nativeEvent,
  nativeEventTarget,
) {
  const shouldSetEventType = isStartish(topLevelType) // === "topTouchStart"
    ? eventTypes.startShouldSetResponder // 요기 !!
    : isMoveish(topLevelType)
      ? eventTypes.moveShouldSetResponder
      : topLevelType === TOP_SELECTION_CHANGE
        ? eventTypes.selectionChangeShouldSetResponder
        : eventTypes.scrollShouldSetResponder;

  const bubbleShouldSetFrom = !responderInst // 대부분 !null -> true
    ? targetInst
    : getLowestCommonAncestor(responderInst, targetInst);

  const skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderInst; // false

  // ReactSyntheticEvent 객체
  const shouldSetEvent = ResponderSyntheticEvent.getPooled(
    shouldSetEventType, // dispatchConfig ← {phaseRegistrationNames: {bubbled: "onStartShouldSetResponder", ...}}
    bubbleShouldSetFrom, // targetInst
    nativeEvent,
    nativeEventTarget,
  );

  if (skipOverBubbleShouldSetFrom) { // false
    accumulateTwoPhaseDispatchesSkipTarget(shouldSetEvent);
  } else {
    // targetInst → root까지 onStartShouldSetResponder 핸들러를 가지고 있는 fiber를 shouldSetEvent.dispatchInstances에 모은다.
    accumulateTwoPhaseDispatches(shouldSetEvent); // 요기 !!!
  }

  // 이름 그대로 shouldSetEvent._dispatchInstances의 fiber를 차례로 순회하면서,
  // onStartShouldSetResponder 핸들러를 실행시키고, 가장 먼저 true를 반환하는 fiber를 반환합니다.
  const wantsResponderInst = executeDispatchesInOrderStopAtTrue(shouldSetEvent);

  if (!shouldSetEvent.isPersistent()) {
    shouldSetEvent.constructor.release(shouldSetEvent);
  }

  if (!wantsResponderInst || wantsResponderInst === responderInst) {
    return null;
  }

  let extracted;

  // wantsResponderInst부터 다음 단계인 onResponderGrant를 위한 이벤트 생성
  const grantEvent = ResponderSyntheticEvent.getPooled(
    eventTypes.responderGrant,
    wantsResponderInst,
    nativeEvent,
    nativeEventTarget,
  );

  // ...
  
  return extracted;
}

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/legacy-events/ResponderEventPlugin.js#L682

 

accumulateTwoPhaseDispatchs 함수(요기 !!!)만 살펴보겠습니다.

 

shouldSetEvent의 bubbleShouldSetFrom(targetInst)부터 root까지 accumulateDirectionalDispatches가 실행되면서, 각 fiber에 event에 할당된 이벤트 핸들러가 있는지 검사하고, 있다면 event._dispatchInstances로 모읍니다.

function accumulateTwoPhaseDispatches(events) {
  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}

// cb는 T뿐만 아니라 T[]까지 처리할 수 있어야 합니다.
function forEachAccumulated<T>(
  arr: ?(Array<T> | T),
  cb: (elem: T) => void,
  scope: ?any,
) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}

function accumulateTwoPhaseDispatchesSingle(event) { // cb
  if (event && event.dispatchConfig.phasedRegistrationNames) {
    traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
  }
}

function traverseTwoPhase(inst, fn, arg) {
  const path = [];

  // inst → root
  while (inst) {
    path.push(inst);
    inst = getParent(inst);
  }

  let i;

  // root → inst까지 accumulateDirectionalDispatches 실행 (capturing)
  for (i = path.length; i-- > 0; ) {
    fn(path[i], 'captured', arg);
  }

  // inst → root까지 accumulateDirectionalDispatches 실행 (bubbling)
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

function accumulateDirectionalDispatches(inst, phase, event) { // fn
  // inst fiber에  event.dispatchConfig.phasedRegistrationNames에 phase 이벤트 핸들러가 있는지 검색
  // topTouchStart의 경우 bubbled → onStartShouldSetResponder 또는 captured → onStartShouldSetResponderCapture
  // inst fiber에 해당 이벤트 핸들러가 없다면 null 반환
  const listener = listenerAtPhase(inst, event, phase); 

  // 이벤트 핸들러가 있다면, 
  if (listener) {
    // event._dispatchListeners에 해당 핸들러 추가
    event._dispatchListeners = accumulateInto(
      event._dispatchListeners,
      listener,
    );
    // event._dispatchInstances에 inst fiber 추가
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); // 요기 !!
  }
}

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/legacy-events/ResponderEventPlugin.js#L339-L341

 

ResponderEventPlugin의 흐름을 그림으로 표현해 봤습니다.

ResponderEventPlugin

2-3-4. ReactNativeBridgeEventPlugin

ResponderEventPlugin의 eventTypes와 달리, 이벤트 타입을 네이티브로 부터 받아옵니다.

 

UIManagerModule에서 getDefaultEventTypes 메서드를 JS로 노출하면,

// UIManagerModule
@ReactMethod(isBlockingSynchronousMethod = true)
public WritableMap getDefaultEventTypes() {
  return Arguments.makeNativeMap(UIManagerModuleConstantsHelper.getDefaultExportableEventTypes());
}

// UIManagerModuleConstantsHelper
public static Map<String, Object> getDefaultExportableEventTypes() {
  return MapBuilder.<String, Object>of(
      // bubblingEventTypes
      BUBBLING_EVENTS_KEY, UIManagerModuleConstants.getBubblingEventTypeConstants(),
      // directEventTypes
      DIRECT_EVENTS_KEY, UIManagerModuleConstants.getDirectEventTypeConstants());
}

// UIManagerModuleConstants
static Map getBubblingEventTypeConstants() {
    return MapBuilder.builder()
        // ...
        .put(
            TouchEventType.getJSEventName(TouchEventType.START),
            MapBuilder.of(
                "phasedRegistrationNames",
                MapBuilder.of("bubbled", "onTouchStart", "captured", "onTouchStartCapture")))
         // ...
        .build();
}

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java#L279-L282

 

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java#L48

 

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java#L26-L30

 

getBubblingEventTypeConstants 함수가 반환하는 데이터를 JSON으로 변환하면 다음과 같습니다.

{
  "bubblingEventTypes": {
    "topTouchStart": {
      "phasedRegistrationNames": {
        "bubbled": "onTouchStart",
        "captured": "onTouchStartCapture"
      },
      // ...
    }
  },
  "directEventTypes": {
    // ...
  }
}

참고: https://reactnative.dev/docs/native-modules-android#argument-types

 

이렇게 네이티브에서 노출한 getDefaultEventTypes은 JS측 어떤 부분에서 가져오냐 하면, 리액트 네이티브에서 View 컴포넌트를 초기화할 때,

const View: React.AbstractComponent<
  ViewProps,
  React.ElementRef<typeof ViewNativeComponent>,
> = React.forwardRef(
  (
    {
      // ...
    }: ViewProps,
    forwardedRef,
  ) => {
    const actualView = (
      <ViewNativeComponent
        // ...
      />
    );

    return actualView;
  },
);

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/Libraries/Components/View/View.js#L98

 

ViewNativeComponent를 NativeComponentRegistry로 부터 가져오는데,

const ViewNativeComponent: HostComponent<Props> =
  NativeComponentRegistry.get<Props>('RCTView', () => __INTERNAL_VIEW_CONFIG);

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/Libraries/Components/View/ViewNativeComponent.js#L109

 

NativeComponentRegistry의 get 메서드에서 getNativeComponentAttributes 함수를 통해(attachDefaultEventTypes), 네이티브에서 노출하는 UIManagerModule의 getDefaultEventTypes 메서드를 호출하여 반환된 이벤트 타입이 포함된 정보를 ReactNativeViewConfigRegistry에 등록합니다.

export function get<Config>(
  name: string,
  viewConfigProvider: () => PartialViewConfig,
): HostComponent<Config> {
  ReactNativeViewConfigRegistry.register(name, () => {
    const {native, strict, verify} = getRuntimeConfig?.(name) ?? {
      native: !global.RN$Bridgeless,
      strict: false,
      verify: false,
    };

    let viewConfig: ViewConfig;

    if (native) { // true
      viewConfig =
        getNativeComponentAttributes(name) ??
        createViewConfig(viewConfigProvider());
    } else {
      viewConfig =
        createViewConfig(viewConfigProvider()) ??
        getNativeComponentAttributes(name);
    }

    // ...

    return viewConfig;
  });
  // $FlowFixMe[incompatible-return] `NativeComponent` is actually string!
  return name;
}

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/Libraries/NativeComponent/NativeComponentRegistry.js#L66

ViewNativeComponent는 사실 문자열인데 꺽쇄 형태의 엘리먼트로 사용되면 @react-native/babel-preset 바벨 플러그인에 의해 ReactNativeViewConfigRegistry에서 name으로 컴포넌트 정보를 가져오고, 해당 컴포넌트를 엘리먼트로 변환시키는 것으로 추정됩니다.

 

최종적으로 JS의 ReactNativeViewConfigRegistry에 네이티브의 UIManagerModule의 getDefaultEventTypes 메서드가 반환한 정보가 포함됩니다.

 

 react-native-renderer의 ReactNativeBridgeEventPlugin은 ReactNativeViewConfigRegistry에서 bubblingEventTypes를 가져옵니다.

const {customBubblingEventTypes, customDirectEventTypes} = ReactNativeViewConfigRegistry;

https://github.com/facebook/react/blob/v18.3.1/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js#L23-L26

 

customBubblingEventTypes는 bubblingEventTypes와 동일합니다.

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js#L55-L56

 

ResponderEventPlugin과 마찬가지로 네이티브로 부터 topTouchStart가 전달된 경우 Leaf fiber로 부터 root fiber까지 onTouchStart를 추출하는 ReactSyntheticEvent 객체가 생성되어 반환됩니다.

const ReactNativeBridgeEventPlugin = {
  eventTypes: ({}: EventTypes),
  extractEvents: function (
    topLevelType: TopLevelType,
    targetInst: null | Object,
    nativeEvent: AnyNativeEvent,
    nativeEventTarget: null | Object,
  ): ?Object {
    if (targetInst == null) {
      // Probably a node belonging to another renderer's tree.
      return null;
    }

    // { topTouchStart: { phasedRegistrationNames: { bubbled: onTouchStart, ... } } }
    const bubbleDispatchConfig = customBubblingEventTypes[topLevelType]; // 요기 !!
    const directDispatchConfig = customDirectEventTypes[topLevelType];

    const event = SyntheticEvent.getPooled(
      bubbleDispatchConfig || directDispatchConfig,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );

    if (bubbleDispatchConfig) {
      const skipBubbling =
        event != null &&
        event.dispatchConfig.phasedRegistrationNames != null &&
        event.dispatchConfig.phasedRegistrationNames.skipBubbling;
      if (skipBubbling) {
        accumulateCapturePhaseDispatches(event);
      } else {
        accumulateTwoPhaseDispatches(event); // 요기 !! ResponderEventPlugin에서 사용한 함수와 동일한 함수
      }
    } else if (directDispatchConfig) {
      accumulateDirectDispatches(event);
    } else {
      return null;
    }
    return event;
  },
};

https://github.com/facebook/react/blob/main/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js#L171-L214

 

ReactNativeBridgeEventPlugin의 흐름을 그림으로 표현해 봤습니다.

ReactNativeBridgeEventPlugin

2-3-5. ResponderEventPlugin과 ReactNativeBridgeEventPlugin 비교

Touch 이벤트는 무조건 root fiber까지 탐색합니다.

 

Touch와 Gesture가 서로 다른 플러그인에 의해 처리되기 때문에, Touch 이벤트는 Leaf fiber부터 root fiber까지 중단 없이 확인하는 반면, Gesture 이벤트는 Responder가 확정되면 탐색을 중간에 중단합니다.

 

다음과 같이 구성된 상태에서 3번 뷰를 터치하면,

function App(): React.JSX.Element {
  return (
    <View nativeID="1" style={{flex: 1}}>
      <View
        nativeID="2"
        style={{width: 200, height: 200, backgroundColor: 'blue'}}
        onTouchStart={() => {
          console.log('onTouchStart from 2');
        }}
        onStartShouldSetResponder={() => {
          console.log('set responder by 2');
          return true;
        }}>
        <View
          nativeID="3"
          style={{width: 100, height: 100, backgroundColor: 'red'}}
          onTouchStart={() => {
            console.log('onTouchStart from 3');
          }}
          onStartShouldSetResponder={() => {
            console.log('set responder by 3');
            return true;
          }}></View>
      </View>
    </View>
  );
}

 

다음과 같이 출력됩니다.

set responder by 3
onTouchStart from 3
onTouchStart from 2

 

Touch 이벤트는 버블링되지만, Gesture 이벤트는 3번 뷰에서 Responder가 확정되어 버블링이 중단됐습니다.

 

그리고 Pressable의 onPress가 버블링되지 않는 이유는 Gesture Responder System을 기반으로 하기 때문입니다.

3. 느낀점

react-dom의 이벤트 델리게이션을 비슷하게 구현해 놓은 것 같습니다.

 

react-dom의 플러그인을 통한 이벤트 처리, Leaf fiber부터 root까지 올라가면서 이벤트를 처리할 fiber 검색하고 listener 수집, 수집한 listener 리스트 한 번에 처리 등..

 

에뮬레이터에서 마우스로 버튼을 클릭하면 topTouchStarttopTouchEnd 두 개만 전달됩니다.

 

하지만 손가락으로 터치하면 다릅니다.

 

다음 위치에 아래 코드를 추가해서 터치해 보면,
https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/Libraries/Renderer/implementations/ReactNativeRenderer-dev.js#L2526

console.log("topLevelType:", topLevelType, targetInst.stateNode._nativeTag)

 

다음과 같은 결과가 출력됩니다.

topLevelType: topTouchStart 39
topLevelType: topTouchMove 39
topLevelType: topTouchMove 39
topLevelType: topTouchMove 39
topLevelType: topTouchMove 39
topLevelType: topTouchMove 39
topLevelType: topTouchEnd 39

 

topTouchMove 이벤트 갯수는 누르는 정도에 따라 다르지만 3-7개 정도가 전달되는 것 같습니다.

 

각 이벤트마다 2개 의 플러그인이 Leaf fiber부터 최대 root까지 타고 올라가면서 각 fiber에서 이벤트를 처리할 이벤트 핸들러가 있는지 확인합니다.

 

그리고 플러그인에서 핸들러를 모두 모아서 번에 처리합니다.

 

터치 번에 5-10 정도의 이벤트가 발생하고 트리 깊이만큼의 복잡도가 발생하지만, 이것이 큰 부하라고 생각하지는 않습니다.

 

다음과 같은 nested 크기만큼 View로 래핑하는 재귀 컴포넌트로 버튼을 래핑했을 ,

const NestedView = ({children, nested = 300}: any) => {
  if (nested > 0) {
    return (
      <View>
        <NestedView nested={nested - 1}>{children}</NestedView>
      </View>
    );
  }
  return <View>{children}</View>;
};

<NestedView nested={0}>
  <Pressable onPress={() => setTab(index)}>
    <Text style={{color: 'black'}}>{`탭${index}`}</Text>
  </Pressable>
</NestedView>

 

nested 값에 따른 preformance.now 추출하고 1000 얻은 마이크로 단위의 이벤트 추출 시간 다음과 같았습니다.

측정 위치
https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/Libraries/Renderer/implementations/ReactNativeRenderer-dev.js#L2524-L2544

 

모드이고, 프로덕션에선 1/3 정도라고 생각하면 됩니다.

 

nested: 0 → 2097 ( 2.1ms)

 LOG  events extracted topTouchStart 922.1349954605103
 LOG  events extracted topTouchMove 311.4579916000366
 LOG  events extracted topTouchMove 357.49998688697815
 LOG  events extracted topTouchMove 298.5930144786835
 LOG  events extracted topTouchEnd 209.21799540519714

 

nested: 20 → 2778 ( 2.8ms)

 LOG  events extracted topTouchStart 1185.9900057315826
 LOG  events extracted topTouchMove 441.4060115814209
 LOG  events extracted topTouchMove 389.7389769554138
 LOG  events extracted topTouchMove 522.9170024394989
 LOG  events extracted topTouchEnd 241.457998752594

 

nested: 300 → 10258 ( 10ms)

 LOG  events extracted topTouchStart 2870.5729842185974
 LOG  events extracted topTouchMove 2078.8539946079254
 LOG  events extracted topTouchMove 2085.417002439499
 LOG  events extracted topTouchMove 1993.7500059604645
 LOG  events extracted topTouchEnd 1232.9689860343933

 

실제 제품에서 트리 깊이가 300까지 되지도 않을 것이고 , 많아봐야 20정도일 텐데, 이마저도 프로덕션에선 반응하는데 1ms도 걸리지 않을 것으로 예상됩니다.

 

터치 시스템에서 속도 저하를 발생시킬 만한 원인을 찾지 못했습니다.

그렇다면 리액트 네이티브에서 반응 속도 저하와 버벅거림을 발생시키는 원인이 무엇인지 다음 포스트에서 알아보겠습니다.