Skip to main content

네이티브 모듈

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

기본적인 단계는 다음과 같다:

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

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

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

네이티브 영구 저장소

이 가이드에서는 웹 스토리지 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

이 작업은 안드로이드 애플리케이션을 빌드할 때 자동으로 실행된다.

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}>
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. 네이티브 플랫폼 코드 작성

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

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