Skip to main content
Version: Next

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

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

이 가이드에서는 순수 C++ Turbo 네이티브 모듈을 만드는 과정을 단계별로 살펴볼 것이다:

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

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

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

1. JS 스펙 파일 작성하기

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

스펙 파일은 타입이 지정된 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 네이티브 모듈을 작성하면 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 네이티브 모듈 클래스이며, NativeSampleModuleCxxSpec 클래스를 상속받는다. 이 클래스는 Turbo 네이티브 모듈로 동작하도록 하는 글루 코드와 보일러플레이트 코드를 포함한다.
  • 마지막으로, 생성자는 CallInvoker에 대한 포인터를 받아 JS와 통신할 수 있도록 하고, 스펙에서 선언한 함수의 프로토타입을 구현한다.

NativeSampleModule.cpp 파일은 Turbo 네이티브 모듈의 실제 구현이며, 스펙에서 선언한 생성자와 메서드를 구현한다.

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

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

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

Android

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

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

1. CMakeLists.txt 파일 생성

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

  1. 새로운 폴더 SampleApp/android/app/src/main/jni를 생성한다. jni 폴더는 안드로이드의 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 폴더에서 shared 폴더까지 거슬러 올라가야 하는 것을 확인할 수 있다.
  • CMake가 모듈 헤더 파일을 찾을 수 있는 경로를 지정한다. 이 경우에도 jni 폴더에서 거슬러 올라가야 한다.

2. build.gradle에 커스텀 C++ 코드 추가하기

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 Module을 등록할 수 있다.
// 다음 예제와 유사한 방식으로 진행한다 (예: `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

C++ Turbo Native Module을 iOS 앱에서 효과적으로 빌드하려면 다음 단계를 따라야 한다:

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

1. Pods 설치 및 코드 생성 실행

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. 코드 테스트하기

이제 JS에서 C++ Turbo Native Module에 접근할 차례다. 이를 위해 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