고급: 커스텀 C++ 타입 활용
이 가이드는 순수 C++ Turbo 네이티브 모듈 가이드에 익숙하다고 가정한다. 해당 가이드의 내용을 기반으로 더 심화된 내용을 다룬다.
C++ Turbo 네이티브 모듈은 대부분의 std::
표준 타입에 대해 브릿징 기능을 지원한다. 따라서 추가 코드 없이도 이러한 타입을 모듈에서 바로 사용할 수 있다.
앱이나 라이브러리에서 새로운 커스텀 타입을 추가하려면, 필요한 bridging
헤더 파일을 제공해야 한다.
새로운 커스텀 타입 추가: Int64
C++ Turbo Native Modules는 아직 int64_t
숫자를 지원하지 않는다. 이는 JavaScript가 2^53보다 큰 숫자를 지원하지 않기 때문이다. 2^53보다 큰 숫자를 표현하기 위해 JavaScript에서는 string
타입을 사용하고, C++에서는 이를 자동으로 int64_t
로 변환할 수 있다.
1. 브릿징 헤더 파일 생성
새로운 커스텀 타입을 지원하기 위한 첫 번째 단계는 JS 표현과 C++ 표현 간 변환을 처리하는 브릿징 헤더를 정의하는 것이다.
shared
폴더에Int64.h
라는 새 파일을 추가한다.- 해당 파일에 다음 코드를 추가한다:
#pragma once
#include <react/bridging/Bridging.h>
namespace facebook::react {
template <>
struct Bridging<int64_t> {
// JS 표현을 C++ 표현으로 변환
static int64_t fromJs(jsi::Runtime &rt, const jsi::String &value) {
try {
size_t pos;
auto str = value.utf8(rt);
auto num = std::stoll(str, &pos);
if (pos != str.size()) {
throw std::invalid_argument("Invalid number"); // 알파벳 문자열은 지원하지 않음
}
return num;
} catch (const std::logic_error &e) {
throw jsi::JSError(rt, e.what());
}
}
// C++ 표현을 JS 표현으로 변환
static jsi::String toJs(jsi::Runtime &rt, int64_t value) {
return bridging::toJs(rt, std::to_string(value));
}
};
}
커스텀 브릿징 헤더의 주요 구성 요소는 다음과 같다:
Bridging
구조체의 명시적 특수화. 여기서는int64_t
타입을 지정한다.- JS 표현을 C++ 표현으로 변환하는
fromJs
함수 - C++ 표현을 JS 표현으로 변환하는
toJs
함수
iOS에서는 Int64.h
파일을 Xcode 프로젝트에 추가해야 한다.
2. JS 스펙 수정하기
이제 새로운 타입을 사용하는 메서드를 추가하기 위해 JS 스펙을 수정할 수 있다. 평소처럼 Flow나 TypeScript 중 하나를 선택해 스펙을 작성한다.
specs/NativeSampleTurbomodule
파일을 연다.- 다음과 같이 스펙을 수정한다:
- TypeScript
- Flow
import {TurboModule, TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
readonly reverseString: (input: string) => string;
+ readonly cubicRoot: (input: string) => number;
}
export default TurboModuleRegistry.getEnforcing<Spec>(
'NativeSampleModule',
);
// @flow
import type {TurboModule} from 'react-native';
import { TurboModuleRegistry } from "react-native";
export interface Spec extends TurboModule {
+reverseString: (input: string) => string;
+ +cubicRoot: (input: string) => number;
}
export default (TurboModuleRegistry.getEnforcing<Spec>(
"NativeSampleModule"
): Spec);
이 파일들에서 C++에서 구현해야 할 함수를 정의하고 있다.
3. 네이티브 코드 구현
이제 JS 스펙에서 선언한 함수를 구현한다.
specs/NativeSampleModule.h
파일을 열고 다음 변경사항을 적용한다:
#pragma once
#include <AppSpecsJSI.h>
#include <memory>
#include <string>
+ #include "Int64.h"
namespace facebook::react {
class NativeSampleModule : public NativeSampleModuleCxxSpec<NativeSampleModule> {
public:
NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker);
std::string reverseString(jsi::Runtime& rt, std::string input);
+ int32_t cubicRoot(jsi::Runtime& rt, int64_t input);
};
} // namespace facebook::react
specs/NativeSampleModule.cpp
파일을 열고 새로운 함수를 구현한다:
#include "NativeSampleModule.h"
+ #include <cmath>
namespace facebook::react {
NativeSampleModule::NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker)
: NativeSampleModuleCxxSpec(std::move(jsInvoker)) {}
std::string NativeSampleModule::reverseString(jsi::Runtime& rt, std::string input) {
return std::string(input.rbegin(), input.rend());
}
+int32_t NativeSampleModule::cubicRoot(jsi::Runtime& rt, int64_t input) {
+ return std::cbrt(input);
+}
} // namespace facebook::react
이 구현은 수학 연산을 수행하기 위해 <cmath>
C++ 라이브러리를 가져온다. 그런 다음 <cmath>
모듈의 cbrt
기본 함수를 사용해 cubicRoot
함수를 구현한다.
4. 앱에서 코드 테스트하기
이제 앱에서 코드를 테스트할 차례다.
먼저 App.tsx
파일을 업데이트해 TurboModule의 새 메서드를 사용하도록 수정한다. 그런 다음 안드로이드와 iOS에서 앱을 빌드한다.
App.tsx
코드를 열고 다음과 같이 변경한다:
// ...
+ const [cubicSource, setCubicSource] = React.useState('')
+ const [cubicRoot, setCubicRoot] = React.useState(0)
return (
<SafeAreaView style={styles.container}>
<View>
<Text style={styles.title}>
Welcome to C++ Turbo Native Module Example
</Text>
<Text>Write down here the text you want to revert</Text>
<TextInput
style={styles.textInput}
placeholder="Write your text here"
onChangeText={setValue}
value={value}
/>
<Button title="Reverse" onPress={onPress} />
<Text>Reversed text: {reversedValue}</Text>
+ <Text>For which number do you want to compute the Cubic Root?</Text>
+ <TextInput
+ style={styles.textInput}
+ placeholder="Write your text here"
+ onChangeText={setCubicSource}
+ value={cubicSource}
+ />
+ <Button title="Get Cubic Root" onPress={() => setCubicRoot(SampleTurboModule.cubicRoot(cubicSource))} />
+ <Text>The cubic root is: {cubicRoot}</Text>
</View>
</SafeAreaView>
);
}
//...
- 안드로이드에서 앱을 테스트하려면 프로젝트 루트 폴더에서
yarn android
를 실행한다. - iOS에서 앱을 테스트하려면 프로젝트 루트 폴더에서
yarn ios
를 실행한다.
새로운 구조화된 커스텀 타입 추가: 주소
위에서 설명한 접근 방식은 모든 종류의 타입에 일반적으로 적용할 수 있다. 구조화된 타입의 경우, React Native는 JS와 C++ 간의 연결을 더 쉽게 만들어주는 몇 가지 헬퍼 함수를 제공한다.
다음과 같은 속성을 가진 커스텀 Address
타입을 연결하려고 한다고 가정해 보자:
interface Address {
street: string;
num: number;
isInUS: boolean;
}
1. 스펙에 타입 정의하기
첫 번째 단계로, 새로운 커스텀 타입을 JS 스펙에 정의해 Codegen이 필요한 모든 지원 코드를 자동으로 생성할 수 있도록 한다. 이렇게 하면 수동으로 코드를 작성할 필요가 없다.
specs/NativeSampleModule
파일을 열고 다음 변경 사항을 추가한다.
- TypeScript
- Flow
import {TurboModule, TurboModuleRegistry} from 'react-native';
+export type Address = {
+ street: string,
+ num: number,
+ isInUS: boolean,
+};
export interface Spec extends TurboModule {
readonly reverseString: (input: string) => string;
+ readonly validateAddress: (input: Address) => boolean;
}
export default TurboModuleRegistry.getEnforcing<Spec>(
'NativeSampleModule',
);
// @flow
import type {TurboModule} from 'react-native';
import { TurboModuleRegistry } from "react-native";
+export type Address = {
+ street: string,
+ num: number,
+ isInUS: boolean,
+};
export interface Spec extends TurboModule {
+reverseString: (input: string) => string;
+ +validateAddress: (input: Address) => boolean;
}
export default (TurboModuleRegistry.getEnforcing<Spec>(
"NativeSampleModule"
): Spec);
이 코드는 새로운 Address
타입을 정의하고, Turbo Native Module을 위한 validateAddress
함수를 정의한다. validateAddress
함수는 Address
객체를 파라미터로 받는다.
또한 커스텀 타입을 반환하는 함수를 정의할 수도 있다.
2. 브릿징 코드 정의하기
스펙에 정의된 Address
타입을 기반으로, Codegen은 두 가지 헬퍼 타입을 생성한다: NativeSampleModuleAddress
와 NativeSampleModuleAddressBridging
.
첫 번째 타입은 Address
의 정의를 나타낸다. 두 번째 타입은 커스텀 타입을 JS에서 C++로, 또는 그 반대로 전환하기 위한 모든 인프라를 포함한다. 여기서 추가로 필요한 단계는 NativeSampleModuleAddressBridging
타입을 확장하는 Bridging
구조체를 정의하는 것이다.
shared/NativeSampleModule.h
파일을 연다.- 파일에 다음 코드를 추가한다:
#include "Int64.h"
#include <memory>
#include <string>
namespace facebook::react {
+ using Address = NativeSampleModuleAddress<std::string, int32_t, bool>;
+ template <>
+ struct Bridging<Address>
+ : NativeSampleModuleAddressBridging<Address> {};
// ...
}
이 코드는 제네릭 타입인 NativeSampleModuleAddress
에 대한 Address
타입 별칭을 정의한다. 제네릭 인자의 순서는 중요하다: 첫 번째 템플릿 인자는 구조체의 첫 번째 데이터 타입을, 두 번째는 두 번째를, 그리고 그 다음도 마찬가지로 이어진다.
그런 다음, Codegen에 의해 생성된 NativeSampleModuleAddressBridging
을 확장하여 새로운 Address
타입에 대한 Bridging
특수화를 추가한다.
이 타입을 생성할 때는 다음과 같은 규칙을 따른다:
- 이름의 첫 부분은 항상 모듈의 타입이다. 이 예제에서는
NativeSampleModule
이다. - 이름의 두 번째 부분은 항상 스펙에 정의된 JS 타입의 이름이다. 이 예제에서는
Address
이다.
3. 네이티브 코드 구현
이제 C++에서 validateAddress
함수를 구현해야 한다. 먼저 .h
파일에 함수 선언을 추가하고, .cpp
파일에서 실제 구현을 작성한다.
shared/NativeSampleModule.h
파일을 열고 함수 정의를 추가한다.
std::string reverseString(jsi::Runtime& rt, std::string input);
+ bool validateAddress(jsi::Runtime &rt, jsi::Object input);
};
} // namespace facebook::react
shared/NativeSampleModule.cpp
파일을 열고 함수 구현을 추가한다.
bool NativeSampleModule::validateAddress(jsi::Runtime &rt, jsi::Object input) {
std::string street = input.getProperty(rt, "street").asString(rt).utf8(rt);
int32_t number = input.getProperty(rt, "num").asNumber();
return !street.empty() && number > 0;
}
구현에서 Address
를 나타내는 객체는 jsi::Object
이다. 이 객체에서 값을 추출하기 위해 JSI
가 제공하는 접근자를 사용한다:
getProperty()
는 객체에서 이름으로 속성을 가져온다.asString()
은 속성을jsi::String
으로 변환한다.utf8()
은jsi::String
을std::string
으로 변환한다.asNumber()
는 속성을double
로 변환한다.
객체를 수동으로 파싱한 후, 필요한 로직을 구현할 수 있다.
JSI
와 그 동작 방식에 대해 더 자세히 알고 싶다면, App.JS 2024의 이 훌륭한 발표를 참고하라.
4. 앱에서 코드 테스트하기
앱에서 코드를 테스트하려면 App.tsx
파일을 수정해야 한다.
App.tsx
파일을 열고App()
함수의 내용을 지운다.App()
함수의 본문을 다음 코드로 교체한다:
const [street, setStreet] = React.useState('');
const [num, setNum] = React.useState('');
const [isValidAddress, setIsValidAddress] = React.useState<
boolean | null
>(null);
const onPress = () => {
let houseNum = parseInt(num, 10);
if (isNaN(houseNum)) {
houseNum = -1;
}
const address = {
street,
num: houseNum,
isInUS: false,
};
const result = SampleTurboModule.validateAddress(address);
setIsValidAddress(result);
};
return (
<SafeAreaView style={styles.container}>
<View>
<Text style={styles.title}>
C Turbo Native Module 예제에 오신 것을 환영합니다
</Text>
<Text>주소:</Text>
<TextInput
style={styles.textInput}
placeholder="주소를 입력하세요"
onChangeText={setStreet}
value={street}
/>
<Text>번호:</Text>
<TextInput
style={styles.textInput}
placeholder="번호를 입력하세요"
onChangeText={setNum}
value={num}
/>
<Button title="검증" onPress={onPress} />
{isValidAddress != null && (
<Text>
주소가 {isValidAddress ? '유효합니다' : '유효하지 않습니다'}
</Text>
)}
</View>
</SafeAreaView>
);
축하합니다! 🎉
여러분은 JS에서 C++로 타입을 연결하는 첫 번째 작업을 성공적으로 마쳤습니다.