[ReactNative] Android Native Module 만들기
ReactNative 를 시작하면, 해당 플랫폼의 기본 문법 및 제공되는 컴포넌트 및 사용법에 대해서 익히는게 먼저일 것이다.
하지만 내가 맡은 프로젝트는 기존에 잘 짜여진 Android / IOS Native 를 완전한 ReactNative 로 대체를 해야하는 것이었다.
백지부터 다시 짜고 싶은게 당연히 편하겠지만, 현실적인 일정도 있고 순차적으로 전환해나가기 위해서 Native Module을 사용할 수 밖에 없다.
그래서 기존 Native 코드를 재사용할 수 있는 Module로 만드는 법에 대해서 작성해보려고 한다.
아래 ReactNative 공식문서를 참고했다.
https://reactnative.dev/docs/native-modules-intro
Native Modules Intro · React Native
Sometimes a React Native app needs to access a native platform API that is not available by default in JavaScript, for example the native APIs to access Apple or Google Pay. Maybe you want to reuse some existing Objective-C, Swift, Java or C++ libraries wi
reactnative.dev
문서에 보면 Native Module을 셋업하는 방법이 2가지가 있다.
1. 기존 Android/IOS Native 코드에 직접 구현하기
2. NPM 패키지를 사용하기
여기선 Android/IOS Native 코드에 직접 구현하는 방법을 이용해 보려고 한다.
Android Native Module 만들기
해당 섹션은 아래 페이지를 참고한 것이다.
https://reactnative.dev/docs/native-modules-android
Android Native Modules · React Native
Welcome to Native Modules for Android. Please start by reading the Native Modules Intro for an intro to what native modules are.
reactnative.dev
우선 Android Studio 를 열고 생성한 react-native 프로젝트 내의 android 폴더를 선택하여 프로젝트 등록한다.
Android Studio로 빌드를 하는 것은 아니고, Native 코드 수정 및 문법 체크 등을 위한 것으로 봐야할 것 같다.
일단 내가 만든 프로젝트명은 AwesomeProject2 이기 때문에 패키지 명이 com.awesomeproject2 가 된다.
가이드 페이지에 나온 것처럼 CalendarModule을 추가하고, 해당 Module에서 생성한 api를 React Native 단에서 호출해보도록 하자
추가로 여기서 삽질일기를 하나 작성 하려고 한다.
react native project를 생성하고 만들어진 android 폴더에 있는 native code들은 java로 되어있었다.
하지만 습관적으로 Native Module을 kotlin으로 만들고 테스트를 진행했다.
하지만 계속해서 Can't find symbol 에러가 발생을 하는 것이다.
별의별 짓을 다해도 에러가 나길래, 혹시나 싶어서 전부 java코드로 변경을 해봤더니 그제서야 정상적으로 빌드가 되었다.
왜 그런지는 알 수 없지만... 이로 인해 예제는 java로 작성을 하려고 한다.
프로젝트를 점차 진행하다보면 원인을 파악할 수도?
com.awesomeproject2 패키지 내에 CalendarModule.java를 추가한다.
package com.awesomeproject2; // replace com.your-app-name with your app’s name
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;
public class CalendarModule extends ReactContextBaseJavaModule {
CalendarModule(ReactApplicationContext context) {
super(context);
}
}
위와 같이 작성하면 클래스명 선언하는 부분에서 에러가 발생할 것이다.
확인해보면 상속해야할 메서드가 하나 있다고 나온다.
// add to CalendarModule.java
@Override
public String getName() {
return "CalendarModule";
}
위와 같이 getName을 상속받아 모듈명을 리턴해준다.
다음으로 ReactNative에서 호출하고자하는 메서드를 하나 정의해본다.
로그도 찍고, 토스트도 하나 띄워보았다.
import android.util.Log;
@ReactMethod
public void createCalendarEvent(String name, String location) {
Log.d("CalendarModule", "Create event called with name: " + name
+ " and location: " + location);
Toast.makeText(getReactApplicationContext(), "HELLO", Toast.LENGTH_SHORT).show();
}
React Native에서 호출하고자하는 메서드에는 @ReactMethod라는 어노테이션을 붙여준다.
여기에 추가적으로 isBlockingSynchronousMethod라는 옵션을 설정해줄수 있다.
@ReactMethod(isBlockingSynchronousMethod = true)
public void createCalendarEvent(String name, String location) {
Log.d("CalendarModule", "Create event called with name: " + name
+ " and location: " + location);
Toast.makeText(getReactApplicationContext(), "HELLO", Toast.LENGTH_SHORT).show();
}
해당 옵션은 해당 method가 Synchronous 하게 동작하도록 만들어주는 메서드이다.
아마 java에서 보자면 method 앞에 synchronized 를 붙여주는것과 같은 기능을 할 것으로 보인다.
하지만 해당 옵션은 추천하지 않는다고 한다. 퍼포먼수 문제도 있고, Google Chrome 디버거를 사용할 수도 없기 때문이라고 한다.
때문에 일단 그냥 안쓴다고 생각하고 넘어가기로 했다. (나중엔 필요할 수도 있겠지..)
다음은 Android에 특화된 부분인데, Module을 등록하는 것이다.
React Native는 초기화 단계에서 전체 패키지들에 대해서, 각각의 ReactPackage들을 루프를 돌면서 등록한다.
React Native는 등록할 기본 모듈 목록을 가져오기 위해 ReactPackage에서 createNativeModules() 메서드를 호출한다.
Android에서는 createNativeModule에서 인스턴스화 되지 않고 리턴이 될 경우엔 JavaScript에서 사용할 수 없다.
우선 ReactPackage라는 클래스를 상속받아 새로운 클래스를 만들어준다.
package com.awesomeproject2; // replace your-app-name with your app’s name
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MyAppPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new CalendarModule(reactContext));
return modules;
}
}
위에서 언급한것처럼, createNativeModules에 내가 만든 모듈을 객체화 시킨 뒤에 add로 추가해주면 된다.
Native Module들은 앱이 시작될때, 모든 Module들을 초기화 하게되는데 이로 인해 오버헤드가 발생하여 진입 시간에 영향을 줄 수 있다.
이를 대체하기 위해서 TurboReactPackage를 사용 할 수 있는데, 사용법이 보다 복잡하다. 어차피 프로젝트가 커지면 Turbo를 써야 하기 때문에 이부분은 별도로 작성을 해보고자 한다.
다음으론 MyAppPackage를 추가해주는 작업을 해줘야한다.
react native 프로젝트 생성 시, 자동으로 생성되는 클래스인 MainApplication.java열어보면 다음과 같이 getPacakges()라는 메서드가 있는데, 여기에 MyAppPackage를 추가해준다.
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// below MyAppPackage is added to the list of packages returned
packages.add(new MyAppPackage());
return packages;
}
여기까지하면 기본적으로 Native Module을 사용할 준비가 끝난 상태이다.
테스트를 해보기 위해 javascript 코드를 작성해보자
NewModuleButton.js라는 파일을 하나 생성하고 다음과 같이 생성했다.
import React from 'react';
import {Button, NativeModules} from 'react-native';
const {CalendarModule} = NativeModules;
const NewModuleButton = () => {
const onPress = () => {
console.log('We will invoke the native module here!');
CalendarModule.createCalendarEvent('testName', 'testLocation');
};
return (
<Button
title="Click to invoke your native module!"
color="#841584"
onPress={onPress}
/>
);
};
export default NewModuleButton;
import 에서 NativeModules를 가져왔고, CalendarModule을 사용하기 위해서, 아래와 같이 코드를 추가해줬다.
const {CalendarModule} = NativeModules;
그 뒤 버튼 onPress 에서 Native Module에서 정의한 메서드를 호출한다.
const onPress = () => {
CalendarModule.createCalendarEvent('testName', 'testLocation');
};
그리고 App.tsx 에 생성한 Button을 추가해준다.
function App(): JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
return (
<SafeAreaView style={backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={backgroundStyle}>
<Header />
<View
style={{
backgroundColor: isDarkMode ? Colors.black : Colors.white,
}}>
...
<NewModuleButton />
</View>
</ScrollView>
</SafeAreaView>
);
}
이제 두근대는 빌드의 시간...
아래와 같이 빌드를 진행한다.
npx react-native run-android
정상적으로 빌드가 되었다면 에뮬레이터에 아래의 react native app이 실행되는 것을 볼 수 있다.
보라색 버튼이 임의로 추가한 버튼이고, 눌렀을 때 정상적으로 토스트가 뜬다면 정상적으로 Native Module이 연결된 것이다.
한가지 아쉬운 점은 Javascript 에서 native module이 자동완성이 되면 좋을텐데 아직 그런거까진 안되는 것 같다.