ReactNative

[React-Native] ReactNative 에서 Android Native 함수 호출

졸려질려 2022. 11. 17. 13:56
반응형

 개발 중이던 Android 앱의 특정 기능들을 다른 ReactNative 앱과 결합할 일이 생겼다. 그래서 가장 먼저 Android 앱에서 필요한 기능들을 하나의 라이브러리 모듈로 통합을 해두었고, ReactNative 앱에서 생성된 Android Project 에 모듈을 Import 해주었다. 해당 모듈에는 필요한 기능들을 모두 사용하는 Activity 까지 넣어두었기 때문에, ReactNative 앱에서 Activity 를 호출해주기만 하면 됐다. 다만, 본 글에서는 ReactNative 에서 Android Project 를 생성하고, Android Project 내에서 ReactNative 에서 사용할 수 있는 함수를 어떻게 만들고, 호출하는지에 대해 정리한다.


1. Create React Native Project

 본 글에서는 Expo 를 사용하여 React Native 프로젝트를 생성했다. Expo 는 React Native 앱을 좀 더 쉽고, 빠르게 만들 수 있도록 도와주는 프레임워크라 보면 되는데, 본질은 React Native 로 동일하기 때문에 어떤 프레임워크를 쓰더라도 똑같이 Android Native 모듈을 붙일 수 있다. Expo 나 React Native CLI 를 통해 ReactNative Project 를 생성해주도록 한다. 만약, Expo CLI 를 사용하고 싶다면, 다음 명령어를 통해 Expo CLI 를 설치할 수 있다.

npm install -g expo-cli

expo 로 RN Project 생성

ReactNative CLI 나 Expo 를 통해 Project 를 생성하면 에디터로 해당 Project 폴더를 열어준다.


2. Add Button for Android Method

 Android 프로젝트를 만들기에 앞서서, Android 앱 내의 메소드를 호출 할 버튼을 만들어준다. React Native 에서 버튼은 "Button" 으로 생성한다. 앞의 "B" 는 무조건 대문자로 와야한다. (소문자로 할 경우 HTML 의 button 으로 인식한다.)

export default function App() {
  return (
    <View style={styles.container}>
      ....
      <Button
        title = "GoToAndroid"
        onPress = {() => {
          console.log("Clicked");
        }}
      />
    </View>
  );
}

 우선, Android 에서 호출할 메소드를 아직 정의하지 않았기 때문에, 버튼을 클릭했을 때 간단한 로그가 뜨도록 한다.


3. Create Android Project from RN

 이제 RN Project 에서 Android Project 를 만들어준다. 현재 Expo 에서는 ExpoKit 을 사용하여 Android Project 를 Eject 할 수 있게 해주는데, 곧 Deprecated 된다는 경고문이 붙어있다. (문서 링크) 그래도 지금은 되는 것 같으니, ExpoKit 을 사용해서 Android Project 를 Eject 해준다.

 먼저, "app.json" 파일에서 "name", "version", "icon" 을 확인 및 커스텀 해주고, "android" key 의 json 안에는 "package" 와 "versionCode" 를 추가 및 명시해준다. 완료한 모습은 다음과 같다.

{
  "expo": {
    "name": "Con2And",
    "slug": "Con2And",
    "version": "1.0.0",
    "icon": "./assets/icon.png",
    ....
    "android": {
      "package": "com.emotionwave.conand",
      "versionCode": 1,
      ....
    }
    ....
  }
}

 "name", "package", "version", "versionCode" 는 Android Project 에서 쓰고 싶은 값으로 넣어주면 된다. "icon" 도 원하는 파일로 넣어도 된다. "app.json" 파일 수정이 끝나면, Eject 를 해주도록 한다.

expo eject

or

npx expo prebuild

위 명령어를 사용하면, ios 와 android 폴더가 생성되고 그 안에 각 플랫폼에 맞는 Project 가 생성된다.

 이제 생성된 android 폴더를 Android Studio 로 열어서, RN-Android 연동 간에 Android 쪽에서 구현해야될 부분에 대해 먼저 기술한다.


4. ReactContextBaseJavaModule

 우선, Android Code 를 ReactNative 에서 인식할 수 있도록 중간에 Wrapper Class 를 만들어줘야한다. 본 섹션의 제목에 있는 "ReactContextBaseJavaModule" 을 상속 받아서, WrapperModule 을 생성한다. 생성 위치는 "[Project_Name]/android/app/src/main/java/[APP_Package]" 이다. 간단히 보면, "MainActivity" 와 "MainApplication" 이 생성되어 있는 위치이다.

// src/main/java/com/emotionwave/conand/ReactWrapperModule.java

package com.emotionwave.conand;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;

public class ReactWrapperModule extends ReactContextBaseJavaModule {
    private final ReactApplicationContext reactContext;
    
    ReactWrapperModule(ReactApplicationContext context) {
        super(context);
        this.reactContext = context;
    }

    @NonNull
    @Override
    public String getName() {
        return "ReactWrapperModule";
    }
}

 ReactContextBaseJavaModule 을 상속 받으면, getName() 메소드를 오버라이드 해야한다. 해당 메소드는 ReactWrapperModule 클래스의 이름을 String 형으로 반환하도록 한다. React 는 ReactWrapperModule 의 getName() 메소드를 통해, 해당 모듈의 이름을 파악하고 접근한다. getName() 외에 생성자를 따로 넣어준 것을 확인 할 수 있다. ReactNative 와 연동할 Android Java/Kotlin 코드에서도 Context 가 필요한 경우가 있을 것이다. 하지만, Android 에서 사용하던 Context 를 사용하지 않고, ReactApplicationContext 를 사용해줘야한다. 전역 변수로 ReactApplicationContext 값을 설정하여, 다른 메소드에서 사용할 수 있도록 한다.

 마지막으로, React 에서 호출할 Android 메소드를 하나 추가한다. 메소드의 내용은 전달 받은 인자값을 Toast 로 띄우는 걸로 한다. React 가 호출할 수 있는 Android 의 메소드를 추가하려면, @ReactMethod 어노테이션을 추가해줘야한다.

// src/main/java/com/emotionwave/conand/ReactWrapperModule.java

public class ReactWrapperModule extends ReactContextBaseJavaModule {
    ...
    @ReactMethod
    public void showToast(String message, int number) {
        Toast.makeText(reactContext, "MSG: " + message + " / " + number, Toast.LENGTH_SHORT).show();
    }
    ...
}

 Toast 에 사용하는 Context 값은 기존에 Android 에서 사용하던 Context 가 아니라 ReactApplicationContext 의 값이 들어가줘야한다. 그리고, showToast() 메소드에서 전달받은 인자값들을 Toast 메시지에 모두 출력하도록 문자열을 구성한다. 이제 React 에서 호출해주는 것만 남았을 것 같지만, 한 단계가 더 남았다.


5. Register the Module (Android Specific)

 RN-Android 연동 간에는 Android 에서 만든 WrapperModule 을 등록하는 과정이 필요하다. 처음에 ReactNative 앱이 실행될 때, 앱과 연계된 패키지들을 모두 훑어보게 된다. 그 때, Android Native 로 만든 Package 가 인식되어야, 그와 관련된 메소드들을 실행시킬 수 있다. 해당 등록 과정을 거치지 않으면, React 코드에서 ReactWrapperModule 을 불러오고 싶어도 하루종일 null 만 반환된다.

  먼저, WrapperModule 과 React 를 연결해 줄 Package 를 먼저 구현해야한다. ReactPackage 를 implement 하여 클래스를 구성한다. Package 를 생성하는 위치는 위에서 만든 ReactWrapperModule 의 위치와 같다.

// src/main/java/com/emotionwave/conand/ReactWrapperPackage.java

import androidx.annotation.NonNull;

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.List;

public class ReactWrapperPackage implements ReactPackage {
    @NonNull
    @Override
    public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
        return null;
    }

    @NonNull
    @Override
    public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
        return null;
    }
}

 ReactPackage 를 implements 해주면, 두 메소드를 Override 해야한다. 하나는 NativeModule 들을 생성하는것에 관련된 메소드이고, 다른 하나는 ViewManager 들을 생성하는 것과 관련된 것으로 보인다. ViewManager 는 따로 만들어둔 것이 없으니, createViewManagers() 메소드는 빈 List 가 반환되도록 한다. 반면에 createNativeModules() 메소드는 이미 등록하려고 만들어둔 것이 있으니, 해당 NativeModule 을 넣은 List 가 반환되도록 하면 될 것이다. 구현하면 다음과 같다.

// src/main/java/com/emotionwave/conand/ReactWrapperPackage.java

public class ReactWrapperPackage implements ReactPackage {
    @NonNull
    @Override
    public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new ReactWrapperModule(reactApplicationContext));
        
        return modules;
    }

    @NonNull
    @Override
    public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
        return Collections.emptyList();
    }
}

 위에서 언급했듯이, ReactNative 앱이 최초로 실행되어 초기화 과정을 진행하면, 앱과 관련된 Package 들을 순회하여 확인한다. 앱이 Package 목록 확인 과정을 거칠 때, 방금 만든 ReactWrapperPackage 가 확인될 수 있도록 순회하는 앱 관련 Package 목록에 ReactWrapperPackage 를 넣어준다. 이 작업은 새로 클래스를 만드는 것이 아니라, ReactNative 의 Android Project 를 추출할 때 자동으로 만들어진 "MainApplication.java" 파일에서 수정한다. "MainApplication.java" 파일을 보면 "getPackages()" 메소드가 존재한다. 해당 메소드를 아래와 같이 변경해준다.

// src/main/java/com/emotionwave/conand/MainApplication.java

public class MainApplication extends Application implements ReactApplication {
    private final ReactNativeHost mReactNativeHost = new ReactNativeHostWrapper(this,
            new ReactNativeHost(this) {
                ....
                @Override
                protected List<ReactPackage> getPackages() {
                    @SuppressWarnings("UnnecessaryLocalVariable")
                    List<ReactPackage> packages = new PackageList(this).getPackages();

                    // Add My Wrapper Package
                    packages.add(new ReactWrapperPackage());

                    return packages;
                }
                ....
            });
    ....
}

 ReactNativeHost 변수를 초기화하는 코드 속에 오버라이드 되어있는 "getPackages()" 메소드의 내용을 수정하였다. 반환하는 packages 리스트 안에 방금 만든 ReactWrapperPackage() 변수를 넣어서 같이 반환되도록 한 것이다. 이제 React 코드에서 ReactPackage 를 인식하고, ReactPackage 안에 등록했던 ReactWrapperModule 을 인식하게 되어, "showToast()" 메소드를 호출할 수 있다.


6. Call Method from ReactNative

 본 글의 위에서 "2. Add Button for Android Method" 파트에서 간단하게 구현했던 Button 으로 돌아간다. 해당 Button 을 클릭했을 때, Android 앱에서 Toast 가 뜨면 본 글의 목표는 성공한 것이다. React 코드에서 Android 의 메소드를 호출하려면, 가장 먼저 Android 에서 만들었던 ReactWrapperModule 을 가져와야할 것이다. 'react-native' 패키지에서 "NativeModules" 라는 객체를 통해 ReactWrapperModule 을 가져올 수 있다.

      <Button
        title = "GoToAndroid"
        onPress = {() => {
          const { ReactWrapperModule } = NativeModules;
          console.log(ReactWrapperModule);
        }}
      />

 위 코드에서 주의할 점은, JS 에서 NativeModule 에 대한 변수를 선언할 때, Android 에서 만든 WrapperModule 이름이 JS 에서 선언할 변수명과 같아야한다. 위 코드처럼 Android 에서 ReactWrapperModule 클래스를 만들고, JS 에서도 ReactWrapperModule 이라는 변수로 선언해야, Android 의 WrapperModule 을 가져올 수 있다. 로그를 찍어보면 다음과 같이 나타난다.

 로그 안에 "showToast" 라는 문자열이 나오는 것으로 보아, ReactWrapperModule 안에 "showToast()" 메소드가 있음을 인식한 것으로 보인다. 이제 showToast() 메소드를 호출하는 코드는 다음과 같다.

      <Button
        title = "GoToAndroid"
        onPress = {() => {
          const { ReactWrapperModule } = NativeModules;
          ReactWrapperModule.showToast("Hello Android", 3344);
        }}
      />

 JS 의 변수로 선언한 ReactWrapperModule 를 통해 showToast() 메소드를 호출했다. showToast() 메소드는 파라미터로 String 과 int 값을 받기 때문에, 두 값을 메소드 호출에 넣어주었다. 그리고, 해당 버튼을 클릭하면 다음과 같이 나타나는 것을 볼 수 있다.

Android Emulator 에서 Toast 가 나타난 모습


 이렇게 ReactNative 와 Android Native 를 결합하여, ReactNative 에서 Android 메소드를 호출해보았다. 다음 글에서는 ReactNativie 프로젝트에서 추출한 Android Project 에서 Activity 를 생성하고, React 로 호출한 메소드로 Activity 이동이 가능한지 살펴본다. 그리고, React Native 에서 Android 를 호출하는 것이 아니라, Android 에서 React Native 로 값을 전달하는 것은 어떻게 진행되는지에 대해 정리한다.

반응형