Skip to main content

크로스 플랫폼 네이티브 모듈 (C++)

C++로 모듈을 작성하면 Android와 iOS 간에 플랫폼에 구애받지 않는 코드를 공유할 수 있다. 순수 C++ 모듈을 사용하면 로직을 한 번만 작성하고 모든 플랫폼에서 바로 재사용할 수 있다. 플랫폼별 코드를 따로 작성할 필요가 없다.

이 가이드에서는 순수 C++ Turbo 네이티브 모듈을 만드는 과정을 단계별로 설명한다:

  1. JS 스펙 생성
  2. 스캐폴딩 생성을 위해 Codegen 설정
  3. 네이티브 로직 구현
  4. Android와 iOS 애플리케이션에서 모듈 등록
  5. JS에서 변경 사항 테스트

이 가이드의 나머지 부분은 다음 커맨드를 실행해 애플리케이션을 생성했다고 가정한다:

shell
npx @react-native-community/cli@latest init SampleApp --version 0.76.0

1. JS 스펙 파일 생성

순수 C++ Turbo Native Module은 Turbo Native Module이다. Codegen이 스캐폴딩 코드를 생성할 수 있도록 스펙 파일(또는 spec 파일)이 필요하다. 이 스펙 파일은 JS에서 Turbo Native Module에 접근할 때 사용한다.

스펙 파일은 타입이 지정된 JS 언어로 작성해야 한다. React Native는 현재 Flow나 TypeScript를 지원한다.

  1. 앱의 루트 폴더 안에 specs라는 새 폴더를 만든다.
  2. NativeSampleModule.ts라는 새 파일을 생성하고 다음 코드를 추가한다.
warning

모든 Native Turbo Module 스펙 파일은 Native 접두사를 붙여야 한다. 그렇지 않으면 Codegen이 이를 무시한다.

specs/NativeSampleModule.ts
import {TurboModule, TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
readonly reverseString: (input: string) => string;
}

export default TurboModuleRegistry.getEnforcing<Spec>(
'NativeSampleModule',
);

2. Codegen 설정

다음 단계는 package.json 파일에서 Codegen을 설정하는 것이다. 파일을 다음과 같이 업데이트한다:

package.json
     "start": "react-native start",
"test": "jest"
},
"codegenConfig": {
"name": "AppSpecs",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.sampleapp.specs"
}
},
"dependencies": {

이 설정은 Codegen이 specs 폴더에서 스펙 파일을 찾도록 지시한다. 또한 Codegen이 modules에 대해서만 코드를 생성하고, 생성된 코드를 AppSpecs 네임스페이스로 묶도록 설정한다.

3. 네이티브 코드 작성하기

C++ Turbo Native Module을 작성하면 Android와 iOS 간에 코드를 공유할 수 있다. 따라서 한 번만 코드를 작성하고, 플랫폼별로 어떤 변경이 필요한지 살펴보면 C++ 코드를 활용할 수 있다.

  1. androidios 폴더와 동일한 레벨에 shared 폴더를 생성한다.

  2. shared 폴더 안에 NativeSampleModule.h 파일을 만든다.

    shared/NativeSampleModule.h
    #pragma once

    #include <AppSpecsJSI.h>

    #include <memory>
    #include <string>

    namespace facebook::react {

    class NativeSampleModule : public NativeSampleModuleCxxSpec<NativeSampleModule> {
    public:
    NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker);

    std::string reverseString(jsi::Runtime& rt, std::string input);
    };

    } // namespace facebook::react

  3. shared 폴더 안에 NativeSampleModule.cpp 파일을 생성한다.

    shared/NativeSampleModule.cpp
    #include "NativeSampleModule.h"

    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());
    }

    } // namespace facebook::react

이제 생성한 두 파일을 살펴보자:

  • NativeSampleModule.h 파일은 순수 C++ TurboModule의 헤더 파일이다. include 문은 Codegen에 의해 생성될 스펙을 포함하며, 이 스펙은 구현해야 할 인터페이스와 기본 클래스를 담고 있다.
  • 이 모듈은 facebook::react 네임스페이스 안에 위치해 해당 네임스페이스의 모든 타입에 접근할 수 있다.
  • NativeSampleModule 클래스는 실제 Turbo Native Module 클래스이며, NativeSampleModuleCxxSpec 클래스를 상속받는다. 이 클래스는 Turbo Native Module로 동작할 수 있도록 하는 글루 코드와 보일러플레이트 코드를 포함한다.
  • 마지막으로, JS와 통신할 때 필요한 CallInvoker에 대한 포인터를 받는 생성자와 구현해야 할 함수의 프로토타입이 있다.

NativeSampleModule.cpp 파일은 Turbo Native Module의 실제 구현을 담고 있으며, 스펙에서 선언한 생성자와 메서드를 구현한다.

4. 플랫폼에 모듈 등록하기

다음 단계에서는 플랫폼에 모듈을 등록한다. 이 과정은 네이티브 코드를 JS에 노출시켜 React Native 애플리케이션이 JS 레이어에서 네이티브 메서드를 호출할 수 있게 만드는 핵심 단계다.

이 단계는 플랫폼별 코드를 작성해야 하는 유일한 경우다.

Android

안드로이드 앱이 C++ Turbo Native Module을 효과적으로 빌드할 수 있도록 하려면 다음 단계를 따라야 한다:

  1. C++ 코드에 접근하기 위해 CMakeLists.txt 파일을 생성한다.
  2. 새로 생성된 CMakeLists.txt 파일을 가리키도록 build.gradle 파일을 수정한다.
  3. 새로운 Turbo Native Module을 등록하기 위해 안드로이드 앱에 OnLoad.cpp 파일을 생성한다.

1. CMakeLists.txt 파일 생성

Android는 빌드 도구로 CMake를 사용한다. CMake가 공유 폴더에 정의한 파일에 접근하려면 해당 파일들을 빌드할 수 있어야 한다.

  1. SampleApp/android/app/src/main/jni 폴더를 새로 만든다. jni 폴더는 Android의 C++ 코드가 위치하는 곳이다.
  2. CMakeLists.txt 파일을 생성하고 다음 내용을 추가한다:
CMakeLists.txt
cmake_minimum_required(VERSION 3.13)

# 라이브러리 이름을 정의한다.
project(appmodules)

# React Native 애플리케이션을 빌드하는 데 필요한 모든 것을 포함한다.
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)

# 추가 소스 코드가 위치한 디렉토리를 정의한다. jni, main, src, app, android 폴더를 거슬러 올라가야 한다.
target_sources(${CMAKE_PROJECT_NAME} PRIVATE ../../../../../shared/NativeSampleModule.cpp)

# CMake가 추가 헤더 파일을 찾을 수 있는 디렉토리를 정의한다. jni, main, src, app, android 폴더를 거슬러 올라가야 한다.
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ../../../../../shared)

이 CMake 파일은 다음과 같은 작업을 수행한다:

  • appmodules 라이브러리를 정의한다. 이 라이브러리에는 앱의 모든 C++ 코드가 포함된다.
  • React Native의 기본 CMake 파일을 로드한다.
  • target_sources 지시어를 사용해 빌드해야 할 모듈의 C++ 소스 코드를 추가한다. 기본적으로 React Native는 appmodules 라이브러리에 기본 소스 코드를 포함시키는데, 여기서는 커스텀 소스 코드를 추가한다. jni 폴더에서 C++ Turbo Module이 위치한 shared 폴더까지 거슬러 올라가는 경로를 확인할 수 있다.
  • CMake가 모듈 헤더 파일을 찾을 수 있는 디렉토리를 지정한다. 이 경우에도 jni 폴더에서 거슬러 올라가는 경로가 필요하다.

2. 커스텀 C++ 코드를 포함하도록 build.gradle 수정

Gradle은 안드로이드 빌드를 관리하는 도구이다. Turbo Native Module을 빌드하기 위해 CMake 파일이 위치한 경로를 Gradle에 알려줘야 한다.

  1. SampleApp/android/app/build.gradle 파일을 연다.
  2. 기존 android 블록 안에 다음 코드를 추가한다:
android/app/build.gradle
    buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// 주의! 프로덕션 환경에서는 자체 keystore 파일을 생성해야 한다.
// 자세한 내용은 https://reactnative.dev/docs/signed-apk-android 참고.
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}

+ externalNativeBuild {
+ cmake {
+ path "src/main/jni/CMakeLists.txt"
+ }
+ }
}

이 블록은 Gradle 파일에 CMakeLists.txt 파일을 찾을 위치를 알려준다. 경로는 build.gradle 파일이 있는 폴더를 기준으로 상대 경로로 지정되며, jni 폴더 내 CMakeLists.txt 파일의 경로를 추가한다.

3. 새로운 Turbo Native Module 등록하기

마지막 단계는 새로운 C++ Turbo Native Module을 런타임에 등록하는 것이다. 이렇게 하면 JS가 C++ Turbo Native Module을 요청할 때 앱이 어디서 찾아야 하는지 알고 반환할 수 있다.

  1. SampleApp/android/app/src/main/jni 폴더에서 다음 명령어를 실행한다:
sh
curl -O https://raw.githubusercontent.com/facebook/react-native/v0.76.0/packages/react-native/ReactAndroid/cmake-utils/default-app-setup/OnLoad.cpp
  1. 그런 다음, 이 파일을 다음과 같이 수정한다:
android/app/src/main/jni/OnLoad.cpp
#include <DefaultComponentsRegistry.h>
#include <DefaultTurboModuleManagerDelegate.h>
#include <autolinking.h>
#include <fbjni/fbjni.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <rncore.h>

+ // NativeSampleModule 헤더 파일 포함
+ #include <NativeSampleModule.h>

//...

std::shared_ptr<TurboModule> cxxModuleProvider(
const std::string& name,
const std::shared_ptr<CallInvoker>& jsInvoker) {
// 여기서 애플리케이션 또는 외부 라이브러리에서 제공하는 CXX Turbo Modules을 등록할 수 있다.
// 아래 예시는 `NativeCxxModuleExample`이라는 모듈을 등록하는 방법을 보여준다:
//
// if (name == NativeCxxModuleExample::kModuleName) {
// return std::make_shared<NativeCxxModuleExample>(jsInvoker);
// }

+ // 이 코드는 모듈을 등록하여 JS 측에서 요청할 때 앱이 반환할 수 있도록 한다.
+ if (name == NativeSampleModule::kModuleName) {
+ return std::make_shared<NativeSampleModule>(jsInvoker);
+ }

// 그리고 CXX 모듈 프로바이더를 자동으로 연결한다.
return autolinking_cxxModuleProvider(name, jsInvoker);
}

// 파일의 나머지 부분은 그대로 둔다.

이 단계들은 React Native에서 원본 OnLoad.cpp 파일을 다운로드하여, C++ Turbo Native Module을 앱에 안전하게 로드할 수 있도록 오버라이드한다.

파일을 다운로드한 후, 다음과 같이 수정한다:

  • 모듈을 가리키는 헤더 파일을 포함한다.
  • Turbo Native Module을 등록하여 JS가 요청할 때 앱이 반환할 수 있도록 한다.

이제 프로젝트 루트에서 yarn android를 실행하여 앱이 성공적으로 빌드되는지 확인할 수 있다.

iOS

iOS 앱이 C++ Turbo Native Module을 효과적으로 빌드할 수 있도록 하려면 다음 단계를 따라야 한다:

  1. pods를 설치하고 Codegen을 실행한다.
  2. iOS 프로젝트에 shared 폴더를 추가한다.
  3. 애플리케이션에서 C++ Turbo Native Module을 등록한다.

1. Pods 설치와 Codegen 실행

첫 번째 단계는 iOS 애플리케이션을 준비할 때마다 수행하는 일반적인 과정이다. CocoaPods는 React Native 의존성을 설정하고 설치하는 데 사용하는 도구이며, 이 과정의 일부로 Codegen도 자동으로 실행한다.

bash
cd ios
bundle install
bundle exec pod install

2. iOS 프로젝트에 shared 폴더 추가하기

이 단계에서는 shared 폴더를 프로젝트에 추가해 Xcode에서 볼 수 있게 한다.

  1. CocoPods로 생성된 Xcode Workspace를 연다.
bash
cd ios
open SampleApp.xcworkspace
  1. 왼쪽에서 SampleApp 프로젝트를 클릭하고 Add files to "Sample App"...을 선택한다.

Add Files to Sample App...

  1. shared 폴더를 선택하고 Add를 클릭한다.

Add Files to Sample App...

모든 과정을 올바르게 진행했다면, 왼쪽의 프로젝트가 다음과 같이 보일 것이다:

Xcode Project

3. 앱에 Cxx Turbo Native Module 등록하기

마지막 단계에서는 iOS 앱이 순수 C++ Turbo Native Module을 찾을 위치를 알려준다.

Xcode에서 AppDelegate.mm 파일을 열고 다음과 같이 수정한다:

SampleApp/AppDelegate.mm
#import <React/RCTBundleURLProvider.h>
+ #import <RCTAppDelegate+Protected.h>
+ #import "NativeSampleModule.h"

// ...
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

+- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
+ jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
+{
+ if (name == "NativeSampleModule") {
+ return std::make_shared<facebook::react::NativeSampleModule>(jsInvoker);
+ }
+
+ return [super getTurboModule:name jsInvoker:jsInvoker];
+}

@end

이 변경 사항은 몇 가지 작업을 수행한다:

  1. RCTAppDelegate+Protected 헤더를 임포트해 AppDelegate가 RCTTurboModuleManagerDelegate 프로토콜을 준수한다는 것을 알린다.
  2. 순수 C++ Native Turbo Module 인터페이스인 NativeSampleModule.h를 임포트한다.
  3. C++ 모듈을 위해 getTurboModule 메서드를 오버라이드한다. 이제 JS 측에서 NativeSampleModule이라는 모듈을 요청하면, 앱이 어떤 모듈을 반환해야 하는지 알 수 있다.

이제 Xcode에서 앱을 빌드하면 성공적으로 빌드될 것이다.

5. 코드 테스트

이제 C++ Turbo Native Module을 JS에서 사용해 볼 차례다. 이를 위해 App.tsx 파일을 수정해 Turbo Native Module을 불러오고 코드에서 호출해야 한다.

  1. App.tsx 파일을 연다.
  2. 템플릿의 내용을 다음 코드로 대체한다:
App.tsx
import React from 'react';
import {
Button,
SafeAreaView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import SampleTurboModule from './specs/NativeSampleModule';

function App(): React.JSX.Element {
const [value, setValue] = React.useState('');
const [reversedValue, setReversedValue] = React.useState('');

const onPress = () => {
const revString = SampleTurboModule.reverseString(value);
setReversedValue(revString);
};

return (
<SafeAreaView style={styles.container}>
<View>
<Text style={styles.title}>
Welcome to C++ Turbo Native Module Example
</Text>
<Text>Write down here he 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>
</View>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 18,
marginBottom: 20,
},
textInput: {
borderColor: 'black',
borderWidth: 1,
borderRadius: 5,
padding: 10,
marginTop: 10,
},
});

export default App;

이 앱에서 주목할 만한 부분은 다음과 같다:

  • import SampleTurboModule from './specs/NativeSampleModule';: 이 줄은 앱에서 Turbo Native Module을 불러온다.
  • onPress 콜백 안의 const revString = SampleTurboModule.reverseString(value);: 이 부분은 앱에서 Turbo Native Module을 사용하는 방법을 보여준다.
warning

이 예제를 간단하게 유지하기 위해 스펙 파일을 직접 앱에서 불러왔다. 하지만 실제로는 스펙을 감싸는 별도의 파일을 만들어 앱에서 사용하는 것이 좋다. 이렇게 하면 스펙에 대한 입력을 준비하고 JS에서 더 많은 제어를 할 수 있다.

축하한다! 첫 번째 C++ Turbo Native Module을 작성했다.

Android
iOS
Android Video
iOS video