
Native Modules
Bridge
React Native는 Main Thread → JavaScript Thread → Shadow Thread → Native side의 일련의 실행과정으로 실행이 된다.
Native Module 생성
JS에서 Native Module 호출
// StopWatchModule.java
public class StopWatchModule extends ReactContextBaseJavaModule {
private static ReactApplicationContext reactContext;
public StopWatchModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "StopWatchModule";
}
@ReactMethod
public void getNumber(int appWidgetId) {
Log.d("StopWatchModule", "This is a simple log from Native Module!");
}
}
getNumber을 호출하면 Log가 찍히는 함수를 간단하게 만들었다.
@ReactMethod를 사용해야 JS에서 접근이 가능하다.
import { NativeModules } from "react-native";
const { StopWatchModule } = NativeModules;
const App = () => {
const [widgetData, setWidgetData] = React.useState(null);
const getWidgetData = async () => {
const widgetData = await StopWatchModule.getNumber(1);
setWidgetData(widgetData);
};
// StopWatchPackage.java
public class StopWatchPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new StopWatchModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
React Native에 등록할 패키지이며, reactContext를 인자로 받아와 모듈 인스턴스에 등록한다.
createViewManagers는 지금 필요없으므로, 빈 값을 넘겨준다.
// MainApplication.java
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new StopWatchPackage());
return packages;
}
만든 패키지를 넘겨주는 것을 끝으로 Native Module이 연결완료되었다.

버튼클릭하면?

로그가 잘 나온다. 연결은 되었다.
appWidgetId는 이제부터 그냥 id라고 말하겠다.
id는 위젯의 인스턴스를 식별하기 위해 시스템에 의해 자동으로 생성된다. 따라서 위젯이 추가되거나 제거되면 id는 변경된다. 이런 상황에서 구별할 수 있는 방법을 생각해봤다.
**Event Emitte 생성**
public static void emitDeviceEvent(String eventName, @Nullable WritableMap eventData) {
if (StopWatchModule.reactContext != null) {
StopWatchModule.reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, eventData);
}
}
이벤트를 전달하기위한 메서드. StopWatchModule에 작성해준다.
onUpdate 메서드 수정
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
WritableMap map = Arguments.createMap();
map.putInt("appWidgetId", appWidgetId);
StopWatchModule.emitDeviceEvent("onAppWidgetUpdate", map);
}
}
위젯이 업데이트될 때마다, 다시말해 위젯이 홈 화면에 추가 될 때, id를 React Native에 전달하기위한 메서드. StopWatch.java에 작성한다.
React Native 코드 작성
import React, { useEffect } from "react";
import { View, Text, Button } from "react-native";
import { NativeModules, DeviceEventEmitter } from "react-native";
import * as SecureStore from "expo-secure-store";
const { StopWatchModule } = NativeModules;
const WidgetText = () => {
const [widgetData, setWidgetData] = React.useState<String | null>(null);
const [appWidgetId, setAppWidgetId] = React.useState<String | null>(null);
useEffect(() => {
const fetchStoredAppWidgetId = async () => {
const storedId = await SecureStore.getItemAsync("appWidgetId");
if (storedId) {
setAppWidgetId(storedId);
}
};
fetchStoredAppWidgetId();
const listener = DeviceEventEmitter.addListener(
"onAppWidgetUpdate",
async (data) => {
setAppWidgetId(data.appWidgetId);
await SecureStore.setItemAsync(
"appWidgetId",
data.appWidgetId.toString(),
);
},
);
return () => {
if (listener) {
listener.remove();
}
};
}, []);
const getWidgetData = async () => {
if (appWidgetId) {
const widgetData = await StopWatchModule.getNumber(Number(appWidgetId));
console.warn(
"appWidgetId : ",
appWidgetId + " widgetData : ",
widgetData,
);
setWidgetData(widgetData);
} else {
console.warn("AppWidgetId is not yet available.");
}
};
return (
<View>
<Text>{widgetData}</Text>
<Button
title="get widget data"
onPress={() => getWidgetData()}
disabled={!appWidgetId}
/>
<Text>{appWidgetId}</Text>
</View>
);
};
export default WidgetText;
React 코드 작성하는게 이렇게 반가울줄은 몰랐다..
SecureStore는 react app이 종료되었을때도 id를 가지고있어야하기 때문에 id를 저장해두는 용도이다.
useEffect로 store에 id있으면 불러와주고, addListener로 위젯에 이벤트가 발생한다면 그 id값을 상태에도 저장하고 storage에도 저장한다.
끝으로 불필요한 리스너를 메모리에서 제거해 누수를 방지한다.
이로서 정상적으로 id를 받아오고, 앱을 종료해도 id를 가지고있는 로직이 완성되었다.

위젯에서 버튼들을 눌러 69의 숫자의 상태이다.

앱 켜서 위젯 데이터를 받아왔더니, 69의 값을 정상적으로 받아오는 걸 확인했다.
끗