[React Native] 안드로이드 Turbo Native Module 만들어보기
- 안드로이드 Turbo Native Module 만들어보기
- Turbo Native Module이 React Native 앱 성능을 개선하는 방법
- 디바이스 정보를 가져오는 간단한 네이티브 안드로이드 Turbo Native Module 만들기
안드로이드 Turbo Native Module 만들어보기
개요
터보 네이티브 모듈(Turbo Native Modules)은 React Native 아키텍처에서 새롭게 추가된 기능.
기존 네이티브 모듈의 수정 및 최적화된 접근 방식으로 성능 향상시키고, 최신 React Native 앱과의 통합을 쉽게 만듦.
기존에는 네이티브 모듈(Native Modules)이라고 불렀음.
안드로이드용 커스텀 React Native 터보 모듈 만들기
안드로이드용 커스텀 터보 네이티브 모듈을 만들어보자.
이 모듈을 통해 React Native 앱에서 네이티브 모바일 API에 접근하고,
- 디바이스 모델
- IP 주소
- 가동 시간(Uptime)
- 배터리 상태
- 배터리 잔량
- 안드로이드 버전
등의 정보를 가져올 수 있음.
사전 준비 사항
이 튜토리얼을 따라가려면 다음 준비물 필요.
- 안드로이드 에뮬레이터나 실제 안드로이드 기기
- Node ≥ v20
- 당연한 React Native 앱 개발 경험
React Native에서 터보 네이티브 모듈 이해하기
터보 네이티브 모듈은 기존 네이티브 모듈에서 성능 향상을 위해 추가된 기능들.
비동기 브리지를 JSI(JavaScript Interface)로 대체해서 JavaScript와 네이티브 코드 간 통신 성능 문제를 해결함.
터보 네이티브 모듈 아키텍처는 C++로 구현되어 있고, 이점은 다음과 같음.
- JavaScript와 네이티브 코드 간 타입 안정성 제공
- JavaScript와 네이티브 레이어 간 동기화된 데이터 처리
- 다양한 플랫폼에서 코드 공유 가능
- 네이티브 런타임과 동기적 접근 지원
Turbo Native Module이 React Native 앱 성능을 개선하는 방법
Turbo Native Module이 React Native 앱 성능을 개선하는 방법을 이해하려면,
React Native 아키텍처에서 중요한 몇 가지 키워드를 알아야 함.
- 비동기 브리지
- JSI (JavaScript Interface)
- 코드젠(Codegen)
- 패브릭 렌더링 아키텍처(Fabric Rendering Architecture)
비동기 브리지
비동기 브리지는 이전 아키텍처에서 네이티브 플랫폼(iOS, Android)과 JavaScript 간의 주요 통신 방법.
작동 방식은 이렇다:
- 요청은 JSON 문자열로 변환됨
- 이 문자열은 JavaScript 엔진과 네이티브 측 간에 비동기적으로 전달됨
- 응답은 JSON 문자열로 인코딩하고 디코딩됨
JSI (JavaScript Interface)
- JSI는 JavaScript와 C++가 메모리 참조를 공유할 수 있게 해주는 인터페이스.
- 이를 통해 JavaScript와 네이티브 플랫폼 간의 직접적인 통신이 가능해지며, 직렬화 비용 없이 호출할 수 있음.
- JavaScript 엔진에서 네이티브 메서드(C++, Objective-C, Java)를 직접 호출하고, 데이터베이스나 복잡한 인스턴스 기반 타입에 접근할 수 있음.
Codegen
코드젠은 JavaScript 엔진과 Turbo Native Module을 연결하는 보일러플레이트 코드를 자동으로 생성하는 도구.
이 도구는 네이티브 모듈을 만들 때 발생할 수 있는 크로스 바운더리 타입 오류를 줄여주며(크로스 플랫폼 앱에서 자주 발생하는 충돌 원인),
JavaScript와 네이티브 플랫폼 코드 간의 통신을 일관되게 처리할 수 있게 해줌.
Fabric Architecture
- 패브릭 렌더링 아키텍처는 네이티브 모듈과 JSI와 함께 작동하여 렌더링 성능을 향상시킴.
- 불필요한 업데이트를 줄여줌으로써 성능을 개선하며, 비동기적 및 동기적 업데이트를 모두 지원함.
C++ Turbo Native Module
- 네이티브 모듈 아키텍처는 C++로 작성된 모듈을 지원.
- 네이티브 모듈은 Swift 또는 Objective-C로 iOS 플랫폼 코드를, Java 또는 Kotlin으로 Android - 플랫폼 코드를 작성할 수 있지만,
- C++ Turbo 모듈을 사용하면 C++로 모듈을 작성할 수 있고, Android, iOS, Windows, macOS 등 모든 플랫폼에서 작동함.
앱에 성능 최적화와 세밀한 메모리 관리가 필요하다면 C++ Turbo Native Module을 고려해 볼 것.
디바이스 정보를 가져오는 간단한 네이티브 안드로이드 Turbo Native Module 만들기
React Native 앱이 네이티브 안드로이드 API를 통해 디바이스 모델, IP 주소, 가동 시간, 배터리 상태, 배터리 잔량, 안드로이드 버전 등의 정보를 가져올 수 있도록 커스텀 Turbo Native Module을 만들어보자
이 작업을 위해서는 다음의 안드로이드 API들을 사용해야 함:
프로젝트 설정
React Native 프로젝트를 먼저 생성하자
npx @react-native-community/cli@latest init TurboModuleExample --version 0.76.0
이후
npm run android
Typed JavaScript Specification
Turbo Module을 구현하려면 TypeScript를 사용하여 타입이 지정된 JavaScript 사양을 정의해야 함. 이 사양은 네이티브 플랫폼 코드에서 사용하는 데이터 타입과 메서드를 선언함.
프로젝트의 루트 디렉터리에 spec
폴더를 만들고 NativeGetDeviceInfo.ts
파일을 추가한 후, 다음과 같이 작성함:
import type { TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";
export interface Spec extends TurboModule {
getDeviceModel(): Promise<string>;
getDeviceIpAddress(): Promise<string>;
getDeviceUptime(): Promise<string>;
getBatteryStatus(): Promise<string>;
getBatteryLevel(): Promise<string>;
getAndroidVersion(): Promise<string>;
}
export default TurboModuleRegistry.getEnforcing<Spec>("NativeGetDeviceInfo");
Codegen 설정하기
Codegen 도구를 설정하여 타입 지정된 사양을 사용해 플랫폼별 인터페이스와 보일러플레이트 코드를 생성함. 이를 위해 package.json 파일을 업데이트하여 다음을 추가함
"dependencies": {
...
},
"codegenConfig": {
"name": "NativeGetDeviceInfoSpec",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.nativegetdeviceinfo"
}
}
타입 지정된 사양을 사용하여 보일러플레이트 코드를 생성하려면 다음 명령어를 실행
cd android
./gradlew generateCodegenArtifactsFromSchema
네이티브 플랫폼 코드 구현
프로젝트 루트 디렉터리에서 android/app/src/main/java/com
디렉터리로 이동하고, nativegetdeviceinfo
라는 폴더를 생성함. 폴더 안에 NativeGetDeviceInfoModule.kt
라는 파일을 만들고, 다음 코드를 추가함:
package com.nativegetdeviceinfo
import android.content.Context
import android.os.BatteryManager
import android.os.Build
import android.os.SystemClock
import android.net.wifi.WifiManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.text.format.Formatter
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.nativegetdeviceinfo.NativeGetDeviceInfoSpec
class NativeGetDeviceInfoModule(reactContext: ReactApplicationContext) : NativeGetDeviceInfoSpec(reactContext) {
companion object {
const val NAME = "NativeGetDeviceInfo"
}
// 안드로이드 버전 가져오기
override fun getAndroidVersion(promise: Promise) {
val androidVersion = Build.VERSION.RELEASE
promise.resolve("안드로이드 $androidVersion")
}
// 디바이스 모델을 가져오는 메소드
override fun getDeviceModel(promise: Promise) {
val manufacturer = Build.MANUFACTURER
val model = Build.MODEL
promise.resolve("$manufacturer $model")
}
// 디바이스 IP 주소를 가져오는 메소드
override fun getDeviceIpAddress(promise: Promise) {
try {
val connectivityManager = getReactApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(network)
val ipAddress = when {
networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> {
val wifiManager = getReactApplicationContext().getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiInfo = wifiManager.connectionInfo
Formatter.formatIpAddress(wifiInfo.ipAddress)
}
networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "셀룰러 네트워크 IP를 확인할 수 없습니다"
else -> "알 수 없음"
}
promise.resolve(ipAddress)
} catch (e: Exception) {
promise.reject("IP_ERROR", "IP 주소를 가져올 수 없습니다: ${e.message}")
}
}
// 기기의 가동시간을 가져오는 메소드
override fun getDeviceUptime(promise: Promise) {
val uptimeMillis = SystemClock.uptimeMillis() // 기기 가동 시간 (밀리초)
val uptimeSeconds = uptimeMillis / 1000
val hours = uptimeSeconds / 3600
val minutes = (uptimeSeconds % 3600) / 60
val seconds = uptimeSeconds % 60
promise.resolve("$hours 시간, $minutes 분, $seconds 초")
}
// 배터리 충전 상태를 가져오는 메소드
override fun getBatteryStatus(promise: Promise) {
try {
val batteryManager = getReactApplicationContext().getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val isCharging = batteryManager.isCharging
promise.resolve(if (isCharging) "충전 중" else "충전 중 아님")
} catch (e: Exception) {
promise.reject("BATTERY_STATUS_ERROR", "배터리 상태를 가져올 수 없습니다: ${e.message}")
}
}
// 배터리 수준을 가져오는 메소드
override fun getBatteryLevel(promise: Promise) {
try {
val batteryManager = getReactApplicationContext().getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val level = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
promise.resolve("$level%")
} catch (e: Exception) {
promise.reject("BATTERY_LEVEL_ERROR", "배터리 잔량을 가져올 수 없습니다: ${e.message}")
}
}
}
NativeGetDeviceInfoModule 패키징하기
다음 단계로는 NativeGetDeviceInfoModule
을 패키징하고, React Native 런타임에 등록하기 위해 Base Native Package로 감싸야 함.
// NativeGetDeviceInfoPackage 클래스는 NativeGetDeviceInfoModule을 Turbo 네이티브 모듈로 통합하기 위한 커스텀 React Native 패키지를 정의
package com.nativegetdeviceinfo
import com.facebook.react.TurboReactPackage
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 NativeGetDeviceInfoPackage : TurboReactPackage() {
// getModule 메서드는 요청된 모듈 이름이 NativeGetDeviceInfoModule.NAME과 일치하는지 확인하고, 일치하면 모듈의 인스턴스를 반환하며, 그렇지 않으면 null을 반환합니다.
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
if (name == NativeGetDeviceInfoModule.NAME) {
NativeGetDeviceInfoModule(reactContext)
} else {
null
}
// getReactModuleInfoProvider 메서드는 ReactModuleInfo 객체를 생성하여 모듈에 대한 메타데이터를 제공합니다. 이를 통해 모듈이 React Native 프레임워크에 올바르게 등록되고 인식되도록 보장합니다.
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
NativeGetDeviceInfoModule.NAME to ReactModuleInfo(
_name = NativeGetDeviceInfoModule.NAME,
_className = NativeGetDeviceInfoModule.NAME,
_canOverrideExistingModule = false,
_needsEagerInit = false,
isCxxModule = false,
isTurboModule = true
)
)
}
}
React Native에 패키지 등록하기
이제 React Native가 어떻게 이 패키지를 찾을 수 있을지 알려줘야 함. 이를 위해 android/app/src/main/java/com/turbomoduleexample/MainApplication.kt
파일에서 NativeGetDeviceInfoPackage
를 import함.
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// 자동으로 연결되지 않는 패키지는 여기서 수동으로 추가할 수 있음.
// 내가 만든 패키지를 등록해줌
add(NativeGetDeviceInfoPackage())
}
Turbo Native Module 구현 테스트해보기
AndroidManifest.xml
파일을 업데이트하여 네트워크 및 Wi-Fi 상태 접근에 대한 권한을 허용하고, getIPAddress
메서드가 정상적으로 작동할 수 있도록 필요한 권한을 추가하는 것임.
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
## React Native 코드에서 Turbo Native Module 사용하기
이제 React Native 코드에서 `NativeGetDeviceInfo` 명세에 정의된 메서드를 호출할 수 있음.
```typescript
import React, { useState, useEffect } from "react";
import { View, Text, Button, StyleSheet } from "react-native";
import NativeGetDeviceInfo from "./specs/NativeGetDeviceInfo";
const App = () => {
const [value, setValue] = useState<string | null>("");
const getBatteryLevel = async () => {
const data = await NativeGetDeviceInfo?.getBatteryLevel();
setValue(data ?? "");
};
const getDeviceModel = async () => {
const data = await NativeGetDeviceInfo?.getDeviceModel();
setValue(data ?? "");
};
const getDeviceIpAddress = async () => {
const data = await NativeGetDeviceInfo?.getDeviceIpAddress();
setValue(data ?? "");
};
const getDeviceUptime = async () => {
const data = await NativeGetDeviceInfo?.getDeviceUptime();
setValue(data ?? "");
};
const getAndroidVersion = async () => {
const data = await NativeGetDeviceInfo?.getAndroidVersion();
setValue(data ?? "");
};
useEffect(() => {
getBatteryLevel();
}, []);
return (
<View style={styles.container}>
<Text style={styles.title}>{value}</Text>
<View style={styles.buttonContainer}>
<Button title={"배터리 잔량 확인"} onPress={getBatteryLevel} />
</View>
<View style={styles.buttonContainer}>
<Button title={"기기 모델 확인"} onPress={getDeviceModel} />
</View>
<View style={styles.buttonContainer}>
<Button title={"IP 주소 확인"} onPress={getDeviceIpAddress} />
</View>
<View style={styles.buttonContainer}>
<Button title={"기기 가동 시간 확인"} onPress={getDeviceUptime} />
</View>
<View style={styles.buttonContainer}>
<Button title={"안드로이드 버전 확인"} onPress={getAndroidVersion} />
</View>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#f5f5f5" },
title: { fontSize: 24, fontWeight: "bold", marginBottom: 20 },
taskTitle: { fontSize: 18 },
buttonContainer: { marginBottom: 20 },
});
export default App;
결과 확인