Skip to main content
Version: Next

네이티브 모듈

여러분의 React Native 애플리케이션 코드가 React Native나 기존 라이브러리에서 제공하지 않는 네이티브 플랫폼 API와 상호작용해야 할 수 있다. 이때 Turbo Native Module을 사용해 직접 통합 코드를 작성할 수 있다. 이 가이드에서는 Turbo Native Module을 작성하는 방법을 단계별로 설명한다.

기본 단계는 다음과 같다:

  1. Flow나 TypeScript와 같은 인기 있는 자바스크립트 타입 어노테이션 언어를 사용해 타입이 정의된 자바스크립트 스펙을 작성한다.
  2. 스펙을 네이티브 언어 인터페이스로 변환하는 Codegen을 실행하도록 의존성 관리 시스템을 설정한다.
  3. 작성한 스펙을 사용해 애플리케이션 코드를 작성한다.
  4. 생성된 인터페이스를 사용해 네이티브 플랫폼 코드를 작성하고, 이를 React Native 런타임 환경에 연결한다.

이제 예제 Turbo Native Module을 만들어가며 각 단계를 차례대로 살펴보자. 이 가이드의 나머지 부분에서는 다음 커맨드를 실행해 애플리케이션을 생성했다고 가정한다:

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

네이티브 영구 저장소

이 가이드에서는 Web Storage APIlocalStorage를 구현하는 방법을 설명한다. 이 API는 프로젝트에서 애플리케이션 코드를 작성하는 React 개발자에게 친숙할 것이다.

모바일에서 작동하도록 만들기 위해 Android와 iOS API를 사용해야 한다:

1. 타입 정의 명세서 작성

React Native는 Codegen이라는 도구를 제공한다. 이 도구는 TypeScript나 Flow로 작성된 명세서를 받아 Android와 iOS 플랫폼에 맞는 코드를 생성한다. 명세서는 네이티브 코드와 React Native JavaScript 런타임 간에 주고받을 메서드와 데이터 타입을 정의한다. Turbo Native Module은 명세서, 직접 작성한 네이티브 코드, 그리고 명세서에서 생성된 Codegen 인터페이스를 모두 포함한다.

명세서 파일을 생성하려면 다음 단계를 따른다:

  1. 앱의 루트 폴더 안에 specs라는 새 폴더를 만든다.
  2. NativeLocalStorage.ts라는 새 파일을 생성한다.
info

명세서에서 사용할 수 있는 모든 타입과 생성된 네이티브 타입은 부록 문서에서 확인할 수 있다.

다음은 localStorage 명세서의 구현 예시다:

specs/NativeLocalStorage.ts
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',
);

2. Codegen 실행 설정

React Native Codegen 도구는 플랫폼별 인터페이스와 보일러플레이트 코드를 생성하기 위해 스펙을 사용한다. 이를 위해 Codegen은 스펙을 찾을 위치와 처리 방법을 알아야 한다. package.json 파일을 다음과 같이 업데이트한다:

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

Codegen 설정이 완료되면, 생성된 코드와 연결할 네이티브 코드를 준비해야 한다.

Codegen은 generateCodegenArtifactsFromSchema Gradle 태스크를 통해 실행된다:

bash
cd android
./gradlew generateCodegenArtifactsFromSchema

BUILD SUCCESSFUL in 837ms
14 actionable tasks: 3 executed, 11 up-to-date

이 태스크는 Android 애플리케이션을 빌드할 때 자동으로 실행된다.

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을 사용할 수 없는 경우 예외를 발생시킨다. 이 모드는 모듈이 항상 사용 가능하다고 가정한다.
App.tsx
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. 네이티브 플랫폼 코드 작성하기

모든 준비가 끝났다면, 이제 네이티브 플랫폼 코드를 작성할 차례다. 이 작업은 두 부분으로 나눠 진행한다.

note

이 가이드는 새로운 아키텍처(New Architecture)에서만 동작하는 Turbo Native Module을 만드는 방법을 보여준다. 만약 새로운 아키텍처와 기존 아키텍처(Legacy Architecture)를 모두 지원해야 한다면, 하위 호환성 가이드를 참조하길 바란다.

이제 애플리케이션이 종료된 후에도 localStorage가 유지되도록 안드로이드 플랫폼 코드를 작성해 보자.

첫 번째 단계는 생성된 NativeLocalStorageSpec 인터페이스를 구현하는 것이다:

android/app/src/main/java/com/nativelocalstorage/NativeLocalStorageModule.java
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();
}
}

다음으로 NativeLocalStoragePackage를 생성해야 한다. 이 패키지는 모듈을 React Native 런타임에 등록하기 위한 객체를 제공하며, Base Native Package로 래핑한다:

android/app/src/main/java/com/nativelocalstorage/NativeLocalStoragePackage.java
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;
}
};
}
}

마지막으로, React Native가 이 Package를 찾을 수 있도록 메인 애플리케이션에 알려줘야 한다. 이를 React Native에서 패키지를 "등록"한다고 한다.

이 경우, getPackages 메서드에서 반환되도록 추가한다.

info

나중에 네이티브 모듈을 npm 패키지로 배포하는 방법을 배우게 될 것이다. 이때 빌드 도구가 자동으로 링크를 생성해 준다.

android/app/src/main/java/com/turobmoduleexample/MainApplication.java
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();
}
}
}

이제 에뮬레이터에서 코드를 빌드하고 실행할 수 있다:

bash
npm run android