React native JSI

이승훈·2024년 8월 4일
0

TIL

목록 보기
32/32
post-thumbnail

참고
이 글을 읽고 이해한 내용을 그대로 작성한 글 입니다.

들어가기 전에

리액트네이티브에서 JS단과 Native단은 브릿지 기반의 통신을 합니다.
그러나 이러한 통신 방법은 이상적이지 않습니다.
브릿지는 메세지를 배치(일괄 처리)합니다.
그로인해 메시지를 보낼 때 지연이 발생할 수 있습니다.

또한 메시지를 JSON 형식으로 감싸기 위해 직렬화작업이 필요하고 이 또한 지연을 발생시킵니다.

JSI의 등장

Hermes, V8같은 Javascript 런타임들은 C와 C++로 작성되었습니다. 이는 고성능 작업을 위해서 입니다.
개발자들은 이러한 고성능 특성을 활용하기 위해 JSI라는 C++ API를 개발하였습니다.

JSI를 사용하여 얻을 수 있는 장점은 다음과 같습니다.

  • JSI는 Hermes나 V8과 같은 다양한 Javascript엔진들과 네이티브 코드가 일관되게 통신할 수 있는 공통된 인터페이스를 제공합니다.
  • JSI는 타입 변환이나 직렬화 없이 직접적인 데이터 교환을 가능하게하여 성능을 향상 시킵니다.
  • 개발자가 JSI를 사용하면, 특정 Javascript엔진의 세부 구현에 대해 걱정할 필요 없이 일관된 API를 통해 작업할 수 있습니다.

Javascript와 JSI에서의 변수 정의

문자열과 숫자형변수의 정의는 아래처럼 간단하게 이루어집니다.

문자열

const number = 42
jsi::Value number = jsi::Value(42);

숫자

const name = "Marc"
jsi::Value name = jsi::String::createFromUtf8(runtime, "Marc")

함수 정의

JavaScript에서는 일반적인 방식으로 함수를 정의합니다:

const add = (first, second) => {
  return first + second
}

JSI(C++)에서는 createFromHostFunction 메소드를 사용합니다:

auto add = jsi::Function::createFromHostFunction(
    runtime,
    jsi::PropNameID::forAscii(runtime, "add"),
    2,
    [](jsi::Runtime& runtime, const jsi::Value& thisValue,
       const jsi::Value* arguments, size_t count) -> jsi::Value {
        double result = arguments[0].asNumber() + arguments[1].asNumber();
        return jsi::Value(result);
    }
);

이렇게 정의된 함수는 JavaScript에서 직접 사용할 수 있습니다.

const result = add(5, 8)

C++ 측에서도 사용 가능합니다:

auto result = add.call(runtime, 5, 8);

전역 네임스페이스에 함수 추가:

global.add = add;
runtime.global().setProperty(runtime, "add", std::move(add));

JSI 함수의 특징:

  1. 동기적 실행 (await 키워드 불필요):

기존 방식: 네이티브 코드를 호출할 때, JavaScript에서는 보통 비동기 처리를 해야 했습니다.

const result = await someNativeFunction();

JSI 방식: 함수 호출이 즉시 실행되고 결과를 바로 받습니다.

const result = someJSIFunction(); 

차이점: 코드가 순차적으로 실행되며, 결과를 기다리기 위해 특별한 처리(await)가 필요 없습니다.

  1. 직접적이고 빠른 호출:

기존 방식: JavaScript에서 네이티브 함수를 호출할 때, 여러 단계의 변환 과정을 거쳤습니다.
(JavaScript → JSON → 네이티브 코드 → JSON → JavaScript)
JSI 방식: JavaScript에서 직접 C++ 함수를 호출합니다. 중간 변환 과정이 없습니다.
차이점: 마치 JavaScript 내의 일반 함수를 호출하는 것처럼 빠르고 직접적입니다.

비동기 처리 불필요:

기존 방식: 네이티브 코드 호출은 항상 비동기로 처리되어, 콜백이나 Promise를 사용해야 했습니다.
예: someNativeFunction().then(result => { / 처리 / });
JSI 방식: 동기적으로 실행되므로, 일반 JavaScript 함수처럼 사용할 수 있습니다.
예: const result = someJSIFunction(); / 바로 다음 줄에서 결과 사용 가능 /
차이점: 코드 흐름이 더 자연스럽고, 콜백 지옥이나 복잡한 Promise 체인을 피할 수 있습니다.

JSI를 사용하면, C++과 JavaScript 사이의 직접적이고 효율적인 통신이 가능해집니다. 타입 변환이나 직렬화 과정 없이 데이터를 주고받을 수 있어 성능이 향상되며, 동기적 실행으로 코드의 가독성과 사용성도 개선됩니다.

Javascrip 런타임에서 전역 비동기 함수 접근하기

RN에서 JSI를 이용해 C++코드에서 Javascript의 글로벌 비동기 함수에 접근할 수 있다.

1. 글로벌 함수(ex. Promise)접근

Javascript에서는 Promise와 같은 전역 함수가 있고 이 함수는 비동기 작업을 처리하기 위해 많이 사용됩니다.

예를 들어, 데이터를 요청하고 응답을 기다린 다음 그 결과를 처리할 때 Promise를 사용합니다.

JSI를 사용하면 C++코드에서도 이 Promise 함수를 직접 가져와 사용할 수 있습니다.
이를 위해 다음과 같은 코드를 작성합니다.

auto promiseCtor = runtime.global().getPropertyAsFunction(runtime, "Promise");
auto promise = promiseCtor.callAsConstructor(runtime, resolve, reject);

C++은 잘 모르지만 GPT에게 물어본 위 코드의 설명은 아래와 같습니다.

  • runtime.global(): JavaScript의 전역 객체를 가져옵니다. 여기에는 Promise, console.log 등 JavaScript에서 전역적으로 사용할 수 있는 함수들이 들어 있습니다.
  • getPropertyAsFunction(runtime, "Promise"): 전역 객체에서 "Promise"라는 함수를 찾아 가져옵니다.
  • callAsConstructor(runtime, resolve, reject): 이 함수는 마치 JavaScript에서 new Promise(resolve, reject)를 호출하는 것과 동일합니다. resolve와 reject는 Promise의 동작을 정의하는 함수들입니다.

이러한 과정 덕분에 C++ 코드에서 Javascript의 Promise를 직접 생성하고 사용할 수 있게 됩니다.

2. 익명 함수(ex. const x = () => ...) 접근

만약 함수가 const x = () => ... 처럼 이름이 없는 익명 함수라면, 이를 C++코드로 전달해서 호출 할 수 있습니다. 이를 위해 다음과 같은 코드를 작성합니다.

auto nativeFunc = jsi::Function::createFromHostFunction(
  runtime,
  jsi::PropNameID::forAsci(runtime, "someFunc"),
  1,  // 함수가 받을 매개변수의 개수
  [](jsi::Runtime& runtime,
  const jsi::Value& thisValue, 
  const jsi::Value* arguments, 
  size_t count) -> jsi::Value {
    auto func = arguments[0].asObject().asFunction();  // 전달된 첫 번째 매개변수를 함수로 변환
    return func.call(runtime, jsi::Value(42));  // 이 함수를 호출하고 42라는 값을 전달
});

역시나 C++은 하나도 몰라서 GPT에게 물어본 결과 아래와같은 설명을 얻을 수 있었습니다.

  • createFromHostFunction: C++ 코드에서 JavaScript 함수를 생성합니다. 이 함수는 이후 JavaScript에서 사용할 수 있습니다.
  • 람다 함수: 함수의 실제 동작을 정의하는 부분입니다. 여기서는 전달된 첫 번째 매개변수를 함수로 변환하고, 그 함수를 호출하면서 42라는 값을 전달합니다.

이 코드를 통해, C++에서 익명 함수나 전달된 Javascript 함수를 받아서 처리할 수 있습니다.

결론

JSI를 사용하면 C++코드에서 Javascript의 전역 비동기 함수(Promise)나 익명 함수에 접근하고 이를 호출할 수 있습니다.
즉, JSI를 사용하면 이러한 함수들에 더 빠르게 접근하고, Javascript와 네이티브 코드 간의 상호작용이 더 효율적이고 유연해집니다.

추가

JSI는 Javascript 코드를 읽고 C++로 미리 함수를 만들거나 하는게 아닙니다.
JSI는 Javascript와 네이티브 코드(C++등)간의 상호작용을 더 효율적으로 만들어주는 인터페이스 입니다.
그러므로 JSI는 Javascript와 네이티브 코드가 서로 더 가깝게 통신할 수 있는 방법을 제공합니다.

정확히 어떻게 동작하는겨?

  1. Javascript 코드는 여전히 Javascript로 작성됩니다.
  • 내가 작성하는 const x = () => {...} 나 async/await 함수는 여전히 Javascript로 작성되고, 그 자체로 Javascript 런타임에서 실행됩니다.
  1. JSI는 C++과 Javascript 사이의 다리 역할을 합니다.
  • JSI는 Javascript 코드와 네이티브 코드(C++)간의 데이터를 더 효율적으로 주고받을 수 있게 해줍니다. 이는 기존 RN 아키텍쳐의 Bridge와는 다르게, Javasript 런타임과 C++코드가 동일한 메모리 공간을 공유할 수 있도록 해줍니다.
  1. C++로 미리 함수가 만들어지지는 않습니다.
  • JSI는 내가 작성한 Javascript 코드를 C++로 변환하거나 미리 만들어 놓는것이 아닙니다. 대신, 네이티브 코드(C++)를 Javascript에서 호출하거나, Javascript에서 정의된 함수를 C++에서 직접 호출할 수 있는 구조를 제공합니다.
const x = () => { console.log('Hello!'); };

이 코드는 여전히 Javascript 코드로 실행됩니다.
JSI는 이 코드를 C++로 변환하거나 미리 생성하지 않습니다. 대신, JSI를 통해 네이티브 코드(C++)에서 이 함수를 호출하거나, 반대로 Javascript에서 네이티브 코드에 접근할 수 있는 통로를 제공합니다.

그럼 왜 JSI가 중요한가?

JSI가 중요한 이유는 기존의 JS Bridge와 다르게 더 빠르고 효율적인 상호작용을 제공하기 때문입니다. 기존의 JS Bridge에서는 데이터를 JSON으로 직렬화하고, 이를 다시 역 직렬화 하는 과정이 있었기 때문에 성능에 부담이 있었습니다. JSI는 이러한 과정을 제거하고 Javascript와 네이티브 코드 간의 데이터 전달을 직접적으로 처리합니다.

C++과의 관계

JSI를 통해 Javascript 코드가 네이티브 코드(C++)와 더 긴밀하게 연결될 수 있습니다. 예를들어, 내가 만든 Javascript 함수가 C++ 코드에서 호출될 수 있습니다. 또는 C++에서 작성한 함수를 Javascript에서 사용할 수 있습니다.

하지만 이 모든 것은 필요할 때만 일어납니다. Javascript 함수가 C++로 미리 만들어져 있는 것이 아니라, JSI가 이 두 세계(Javascript와 네이티브)를 더 쉽게 연결해줄 뿐입니다.

  • JSI는 JavaScript 코드를 읽고 C++로 미리 함수를 만들어놓는 것이 아닙니다.
  • 작성한 JavaScript 코드는 여전히 JavaScript로 실행됩니다.
  • JSI는 JavaScript와 네이티브 코드 간의 상호작용을 더 빠르고 효율적으로 만들어주는 다리 역할을 합니다.
  • 필요할 때, JavaScript 코드와 네이티브 코드가 서로 쉽게 접근할 수 있도록 도와주는 것이 JSI의 핵심입니다.

이로 인해 RN에서 JSI를 사용하면 더 빠르고 유연한 상호작용이 가능해집니다.

profile
Beyond the wall

0개의 댓글