네이티브 모듈
React Native 애플리케이션 코드에서 React Native나 기존 라이브러리에서 제공하지 않는 네이티브 플랫폼 API와 상호작용해야 할 수 있다. 이때 Turbo Native Module을 사용해 직접 통합 코드를 작성할 수 있다. 이 가이드에서는 Turbo Native Module을 작성하는 방법을 단계별로 설명한다.
기본적인 단계는 다음과 같다:
- 타입이 지정된 JavaScript 명세 정의: Flow나 TypeScript와 같은 인기 있는 JavaScript 타입 어노테이션 언어 중 하나를 사용해 명세를 정의한다.
- 코드젠 실행을 위한 의존성 관리 시스템 설정: 명세를 네이티브 언어 인터페이스로 변환하는 코드젠을 실행하도록 설정한다.
- 애플리케이션 코드 작성: 정의한 명세를 사용해 애플리케이션 코드를 작성한다.
- 생성된 인터페이스를 사용해 네이티브 플랫폼 코드 작성: 생성된 인터페이스를 활용해 네이티브 코드를 작성하고 React Native 런타임 환경에 연결한다.
이제 Turbo Native Module 예제를 만들어가며 각 단계를 차례대로 살펴보자. 이 가이드의 나머지 부분은 다음 커맨드라인 명령어로 애플리케이션을 생성했다고 가정한다:
npx @react-native-community/cli@latest init TurboModuleExample --version 0.76.0
네이티브 영구 저장소
이 가이드에서는 웹 스토리지 API인 localStorage
를 구현하는 방법을 설명한다. 이 API는 프로젝트에서 애플리케이션 코드를 작성하는 React 개발자에게 친숙할 것이다.
모바일에서도 동작하도록 만들려면 Android와 iOS API를 사용해야 한다:
- Android: SharedPreferences
- iOS: NSUserDefaults
1. 타입 명세 선언하기
React Native는 Codegen이라는 도구를 제공한다. 이 도구는 TypeScript나 Flow로 작성된 명세를 받아 Android와 iOS에 특화된 플랫폼 코드를 생성한다. 이 명세는 네이티브 코드와 React Native JavaScript 런타임 간에 주고받을 메서드와 데이터 타입을 선언한다. Turbo Native Module은 명세, 직접 작성한 네이티브 코드, 그리고 명세에서 생성된 Codegen 인터페이스로 구성된다.
명세 파일을 생성하려면 다음 단계를 따른다:
- 앱의 루트 폴더 안에
specs
라는 새 폴더를 만든다. NativeLocalStorage.ts
라는 새 파일을 생성한다.
명세에서 사용할 수 있는 모든 타입과 생성된 네이티브 타입은 부록 문서에서 확인할 수 있다.
다음은 localStorage
명세의 구현 예제이다:
- TypeScript
- Flow
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
setItem(value: string, key: string): void;
getItem(key: string): string | null;
removeItem(key: string): void;
clear(): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>(
'NativeLocalStorage',
);
import type {TurboModule} from 'react-native';
import {TurboModule, TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
setItem(value: string, key: string): void;
getItem(key: string): ?string;
removeItem(key: string): void;
clear(): void;
}
2. Codegen 실행 설정
React Native Codegen 도구는 플랫폼별 인터페이스와 보일러플레이트 코드를 생성하기 위해 스펙을 사용한다. Codegen이 스펙을 찾고 이를 어떻게 처리할지 알 수 있도록 package.json
을 업데이트한다.
"start": "react-native start",
"test": "jest"
},
"codegenConfig": {
"name": "NativeLocalStorageSpec",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.nativelocalstorage"
}
},
"dependencies": {
Codegen 설정이 완료되면, 생성된 코드와 연결할 네이티브 코드를 준비해야 한다.
- Android
- iOS
Codegen은 generateCodegenArtifactsFromSchema
Gradle 태스크를 통해 실행된다:
cd android
./gradlew generateCodegenArtifactsFromSchema
BUILD SUCCESSFUL in 837ms
14 actionable tasks: 3 executed, 11 up-to-date
이 작업은 안드로이드 애플리케이션을 빌드할 때 자동으로 실행된다.
Codegen은 CocoaPods로 생성된 프로젝트에 자동으로 추가되는 스크립트 단계의 일부로 실행된다.
cd ios
bundle install
bundle exec pod install
출력 결과는 다음과 같다:
...
Framework build type is static library
[Codegen] Adding script_phases to ReactCodegen.
[Codegen] Generating ./build/generated/ios/ReactCodegen.podspec.json
[Codegen] Analyzing /Users/me/src/TurboModuleExample/package.json
[Codegen] Searching for codegen-enabled libraries in the app.
[Codegen] Found TurboModuleExample
[Codegen] Searching for codegen-enabled libraries in the project dependencies.
[Codegen] Found react-native
...
3. Turbo Native Module을 사용해 애플리케이션 코드 작성하기
NativeLocalStorage
를 사용해 App.tsx
를 수정했다. 이 파일은 저장할 텍스트, 입력 필드, 그리고 값을 업데이트하는 버튼을 포함한다.
TurboModuleRegistry
는 Turbo Native Module을 검색하는 두 가지 방식을 지원한다:
get<T>(name: string): T | null
: Turbo Native Module을 사용할 수 없으면null
을 반환한다.getEnforcing<T>(name: string): T
: Turbo Native Module을 사용할 수 없으면 예외를 던진다. 이 방식은 모듈이 항상 사용 가능하다고 가정한다.
import React from 'react';
import {
SafeAreaView,
StyleSheet,
Text,
TextInput,
Button,
} from 'react-native';
import NativeLocalStorage from './specs/NativeLocalStorage';
const EMPTY = '<empty>';
function App(): React.JSX.Element {
const [value, setValue] = React.useState<string | null>(null);
const [editingValue, setEditingValue] = React.useState<
string | null
>(null);
React.useEffect(() => {
const storedValue = NativeLocalStorage?.getItem('myKey');
setValue(storedValue ?? '');
}, []);
function saveValue() {
NativeLocalStorage?.setItem(editingValue ?? EMPTY, 'myKey');
setValue(editingValue);
}
function clearAll() {
NativeLocalStorage?.clear();
setValue('');
}
function deleteValue() {
NativeLocalStorage?.removeItem('myKey');
setValue('');
}
return (
<SafeAreaView style={{flex: 1}}>
<Text style={styles.text}>
Current stored value is: {value ?? 'No Value'}
</Text>
<TextInput
placeholder="Enter the text you want to store"
style={styles.textInput}
onChangeText={setEditingValue}
/>
<Button title="Save" onPress={saveValue} />
<Button title="Delete" onPress={deleteValue} />
<Button title="Clear" onPress={clearAll} />
</SafeAreaView>
);
}
const styles = StyleSheet.create({
text: {
margin: 10,
fontSize: 20,
},
textInput: {
margin: 10,
height: 40,
borderColor: 'black',
borderWidth: 1,
paddingLeft: 5,
paddingRight: 5,
borderRadius: 5,
},
});
export default App;
4. 네이티브 플랫폼 코드 작성
모든 준비를 마쳤다면, 이제 네이티브 플랫폼 코드를 작성할 차례이다. 이 작업은 두 부분으로 나누어 진행한다.
이 가이드는 새로운 아키텍처(New Architecture)에서만 동작하는 Turbo Native Module을 만드는 방법을 보여준다. 만약 새로운 아키텍처와 기존 아키텍처(Legacy Architecture) 모두를 지원해야 한다면, 호환성 가이드를 참고한다.
- Android
- iOS
이제 애플리케이션이 종료된 후에도 localStorage
가 유지되도록 안드로이드 플랫폼 코드를 작성할 차례다.
첫 번째 단계는 생성된 NativeLocalStorageSpec
인터페이스를 구현하는 것이다:
- Java
- Kotlin
package com.nativelocalstorage;
import android.content.Context;
import android.content.SharedPreferences;
import com.nativelocalstorage.NativeLocalStorageSpec;
import com.facebook.react.bridge.ReactApplicationContext;
public class NativeLocalStorageModule extends NativeLocalStorageSpec {
public static final String NAME = "NativeLocalStorage";
public NativeLocalStorageModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return NAME;
}
@Override
public void setItem(String value, String key) {
SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString(key, value);
editor.apply();
}
@Override
public String getItem(String key) {
SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
String username = sharedPref.getString(key, null);
return username;
}
@Override
public void removeItem(String key) {
SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
sharedPref.edit().remove(key).apply();
}
}
package com.nativelocalstorage
import android.content.Context
import android.content.SharedPreferences
import com.nativelocalstorage.NativeLocalStorageSpec
import com.facebook.react.bridge.ReactApplicationContext
class NativeLocalStorageModule(reactContext: ReactApplicationContext) : NativeLocalStorageSpec(reactContext) {
override fun getName() = NAME
override fun setItem(value: String, key: String) {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val editor = sharedPref.edit()
editor.putString(key, value)
editor.apply()
}
override fun getItem(key: String): String? {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val username = sharedPref.getString(key, null)
return username.toString()
}
override fun removeItem(key: String) {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val editor = sharedPref.edit()
editor.remove(key)
editor.apply()
}
override fun clear() {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val editor = sharedPref.edit()
editor.clear()
editor.apply()
}
companion object {
const val NAME = "NativeLocalStorage"
}
}
다음으로 NativeLocalStoragePackage
를 생성해야 한다. 이 패키지는 모듈을 React Native 런타임에 등록할 수 있는 객체를 제공하며, Base Native Package로 감싸는 역할을 한다:
- Java
- Kotlin
package com.nativelocalstorage;
import com.facebook.react.BaseReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import java.util.HashMap;
import java.util.Map;
public class NativeLocalStoragePackage extends BaseReactPackage {
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
if (name.equals(NativeLocalStorageModule.NAME)) {
return new NativeLocalStorageModule(reactContext);
} else {
return null;
}
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> get() {
Map<String, ReactModuleInfo> map = new HashMap<>();
map.put(NativeLocalStorageModule.NAME, new ReactModuleInfo(
NativeLocalStorageModule.NAME, // name
NativeLocalStorageModule.NAME, // className
false, // canOverrideExistingModule
false, // needsEagerInit
false, // isCXXModule
true // isTurboModule
));
return map;
}
};
}
}
package com.nativelocalstorage
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
class NativeLocalStoragePackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
if (name == NativeLocalStorageModule.NAME) {
NativeLocalStorageModule(reactContext)
} else {
null
}
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
NativeLocalStorageModule.NAME to ReactModuleInfo(
_name = NativeLocalStorageModule.NAME,
_className = NativeLocalStorageModule.NAME,
_canOverrideExistingModule = false,
_needsEagerInit = false,
isCxxModule = false,
isTurboModule = true
)
)
}
}
마지막으로, React Native가 이 Package
를 찾을 수 있도록 메인 애플리케이션에 알려야 한다. 이를 React Native에서 패키지를 "등록"한다고 표현한다.
이 경우, getPackages 메서드에서 반환되도록 추가한다.
나중에 네이티브 모듈을 npm 패키지로 배포하는 방법을 배우게 될 것이다. 이때 빌드 도구가 자동으로 링크를 처리해준다.
- Java
- Kotlin
package com.inappmodule;
import android.app.Application;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactHost;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactHost;
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.soloader.SoLoader;
import com.nativelocalstorage.NativeLocalStoragePackage;
import java.util.ArrayList;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost reactNativeHost = new DefaultReactNativeHost(this) {
@Override
public List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new NativeLocalStoragePackage());
return packages;
}
@Override
public String getJSMainModuleName() {
return "index";
}
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
public boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
public boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
};
@Override
public ReactHost getReactHost() {
return DefaultReactHost.getDefaultReactHost(getApplicationContext(), reactNativeHost);
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load();
}
}
}
package com.inappmodule
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader
import com.nativelocalstorage.NativeLocalStoragePackage
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(NativeLocalStoragePackage())
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
}
}
이제 에뮬레이터에서 코드를 빌드하고 실행할 수 있다:
- npm
- Yarn
npm run android
yarn run android
이제 애플리케이션이 종료된 후에도 localStorage
가 유지되도록 iOS 플랫폼 코드를 작성할 차례이다.
Xcode 프로젝트 준비
Xcode를 사용해 iOS 프로젝트를 준비한다. 아래 6단계를 완료하면 생성된 NativeLocalStorageSpec
인터페이스를 구현하는 RCTNativeLocalStorage
를 갖게 된다.
- CocoPods로 생성된 Xcode Workspace를 연다:
cd ios
open TurboModuleExample.xcworkspace

- 앱을 우클릭하고
New Group
을 선택한 후, 새로운 그룹 이름을NativeLocalStorage
로 지정한다.

NativeLocalStorage
그룹에서New
→File from Template
를 선택해 새 파일을 생성한다.

Cocoa Touch Class
템플릿을 사용한다.

- Cocoa Touch Class 이름을
RCTNativeLocalStorage
로 지정하고 언어를Objective-C
로 선택한다.

RCTNativeLocalStorage.m
파일을RCTNativeLocalStorage.mm
으로 변경해 Objective-C++ 파일로 변환한다.

NSUserDefaults를 사용해 localStorage 구현하기
먼저 RCTNativeLocalStorage.h
파일을 업데이트한다:
// RCTNativeLocalStorage.h
// TurboModuleExample
#import <Foundation/Foundation.h>
#import <NativeLocalStorageSpec/NativeLocalStorageSpec.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeLocalStorage : NSObject
@interface RCTNativeLocalStorage : NSObject <NativeLocalStorageSpec>
@end
그 다음, 커스텀 suite name을 사용해 NSUserDefaults
를 활용하도록 구현을 업데이트한다.
// RCTNativeLocalStorage.m
// TurboModuleExample
#import "RCTNativeLocalStorage.h"
static NSString *const RCTNativeLocalStorageKey = @"local-storage";
@interface RCTNativeLocalStorage()
@property (strong, nonatomic) NSUserDefaults *localStorage;
@end
@implementation RCTNativeLocalStorage
RCT_EXPORT_MODULE(NativeLocalStorage)
- (id) init {
if (self = [super init]) {
_localStorage = [[NSUserDefaults alloc] initWithSuiteName:RCTNativeLocalStorageKey];
}
return self;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeLocalStorageSpecJSI>(params);
}
- (NSString * _Nullable)getItem:(NSString *)key {
return [self.localStorage stringForKey:key];
}
- (void)setItem:(NSString *)value
key:(NSString *)key {
[self.localStorage setObject:value forKey:key];
}
- (void)removeItem:(NSString *)key {
[self.localStorage removeObjectForKey:key];
}
- (void)clear {
NSDictionary *keys = [self.localStorage dictionaryRepresentation];
for (NSString *key in keys) {
[self removeItem:key];
}
}
@end
중요한 점은 다음과 같다:
RCT_EXPORT_MODULE
은 모듈을 내보내고 등록하며, 이때 사용하는 식별자는 자바스크립트 환경에서 접근할 때 사용하는NativeLocalStorage
이다. 자세한 내용은 이 문서를 참고한다.- Xcode를 사용해 Codegen
@protocol NativeLocalStorageSpec
로 이동할 수 있다. 또한 Xcode를 사용해 스텁을 자동으로 생성할 수도 있다.
시뮬레이터에서 코드 빌드 및 실행
- npm
- Yarn
npm run ios
yarn run ios