네이티브 모듈
여러분의 React Native 애플리케이션 코드가 React Native나 기존 라이브러리에서 제공하지 않는 네이티브 플랫폼 API와 상호작용해야 할 수 있다. 이때 Turbo Native Module을 사용해 직접 통합 코드를 작성할 수 있다. 이 가이드에서는 Turbo Native Module을 작성하는 방법을 단계별로 설명한다.
기본 단계는 다음과 같다:
- Flow나 TypeScript와 같은 인기 있는 자바스크립트 타입 어노테이션 언어를 사용해 타입이 정의된 자바스크립트 스펙을 작성한다.
- 스펙을 네이티브 언어 인터페이스로 변환하는 Codegen을 실행하도록 의존성 관리 시스템을 설정한다.
- 작성한 스펙을 사용해 애플리케이션 코드를 작성한다.
- 생성된 인터페이스를 사용해 네이티브 플랫폼 코드를 작성하고, 이를 React Native 런타임 환경에 연결한다.
이제 예제 Turbo Native Module을 만들어가며 각 단계를 차례대로 살펴보자. 이 가이드의 나머지 부분에서는 다음 커맨드를 실행해 애플리케이션을 생성했다고 가정한다:
npx @react-native-community/cli@latest init TurboModuleExample --version 0.76.0
네이티브 영구 저장소
이 가이드에서는 Web Storage 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
이 태스크는 Android 애플리케이션을 빌드할 때 자동으로 실행된다.
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}>
현재 저장된 값: {value ?? '값 없음'}
</Text>
<TextInput
placeholder="저장할 텍스트를 입력하세요"
style={styles.textInput}
onChangeText={setEditingValue}
/>
<Button title="저장" onPress={saveValue} />
<Button title="삭제" onPress={deleteValue} />
<Button title="초기화" 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