본문 바로가기

리액트 네이티브

리액트 네이티브에서 JS와 Java가 통신하는 방법

리액트 네이티브의 터치 반응 속도가 너무 느려서 원인이 무엇인지 파악하기 위해 내부 코드를 살펴보게 되었습니다.

먼저 전체적인 흐름을 그림으로 표현해 봤습니다.

react native js-java flow

파란색: JS → Java

빨간색: Java → JS

 

목차

  1. JS → Java
    1-1. Java에서 C++로 Module Registry 전달
    1-2. C++에서 JS에 네이티브 호출 함수 등록
    1-3. JS에서 C++의 Module Registry까지 요청
  2. Java → JS
    2-1. C++에서 JS 함수 가져오기
    2-2. C++에서 Java와 JS 함수 연결
    2-3. Java에서 JS까지
  3. 느낀점

리액트 네이티브는 인터페이스(C++), 자바스크립트 엔진, JVM 3개 환경이 유기적으로 상호작용하면서 동작합니다.

JVM → C++

JNI를 사용하여 C++에서 JVM 클래스의 메서드를 구현하고, JVM 객체에선 C++에 의해 구현된 메서드를 호출하여 초기화 또는 필요한 기능을 수행합니다.

 

아래 포스트에서 JVM과 C++ 런타임 통신을 테스트해 볼 수 있습니다.

C++ → JS

JSI를 사용하여 C++ 환경에서 자바스크립트 런타임의 globalC++ 함수를 추가하거나, C++에서 global의 특정 필드를 얻어올 수 있습니다.

 

JSI는 리액트 네이티브만을 위한 것은 아닙니다.

JSC, Hermes, V8 같은 자바스크립트 런타임 엔진은 고성능으로 작동해야 하기 때문에, C, C++작성됩니다.

 

엔진 개발자는 자바스크립트에서 엔진의 기능을 활용할 수 있도록 하기 위해, 자바스크립트 런타임에 C++ 함수과 통신할 수 있는 API를 만들었고, 이것을 JSI(JavaScript Interface)라고 합니다.

 

좀 더 상세한 설명은 다음 링크에서 확인할 수 있습니다.

그리고 2019년 리액트 네이티브 EU 회의에서 JSI에 대해 소개하는 영상입니다. 

JVM과 자바스크립트 엔진간에 자유롭게 서로의 함수를 호출할 수 있는 것은 아닙니다.

상대 환경의 Module Registry(JVM: mNativeModuleRegistry, JS: _lazyCallableModules) 객체에 등록된 모듈(객체)의 메서드만 호출할 수 있습니다.

JSON (역)직렬화 부분은 거의 삭제된 것으로 보입니다.

0.72.0 기준으로 Old Architecture에서 모듈의 메서드를 호출하고 결과를 받을 땐 JSI를 사용하고, 데이터를 문자열로 변환하고 파싱하는 코드를 보지 못했습니다.

 

아래 설명하곘지만, 브릿지는 여전히 사용되는데,  JS에서 JVM으로 요청할 때 5ms 단위로 요청을 모았다가 한 번에 요청을 수행하는 것을 제외하면, 호출 자체는 직접 참조로 이루어집니다.

 

이제 본격적으로 코드를 탐색해 보도록 하겠습니다.

1. JS →Java

1-1 Java에서 C++로 Module Registry 전달

JSI 자바스크립트 런타임에 JAVA에서 노출하는 모듈의 메서드를 호출하는 코드를 등록하기 전에, JNI자바에서 노출하는 Module Registry 얻어와야 합니다.

 

리액트 네이티브 안드로이드 프로젝트는 native_modules.gradle을 이용하여 하여 node_modules에 있는 라이브러리의 gradle 설정을 통합합니다.

 

이때 react-native/ReactAndroid/build.gradle.kts도 통합됩니다.

android {
  // ...
  defaultConfig {
    // ...
    externalNativeBuild {
      cmake {
        arguments(
            "-DREACT_COMMON_DIR=${reactNativeRootDir}/ReactCommon",
            "-DREACT_ANDROID_DIR=$projectDir",
            "-DREACT_BUILD_DIR=$buildDir",
            "-DANDROID_STL=c++_shared",
            "-DANDROID_TOOLCHAIN=clang",
            // Due to https://github.com/android/ndk/issues/1693 we're losing Android
            // specific compilation flags. This can be removed once we moved to NDK 25/26
            "-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=ON")
        targets(
            // ...
            "reactnativejni", // 요기 !!
            // ...
            "reactnative")
      }
    }
    ndk { abiFilters.addAll(reactNativeArchitectures()) }
  }

  externalNativeBuild {
    cmake {
      version = cmakeVersion
      path("src/main/jni/CMakeLists.txt") // 요기 !!
    }
  }
}

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/build.gradle.kts#L597

 

CMakeLists.txtCMake 빌드 시스템의 설정 파일이며, C++을 빌드하기 위해 사용합니다.

 

CMakeLists.txt 파일에 add_library 함수를 지정하여 C++ 파일들을 컴파일하여 공유 라이브러리(so) 를 생성하고, JVAM에서 해당 C++ 라이브러리에 접근하기 위한 "reactnativejni" 같은 ID를 지정할 수 있습니다.

 

그리고 CMakeLists.txt는 다른 CMakeLists.txt를 불러올 수 있습니다.

 

src/main/jni/CMakeLists.txt에 reactnative 라이브러리가 직접 포함되어 있는 것은 아니지만, jni/react/jni 하위 경로의 CMakeLists.txt를 불러오고 해당 파일에서 reactnativejni 라이브러리를 빌드하고 노출합니다.

 

packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt

project(ReactAndroid)

# ...

# ReactAndroid JNI targets
# ...

# build.gradle.kts 파일 경로 기준
add_react_android_subdir(src/main/jni/react/jni) # 해당 경로의 CMakeLists.txt 포함시킵니다.

# ...

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt

 

reactnativejni 동적 라이브러리는 jni/react/jni 경로의 CMakeLists.txt 파일에서 생성합니다.

 

packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt

# CMakeLists.txt 경로의 모든 cpp 파일
file(GLOB reactnativejni_SRC CONFIGURE_DEPENDS *.cpp)

add_library( # CMakeLists.txt 경로의 모든 cpp 파일을 컴파일 대상에 포함시킨다.
        reactnativejni
        SHARED
        ${reactnativejni_SRC}
)

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt

 

해당 경로에 JNI_OnLoad 함수를 정의한 OnLoad.cpp 파일이 포함되어 있고, 해당 공유 라이브러리를 로드하면 JNI_OnLoad 함수가 가장 먼저 실행됩니다. (규칙)

 

여기서 C++에서 자바의 CatalystInstanceImpl 클래스에 메서드를 정의합니다.

 

참고: JNI 실습 (C++에서 자바 클래스에 메서드 구현)

 

위의 그림에서 자바측의 시작점인 CatalystInstance 객체에서 reactnativejni C++ 공유 라이브러리를 로드합니다.

 

참고로 CatalystInstance 클래스는 안드로이드에서 자바스크립트 엔진에서 실행되고 있는 리액트 앱과 상호작용하기 위한 클래스로서 자바스크립트 번들 파일을 실행하고, 특정 모듈의 메서드를 요청받아 호출하는 기능을 포함하고 있습니다.

 

리액트 앱이 실행될 때, 쭉쭉 따라가다 보면 ReactInstanceManager → ReactContext 객체를 만들 때 같이 생성되는데, 이 부분은 설명을 생략하겠습니다.

 

packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp

// ...
#include "CatalystInstanceImpl.h"
// ...

namespace facebook::react {
  extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {

    #ifdef WITH_XPLATINIT
      return facebook::xplat::initialize(vm, [] {
    #else
      return jni::initialize(vm, [] {
    #endif
      // 자바의 CatalystInstanceImpl 클래스에 initializeBridge, jniCallJSFunction 메서드 등 구현
      CatalystInstanceImpl::registerNatives(); // 요기 !!
      // ...
    });
  }
} // namespace facebook::react

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp#L82

 

packages/react-native/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp

void CatalystInstanceImpl::registerNatives() {
  registerHybrid({
      // ...
      makeNativeMethod(
          "initializeBridge", CatalystInstanceImpl::initializeBridge),
      // ...
      makeNativeMethod(
          "jniCallJSFunction", CatalystInstanceImpl::jniCallJSFunction),
      makeNativeMethod(
          "jniCallJSCallback", CatalystInstanceImpl::jniCallJSCallback),
      // ...
  });
}

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L91

 

그리고 JVM에서 CatalystInstace 객체를 생성할 때 C++에서 구현된 initialBridge를 실행합니다.

private native void initializeBridge(/* ... */); // ← C++에서 구현

private CatalystInstanceImpl(
    final ReactQueueConfigurationSpec reactQueueConfigurationSpec,
    final JavaScriptExecutor jsExecutor,
    final NativeModuleRegistry nativeModuleRegistry,
    final JSBundleLoader jsBundleLoader,
    JSExceptionHandler jSExceptionHandler,
    @Nullable ReactInstanceManagerInspectorTarget inspectorTarget) {
  // ...  

  mHybridData = initHybrid();

  // ...

  initializeBridge(
      new InstanceCallback(this),
      jsExecutor, // 요기 !!
      mReactQueueConfiguration.getJSQueueThread(),
      mNativeModulesQueueThread,
      mNativeModuleRegistry.getJavaModules(this), // 요기 !!
      mNativeModuleRegistry.getCxxModules(),
      mInspectorTarget);
  // ...  
}

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java#L143-L149

 

jsExecutor에는 기본적으로 HermesExecutor 객체가 입력되며 C++hermes 공유 라이브러리를 작동시키는 것 말고 특별한 기능은 없는 객체입니다.

public class HermesExecutor extends JavaScriptExecutor {
  static {
    loadLibrary();
  }

  public static void loadLibrary() throws UnsatisfiedLinkError {
    if (mode_ == null) {
      // libhermes must be loaded explicitly to invoke its JNI_OnLoad.
      SoLoader.loadLibrary("hermes");
      SoLoader.loadLibrary("hermes_executor");
      // libhermes_executor is built differently for Debug & Release so we load the proper mode.
      mode_ = ReactBuildConfig.DEBUG ? "Debug" : "Release";
    }
  }
}

https://github.com/facebook/react-native/blob/v0.74.4/packages/react-native/ReactAndroid/src/main/java/com/facebook/hermes/reactexecutor/HermesExecutor.java#L23-L27

 

단지 C++에서 호출되는 initializeBridge 함수에서 해당 객체 타입으로 어떤 자바스크립트 엔진을 실행할지 결정할 뿐입니다.

 

자바스크립트 런타임에 대한 포인터는 아래 설명할 C++ 환경의 JSIExecutor 객체가 가지고 있습니다.

 

mNativeModuleRegistryCatalystInstance 객체를 생성하기 전 빌더를 만들 때 mPackages에서 모듈을 추출하여 입력됩니다.

private ReactApplicationContext createReactContext(
  JavaScriptExecutor jsExecutor, JSBundleLoader jsBundleLoader) {
  // ...
  NativeModuleRegistry nativeModuleRegistry = processPackages(reactContext, mPackages /* 요기 !! */);

  CatalystInstanceImpl.Builder catalystInstanceBuilder =
      new CatalystInstanceImpl.Builder()
          .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault())
          .setJSExecutor(jsExecutor)
          .setRegistry(nativeModuleRegistry)
          .setJSBundleLoader(jsBundleLoader)
          .setJSExceptionHandler(exceptionHandler)
          .setInspectorTarget(getOrCreateInspectorTarget());
}

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java#L1350-L1356

 

mPacakges에는 UIManagerModule 등 리액트 네이티브 코어 모듈이 포함된 CoreModulesPackage와 native_modules.gradle로 수집한 네이티브 패키지들이 포함되어 있습니다.

 

다시 돌아와서 JVM에서 initializeBridge를 호출하면 C++initializeBridge가 호출됩니다.

void CatalystInstanceImpl::initializeBridge(
    jni::alias_ref<JInstanceCallback::javaobject> callback,
    // This executor is actually a factory holder. (HermesExecutorFactoryHolder)
    JavaScriptExecutorHolder* jseh, // 요기 !!
    jni::alias_ref<JavaMessageQueueThread::javaobject> jsQueue,
    jni::alias_ref<JavaMessageQueueThread::javaobject> nativeModulesQueue,
    jni::alias_ref<jni::JCollection<JavaModuleWrapper::javaobject>::javaobject> javaModules,
    jni::alias_ref<jni::JCollection<ModuleHolder::javaobject>::javaobject> cxxModules,
    jni::alias_ref<ReactInstanceManagerInspectorTarget::javaobject> inspectorTarget) {

  moduleMessageQueue_ =
      std::make_shared<JMessageQueueThread>(nativeModulesQueue);

  // ModuleRegistry 객체 !!
  moduleRegistry_ = std::make_shared<ModuleRegistry>(buildNativeModuleList(
      std::weak_ptr<Instance>(instance_),
      javaModules,
      cxxModules,
      moduleMessageQueue_));

  instance_->initializeBridge(
      std::make_unique<InstanceCallbackImpl>(callback),
      jseh->getExecutorFactory(), // HermesExecutorFactory 객체 생성
      std::make_unique<JMessageQueueThread>(jsQueue),
      moduleRegistry_,
      inspectorTarget != nullptr
          ? inspectorTarget->cthis()->getInspectorTarget()
          : nullptr);
}

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L151

그런데 C++의 initializeBridge 구현체를 보면 HermesExecutor 객체가 C++의 JavaScriptExecutorHolder 객체로 변환됩니다.

더 정확히는 자바의 HermesExecutor 객체와 대응하는 C++의 HermesExecutorFactoryHolder 객체입니다.

JNI에서 자바 객체가 C++ 객체로 자동 변환되는 기능은 없습니다.

추가적인 과정이 필요한데, fbjni의 HybridClass가 이것을 처리하는 것으로 추정됩니다.

위에서 표시하진 않았지만, C++의 CatalystInstanceImpl 클래스는 HybridClass를 상속하고, HybridClass에 의해 JVM에서 전달된 객체에 대응하는 적절한 C++ 변환된다. javaobject 클래스가 jni에서 제공하는 클래스인지 fbjni에서 제공하는 클래스인지는 모르겠다. jni에서 JavaVM 객체와 jobject 객체를 사용하여 JVM 객체에 접근합니다.
https://sungcheol-kim.gitbook.io/jni-tutorial/chapter25

반면 javaobject 객체는 해당 객체로 부터 getClass()->getMethod()로 특정 객체의 메서드에 접근할 수 있습니다. https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/jni/react/jni/JavaModuleWrapper.h#L64-L67

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/jni/react/jni/JavaModuleWrapper.cpp#L120-L121  

그리고 위에서 표시하진 않았지만, initHybrid 메서드 또한 C++의 CatalystInstanceImpl에서 구현되었는데, 해당 함수에서 어떤 처리를 하여 변환시킨 것으로 추정됩니다.
https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp#L42-L81

C++은 잘 몰라서 이 부분은 어쩔 수 없이 추정했습니다.

아시는 분 있으시면 댓글로 알려주시면 감사하겠습니다.

 

자바의 ModuleRegistry 객체를 전달받아 생성한 C++의 ModuleRegistry 객체는 javaModules을 통해 클래스, 메서드 ID로 해당 객체와 연결된 자바 객체(mPackages에서 추출된 모듈 객체)의 메서드를 쉽게 호출할 수 있게 지원합니다.

 

ModuleRegistry 객체는 Instance, NativeToJSBridge를 거쳐 JsToNativeBridge 객체까지 전달됩니다.

 

맨 위의 흐름도에서 지금까지 설명한 부분을 표시해 봤습니다.

java → C++ Module Registry

1-2. C++에서 JS에 네이티브 호출 함수 등록

JS에서 자바로의 메서드 호출 최종적으로 ModuleRegistry 객체가 처리합니다.

void ModuleRegistry::callNativeMethod(
    unsigned int moduleId,
    unsigned int methodId,
    folly::dynamic&& params,
    int callId) {
  if (moduleId >= modules_.size()) {
    throw std::runtime_error(folly::to<std::string>(
        "moduleId ", moduleId, " out of range [0..", modules_.size(), ")"));
  }
  modules_[moduleId]->invoke(methodId, std::move(params), callId);
}

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactCommon/cxxreact/ModuleRegistry.cpp#L214-L224

 

이제 JS에서 C++의 ModuleRegistry 객체까지 따라가 보겠습니다.

 

NativeToJsBridge는 이름만 보면 Native → JS 흐름만 담당할 것 같지만, 내부의 JsToNativeBridge 객체(m_delegate)를 통해 JS→Java뿐만 아니라 Java→JS 흐름까지 지원합니다.

NativeToJsBridge::NativeToJsBridge(
    JSExecutorFactory* jsExecutorFactory, // 기본적으로 HermesExecutorFactory
    std::shared_ptr<ModuleRegistry> registry,
    std::shared_ptr<MessageQueueThread> jsQueue,
    std::shared_ptr<InstanceCallback> callback)
    : m_destroyed(std::make_shared<bool>(false)),
      m_delegate(std::make_shared<JsToNativeBridge>(registry, callback)), // 요기 !! registry까지 전달
      // JSIExecutor 상속하는 C++ 자바스크립트 엔진에 대한 참조 생성, 기본적으로 HermesExecutor
      // 여기에 JsToNativeBridge 객체(m_delegate)까지 전달 !!
      m_executor(jsExecutorFactory->createJSExecutor(m_delegate, jsQueue)), 
      m_executorMessageQueueThread(std::move(jsQueue)),
      m_inspectable(m_executor->isInspectable()) {}

 

그리고 JsToNativeBridge 객체를 생성할 때 ModuleRegistry 객체도 전달합니다.

 

그리고 JsToNativeBridge 객체(m_delegate)를 HermesExecutor 객체에 전달합니다.

 

HermesExecutorFactory가 생성하는 HermesExecutor 객체는 JSIExecutor 클래스를 상속합니다. (JSCExecutor도 마찬가지)

 

자바의 CatalystInstnaceImple 객체의 initializeBridge 호출부터 시작하여 C++ JSIExcutor 객체의 initializeRuntime까지 호출되면, JSI를 사용해 JsToNativeBridge 객체(m_delegate)에 포함된 ModuleRegistry 객체에 접근하는 함수를 자바스크립트 global에 추가합니다.

void JSIExecutor::initializeRuntime() {
  runtime_->global().setProperty( // JSI !!
      *runtime_,
      "nativeFlushQueueImmediate",
      Function::createFromHostFunction(
          *runtime_,
          PropNameID::forAscii(*runtime_, "nativeFlushQueueImmediate"),
          1,
          [this](
              jsi::Runtime&,
              const jsi::Value&,
              const jsi::Value* args, // [{moduleId, methodId}[]]
              size_t count) {
            if (count != 1) {
              throw std::invalid_argument(
                  "nativeFlushQueueImmediate arg count must be 1");
            }
            callNativeModules(args[0], false); 
            return Value::undefined();
          }));
}

void JSIExecutor::callNativeModules(const Value& queue, bool isEndOfBatch) {
  // deletage_는 JsToNativeBridge 객체 !!
  delegate_->callNativeModules(
      *this, dynamicFromValue(*runtime_, queue), isEndOfBatch);
}

https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp#L93-L111

 

runtime_ JSExecutorFactory 구현체에서 생성된 자바스크립트 런타임에 대한 참조입니다.

https://github.com/facebook/react-native/blob/defb0bd137711d3e76514d9202005a221a345871/packages/react-native/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp#L93-L111

 

런타임 객체를 통해 자바스크립트 global 객체에 접근할 있습니다.

 

전체 흐름에서 지금까지 설명한 부분을 표시해 봤습니다.

register nativeFlushQueueImmediate

 

JsToNativeBridge 객체(delegate_)는 ModuleRegistry 객체(m_registry)에 네이티브 모듈의 메서드 호출을 요청합니다.

void callNativeModules(
    [[maybe_unused]] JSExecutor& executor,
    folly::dynamic&& calls,
    bool isEndOfBatch) override {
  std::vector<MethodCall> methodCalls = parseMethodCalls(std::move(calls));
  for (auto& call : methodCalls) {
    m_registry->callNativeMethod(
        call.moduleId, call.methodId, std::move(call.arguments), call.callId);
  }
}

https://github.com/facebook/react-native/blob/defb0bd137711d3e76514d9202005a221a345871/packages/react-native/ReactCommon/cxxreact/NativeToJsBridge.cpp#L61-L71

 

ModuleRegistry 객체의 callNativeMethod 메서드는 1-2를 시작할 때 이미 보여드렸습니다.

 

JSIExecutor 객체(HermesExecutor 객체)의 initializeRuntime 메서드가 실행되면, 자바스크림트 런타임에서는 최종적으로 global.nativeFlushQueueImmediate를 호출하여 네이티브 메서드를 호출하게 됩니다.

 

그런데 매개변수 calls는 배열입니다. (참고: parseMethodCalls)

 

이것은 JS에선 네이티브 요청을 5ms(MIN_TIME_BETWEEN_FLUSHES_MS) 동안 모았다가 한 번에 호출하기 때문입니다.

enqueueNativeCall(
  moduleID: number,
  methodID: number,
  params: mixed[],
  onFail: (...mixed[]) => void,
  onSucc: (...mixed[]) => void,
): void {
  this._queue[MODULE_IDS].push(moduleID); // queue 계속 적재
  this._queue[METHOD_IDS].push(methodID);
  this._queue[PARAMS].push(params);
  const now = Date.now();
  if (
    global.nativeFlushQueueImmediate &&
    now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS // 5
  ) {
    const queue = this._queue;
    this._queue = [[], [], [], this._callID];
    this._lastFlush = now;
    global.nativeFlushQueueImmediate(queue); // 요기 !! 한 번에 호출
  }
}

https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/BatchedBridge/MessageQueue.js#L312-L321

 

이 부분이 자바스크립트와 C++ 인접면에서 호출되는 자바스크립트 함수입니다.

1-3. JS에서 C++의 Module Registry까지 요청

그럼 어디서 enqueueNativeCall 함수를 호출하는지 알아보겠습니다.

 

Old Architecture에서 네이티브 모듈을 JS로 노출할 때 다음과 같이 NativeModules 객체를 사용합니다.

import { NativeModules } from 'react-native';

const AwesomeLibrary = NativeModules.AwesomeLibrary;

export function multiply(a: number, b: number): Promise<number> {
  return AwesomeLibrary.multiply(a, b);
}

 

그림에 표시하진 않았지만, NativeModules 객체는 C++의 NativeModuleProxy 객체입니다.

let NativeModules: {[moduleName: string]: $FlowFixMe, ...} = {};
if (global.nativeModuleProxy) {
  NativeModules = global.nativeModuleProxy; // ← C++에서 JSI로 등록
}

module.exports = NativeModules;

https://github.com/facebook/react-native/blob/v0.75.1/packages/react-native/Libraries/BatchedBridge/NativeModules.js#L177-L180

void JSIExecutor::initializeRuntime() {
  runtime_->global().setProperty(
      *runtime_,
      "nativeModuleProxy",
      Object::createFromHostObject(
          *runtime_, std::make_shared<NativeModuleProxy>(nativeModules_)));
  // ...
}

class JSIExecutor::NativeModuleProxy : public jsi::HostObject { // 요기 !!
 public:
  NativeModuleProxy(std::shared_ptr<JSINativeModules> nativeModules)
      : weakNativeModules_(nativeModules) {}

  Value get(Runtime& rt, const PropNameID& name) override {
    // ...
    auto nativeModules = weakNativeModules_.lock();
    return nativeModules->getModule(rt, name); // 요기 !!
  }
};

https://github.com/facebook/react-native/blob/v0.75.1/packages/react-native/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp#L87-L91

 

NativeModuleProxy 객체는 jsi::HostObject를 상속하는데, jsi::HostObject는 다음과 같이 자바스크립트에서 해당 객체의 어떤 속성에 접근하면 자동으로 해당 객체의 get 메서드가 호출되도록 구현되어 있습니다.

const AwesomeLibrary = NativeModules.AwesomeLibrary;

 

이때 JSINativeModules 객체(nativeModules)의 getModule로 모듈을 얻어오는데, 여기서 JSgenModule이 호출됩니다.

JSINativeModules::JSINativeModules(
    std::shared_ptr<ModuleRegistry> moduleRegistry)
    : m_moduleRegistry(std::move(moduleRegistry)) {}

Value JSINativeModules::getModule(Runtime& rt, const PropNameID& name) {
  // m_objects에 캐시가 있다면 캐시 반환

  // 다음 로직은 한 번만 호출
  std::string moduleName = name.utf8(rt);
  auto module = createModule(rt, moduleName); // 요기 !!
  auto result = m_objects.emplace(std::move(moduleName), std::move(*module)).first;
  Value ret = Value(rt, result->second);
  return ret;
}

std::optional<Object> JSINativeModules::createModule(
    Runtime& rt,
    const std::string& name) {
  if (!m_genNativeModuleJS) {
    // JS의 global.__fbGenNativeModule 함수 참조
    m_genNativeModuleJS =
        rt.global().getPropertyAsFunction(rt, "__fbGenNativeModule"); 
  }
  
  auto result = m_moduleRegistry->getConfig(name);

  // JS의 global.__fbGenNativeModule 함수 호출
  Value moduleInfo = m_genNativeModuleJS->call( // genModule 호출
      rt,
      valueFromDynamic(rt, result->config),
      static_cast<double>(result->index));
  // ...
  // moduleInfo 객체에서 module 속성 추출하여 상호 참조 가능한 C++ 객체로 변환 반환 
  std::optional<Object> module(
      moduleInfo.asObject(rt).getPropertyAsObject(rt, "module"));

  return module; // → getModule에서 m_objects에 저장
}

https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/jsiexecutor/jsireact/JSINativeModules.cpp#L46-L83

 

m_moduleRegistry는 C++의 ModuleRegistry 객체입니다.

 

해당 객체를 통해 JAVA 네이티브 모듈뿐만 아니라 모듈에 대한 정보도 얻어올 수 있습니다.

 

모듈 정보를 얻어와서 자바스크립트의 global.__fbGenNativeModule 함수를 호출합니다.

 

global.__fbGenNativeModule 함수는 NativeModules의 genModule 함수입니다.

global.__fbGenNativeModule = genModule; // JSI에서 genModule에 접근할 수 있도록 global에 노출 

// 모듈 생성
function genModule(
  config: ?ModuleConfig,
  moduleID: number,
) {
  const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
  const module: {[string]: mixed} = {};
  // 메서드 생성
  methods &&
    methods.forEach((methodName, methodID) => {
      const isPromise =
        (promiseMethods && arrayContains(promiseMethods, methodID)) || false;
      const isSync =
        (syncMethods && arrayContains(syncMethods, methodID)) || false;
      const methodType = isPromise ? 'promise' : isSync ? 'sync' : 'async';
      module[methodName] = genMethod(moduleID, methodID, methodType);
    });
  Object.assign(module, constants);
   // moduleInfo 객체 (module만 C++에 저장)
  return {name: moduleName, module};
}

// 메서드 생성
function genMethod(moduleID: number, methodID: number, type: MethodType) {
  let fn = null;
  if (type === 'promise') {
    fn = function promiseMethodWrapper(...args: Array<mixed>) { // 요기 !!
      return new Promise((resolve, reject) => {
        BatchedBridge.enqueueNativeCall( // MessageQueue 객체
          moduleID,
          methodID,
          args,
          data => resolve(data),
          errorData =>
            reject(
              updateErrorWithErrorData(
                (errorData: $FlowFixMe),
                enqueueingFrameError,
              ),
            ),
        );
      });
    };
  } else {
    // 비슷한 처리...
  }
  fn.type = type;
  return fn;
}

https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/BatchedBridge/NativeModules.js#L101

 

이제 JS와 C++ 영역 상호간 nativeModuleProxy를 공유하게 되었습니다.

 

좀 복잡하지만 요약하면 JS에서 NativeModuels.AwesomeLibrary와 같이 NativeModules에 접근하면, C++ 측에서 다시 JS의 genModule 함수로 모듈 객체를 만들어(1회성, 이후 캐시 사용) C++ 객체로 저장하고, 이것을 JS측에 전달합니다.

 

JS 함수로 객체를 만들어 C++에서 가지고 있고, 요청을 받으면, 다시 JS로 반환하는 방식입니다.

 

모듈 객체는 C++ 영역에 있지만, 모듈의 메서드를 호출하면 JS 영역의 promiseMethodWrapper 함수가 호출됩니다.

 

promiseMethodWrapper 함수에서 MessageQueue 객체(BatchedBridge)의 enqueueNativeCall 메서드를 호출합니다.

 

결론적으로 위에서 언급한 NativeModules을 다시 보면, 모듈명으로 모듈 객체를 얻어와 메서드를 실행시켰을 때, MessageQueue 객체(BatchedBridge)의 enqueueNativeCall 메서드가 실행됩니다.

import { NativeModules } from 'react-native';
 
const AwesomeLibrary = NativeModules.AwesomeLibrary; // C++ 객체
 
export function multiply(a: number, b: number): Promise<number> {
  // AwesomeLibrary.multiply가 promiseMethodWrapper 함수입니다.
  // BatchedBridge.enqueueNativeCall(AusomeLibrary ID, multiply method ID, ...)를 실행하는 Promise를 반환합니다.
  return AwesomeLibrary.multiply(a, b);
}

 

2. Java → JS

2-1. C++에서 JS 함수 가져오기

더 정확히는 JS 모듈의 메서드를 호출하는 함수를 가져오는 것입니다.

 

Java와 달리 ModuleRegistry 객체 자체를 C++로 노출하지 않습니다.

 

먼저 BatchedBridge 객체의 registerCallableModule 메서드를 통해 모듈을 등록할 수 있습니다.

BatchedBridge.registerCallableModule("Greeting", {
  sayHello: () => console.log("hello"),
});

https://github.com/facebook/react-native/blob/f00e8baff6570e065539ccd85ce72b04e8036f75/packages/react-native/Libraries/BatchedBridge/MessageQueue.js#L144-L146

 

위의 예제를 실행하면 JS의 ModuleRegistry 객체인 _lazyCallableModule 객체에 sayHello 메서드가 있는 Greeting 모듈이 등록됩니다.

 

BatchedBridge는 __fbBatchedBridge라는 이름으로 global에 등록됩니다.

Object.defineProperty(global, '__fbBatchedBridge', {
  configurable: true,
  value: BatchedBridge,
});

https://github.com/facebook/react-native/blob/f00e8baff6570e065539ccd85ce72b04e8036f75/packages/react-native/Libraries/BatchedBridge/BatchedBridge.js#L23-L26

 

BatchedBridge의 callFunctionReturnFlushedQueue → __callFunction를 통해서 _lazyCallableModules에 등록된 모듈의 메서드를 호출할 수 있습니다.

// callFunction + flushedQueue
callFunctionReturnFlushedQueue(
  module: string,
  method: string,
  args: mixed[],
): null | [Array<number>, Array<number>, Array<mixed>, number] {
  this.__guard(() => {
    this.__callFunction(module, method, args); // 요기 !!
  });
  return this.flushedQueue();
}

__callFunction(module: string, method: string, args: mixed[]): void {
  const moduleMethods = this.getCallableModule(module); // this._lazyCallableModules[name];
  moduleMethods[method].apply(moduleMethods, args);
}

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/Libraries/BatchedBridge/MessageQueue.js#L113

 

https://github.com/facebook/react-native/blob/v0.75.2/packages/react-native/Libraries/BatchedBridge/MessageQueue.js#L412-L434

 

그리고 C++에선 JSI를 사용하여 BatchedBridge.callFunctionReturnFlushedQueue 메서드를 호출할 준비를 하고,

void JSIExecutor::bindBridge() {
  std::call_once(bindFlag_, [this] {
    // ...
    Value batchedBridgeValue =
        runtime_->global().getProperty(*runtime_, "__fbBatchedBridge"); // JSI !!
    // ...
    Object batchedBridge = batchedBridgeValue.asObject(*runtime_);

    callFunctionReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction( // 요기 !!
        *runtime_, "callFunctionReturnFlushedQueue");

    // ...
  });
}

https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp#L367-L385

 

JS 함수를 호출할 수 있습니다.

void JSIExecutor::callFunction(
    const std::string& moduleId,
    const std::string& methodId,
    const folly::dynamic& arguments) {
  if (!callFunctionReturnFlushedQueue_) {
    bindBridge(); // 요기 !! callFunctionReturnFlushedQueue_ 준비
  }

  Value ret = Value::undefined();

  try {
    scopedTimeoutInvoker_(
        [&] {
          ret = callFunctionReturnFlushedQueue_->call(
              *runtime_,
              moduleId,
              methodId,
              valueFromDynamic(*runtime_, arguments));
        },
        std::move(errorProducer));
  } catch (...) {
    // ...
    
  }
  // ...
}

https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp#L224-L244

 

이제 이 함수를 Java에서 호출할 수 있게 해야 합니다.

2-2. C++에서 Java와 JS 함수 연결

JS→Java에서 언급했다시피 자바의 CatalystInstance 객체에서 reactnativejni C++ 공유 라이브러리를 로드할 때,

 

packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp

void CatalystInstanceImpl::registerNatives() {
  registerHybrid({
      // ...
      makeNativeMethod(
          "initializeBridge", CatalystInstanceImpl::initializeBridge),
      // ...
      makeNativeMethod(
          "jniCallJSFunction", CatalystInstanceImpl::jniCallJSFunction), //  요기 !!
      makeNativeMethod(
          "jniCallJSCallback", CatalystInstanceImpl::jniCallJSCallback),
      // ...
  });
}

void CatalystInstanceImpl::jniCallJSFunction(
    std::string module,
    std::string method,
    NativeArray* arguments) {
  instance_->callJSFunction(
      std::move(module), std::move(method), arguments->consume());
}

https://github.com/facebook/react-native/blob/v0.74.5/packages/react-native/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L91

 

JS→Java를 위한 initializeBridge 함수뿐만 아니라, CatalystInstance 객체의 jniCallFunction과 jniCallCallback 메서드도 구현합니다.

2-3. Java에서 JS까지

최종적으로 네이티브측에선 다음과 같이 호출할 수 있습니다.

// CatalystInstance 구현 객체
catalystInstance.jniCallJSFunction("Greeting", "sayHello", []);

 

자바측에서 CatalystInstance 구현 객체는 ReactApplication 구현체(MainApplication) ReactNatviveHostReactInstanceManagerReactContext로 부터 얻을 수 있습니다.

3. 느낀점

꽤나 복잡해 보이지만, 리액트 네이티브가 처음 나왔을 때 브릿지에 대해 알려진 것처럼 각 환경에서 이벤트가 호출되길 기다리고, 문자열 이벤트 데이터를 파싱하는 방식은 Old Architecture에서도 사용하지 않는 것으로 보입니다.

 

JNI와 JSI를 사용해서 직접 통신하기 때문에, 단일 환경에서 처리되는 네이티브나 플러터에 비할 바는 아니겠지만, 브릿지때문에 눈에 띄는 성능 저하가 나타나는 것은  아닌 것 같았습니다. 

 

기계어에 비해 느린 스크립트 언어때문일까 생각해 봤지만, 이것도 사람이 느낄 정도까지는 아니라고 생각합니다.

 

그렇다면 리액트 네이티브가 느리다고 느껴지는 원인"터치 시스템"에 있을까요?

다음 포스트인 "리액트 네이티브 터치 이벤트 흐름"에선 CatalystInstance 객체를 사용하여 안드로이트 터치 이벤트를 자바스크립트로 어떻게 전송하는지 알아보겠습니다.