고급: 커스텀 C++ 타입 사용법
이 가이드는 순수 C++ 터보 네이티브 모듈 가이드에 익숙하다고 가정한다. 해당 가이드의 내용을 기반으로 설명을 진행한다.
C++ 터보 네이티브 모듈은 대부분의 std::
표준 타입에 대한 브릿징 기능을 지원한다. 별도의 추가 코드 없이도 모듈에서 이 타입들을 사용할 수 있다.
앱이나 라이브러리에서 새로운 커스텀 타입을 지원하려면, 필요한 bridging
헤더 파일을 제공해야 한다.
새로운 커스텀 타입 추가: Int64
C++ Turbo Native Modules는 아직 int64_t
숫자를 지원하지 않는다. JavaScript가 2^53보다 큰 숫자를 지원하지 않기 때문이다. 2^53보다 큰 숫자를 표현하기 위해 JavaScript에서는 string
타입을 사용하고, C++에서 자동으로 int64_t
로 변환할 수 있다.
1. 브릿징 헤더 파일 생성하기
새로운 커스텀 타입을 지원하기 위한 첫 번째 단계는 JS 표현에서 C++ 표현으로, 그리고 C++ 표현에서 JS 표현으로 변환을 처리하는 브릿징 헤더를 정의하는 것이다.
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의 새 메서드를 사용하도록 한다. 그런 다음 Android와 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>
);
}
//...
- Android에서 앱을 테스트하려면 프로젝트 루트 폴더에서
yarn android
를 실행한다. - iOS에서 앱을 테스트하려면 프로젝트 루트 폴더에서
yarn ios
를 실행한다.
새로운 구조화된 커스텀 타입 추가: Address
위에서 설명한 접근 방식은 모든 종류의 타입에 적용할 수 있다. 구조화된 타입의 경우, 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
함수를 정의한다. validateFunction
은 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}>
Welcome to C Turbo Native Module Example
</Text>
<Text>Address:</Text>
<TextInput
style={styles.textInput}
placeholder="Write your address here"
onChangeText={setStreet}
value={street}
/>
<Text>Number:</Text>
<TextInput
style={styles.textInput}
placeholder="Write your address here"
onChangeText={setNum}
value={num}
/>
<Button title="Validate" onPress={onPress} />
{isValidAddress != null && (
<Text>
Your address is {isValidAddress ? 'valid' : 'not valid'}
</Text>
)}
</View>
</SafeAreaView>
);
축하한다! 🎉
이제 JS에서 C++로 첫 번째 타입을 연결하는 데 성공했다.