[React Native] Android(java) 앱 통합하기(합치기)

react native는 처음부터 새로운 모바일 앱을 만들때 훌륭한 도구입니다. 하지만 기존 네이티브 애플리케이션도 단일 뷰나 사용자 흐름을 추가하는 데도 잘 동작합니다. 몇 가지 단계만 새로운 react native 기반 기능, 화면, 뷰 등을 추가할 수 있습니다.

핵심 개념

react native 구성 요소를 android 애플리케이션에 통합하기 위한 핵심 사항은 다음과 같습니다.

  • react native 의존성 및 디렉토리 구조 설정
  • javascript로 react native 구성 요소 개발
  • android 앱에 ReactRootView 추가
  • react native 서버 시작 및 네이티브 애플리케이션 실행
  • 애플리케이션의 react native 측면이 예상대로 작동하는지 확인

전제 조건

react native 앱을 빌드하기 위해 개발 환경을 설정하는 가이드를 따라 개발 환경을 구성합니다.

1. 디렉토리 구조 설정

원활한 경험을 위해 통합된 react native 프로젝트를 위한 새 폴더를 만들고 기존 android 프로젝트를 /android 하위폴더에 복사합니다.

2.javascript 의존성 설치

프로젝트의 루트 디렉토리로 이동하여 다음 내용을 포함한 새 package.json 파일을 만듭니다.

{
 "name":"reactnativeApp",
 "version":"0.0.1",
 "private":true,
 "scripts": {
  "start": "react-native start"
 }
}

그런 다음, react 및 react-native 패키지를 설치합니다. 터미널이나 명령 프롬프트를 열고 package.json 파일이 있는 디렉토리로 이동한 후 다음 명령을 실행합니다.

npm install react-native

이 명령은 다음과 같은 메시지를 출력합니다.

warning 'react-native@0.70.5' has unmet peer dependency "react@18.1.0"

React도 설치해야 함을 의미합니다.

npm install react@version_printed_above

설치 과정에서 새로운 /node-modules 폴더가 생성됩니다. 이 폴더에는 프로젝트를 빌드하는 데 필요한 모든 javascript 의존성이 저장됩니다.

.gitignore 파일에 node_modules/를 추가합니다.

react native를 앱에 추가하기

gradle 구성

react native는 react native gradle 플러그인을 사용하여 의존성 및 프로젝트 설정을 구성합니다. 먼저 android밑에 setting.gradle 파일을 편집하여 다음 줄을 추가합니다.

includeBuild('../node_modules/@react-native/gradle-plugin')

그런 다음 최상위 build.gradle 파일을 열고 다음 줄을 추가합니다.

buildscript {
 repositories {
  google()
  mavenCentral()
 }
 dependencies {
  classpath("com.android.tools.build:gradle:7.3.1")
  classpath("com.facebook.react:react-native-gradle-plugin")
 }
}

이 플러그인이 프로젝트 내에서 사용 가능하도록 합니다. 마지막으로 앱의 build.gradle 파일에 다음 줄을 추가합니다.(app 폴더 내의 다른 build.gradle 파일입니다.)

apply plugin: "com.android.application"
apply plugin: "com.facebook.react"

repositories {
 mavenCentral()
}

dependencies {
 implementation("com.facebook.react:react-android")
 implementation("com.facebook.react:hermes-android")
}

이러한 의존성은 mavenCentral()에서 사용 가능하므로 repositories{} 블록에 정의되어 있는지 확인합니다.

이러한 implementation 의존성의 버전을 명시적으로 지정하지 않는 이유는 React Native Gradle 플러그인이 이를 관리하기 때문입니다. React native gradle 플러그인을 사용하지 않는 경우, 버전을 수동으로 지정해야 합니다.

네이티브 모듈 자동 연결 활성화

자동 연결 기능을 사용하려면 몇 가지 장소에 적용해야 합니다. 먼저 settings.gradle 파일에 다음 항목을 추가합니다.

apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)

그런 다음, app/build.gradle의 맨 아래에 다음 항목을 추가합니다.

apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

권한 구성

다음으로 AndroidManifest.xml에 인터넷 권한이 있는지 확인합니다.

<uses-permission android:name="android.permission.INTERNET" />

개발 모드에서 DevSettingActivity에 접근하려면 AndroidManifest.xml에 다음 항목을 추가합니다.

<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

이 권한은 개발 모드에서 javascript를 다시 로드할 때만 사용되므로, 필요에 따라 릴리스 빌드에서는 제거할 수 있습니다.

클리어 텍스트 트래픽(API 레벨 28이상)

Android 9(API level 28)부터는 클리어 텍스트 트래픽이 기본적으로 비활성화되어 있어 Metro 번들러에 연결할 수 없습니다. 디버그 필드에서 클리어 텍스트 트래픽을 허용하려면 아래 변경 사항을 적용합니다.

디버그 AndroidManifest.xml에 usesCleartextTraffic 옵션 적용

<application
 android:usesCleartextTraffic="true" tools:targetApi="28"
>

</application>

릴리스 빌드에는 필요하지 않습니다.

코드 통합

이제 네이티브 Android 애플리케이션을 수정하여 React Native를 통합합니다.

React Native 구성 요소

새 High Score 화면을 애플리케이션에 통합하기 위해 실제 react native 코드를 작성합니다.

1. index.js 파일 생성

먼저 react native 프로젝트의 루트에 빈 index.js 파일을 만듭니다. index.js는 react native 애플리케이션의 시작점이며 항상 필요합니다. 다른 파일을 요구하거나 모든 코드를 포함할 수 있습니다. 여기서는 모든 코드를 index.js에 넣겠습니다.

2. React Native 코드 추가

index.js 파일에서 코드를 생성합니다.

import React from 'react';
import {AppRegistry, StyleSheet, Text, View} from 'react-native';

const HelloWorld = () => {
  return (
    <View style={styles.container}>
      <Text style={styles.hello}>Hello, World</Text>
    </View>
  );
};
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
  hello: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});

AppRegistry.registerComponent(
  'MyReactNativeApp',
  () => HelloWorld,
);

3. 개발 오류 오버레이를 위한 권한 구성

앱이 Android API 레벨 23이상을 대상으로 하는 경우, 개발 빌드에서 android.permission.SYSTEM.ALERT.WINDOW 권한이 활성화되어 있는지 확인합니다. 이는 Settings.canDrawOverlays(this)로 확인할 수 있습니다. 네이티브 개발 오류는 다른 모든 창 위에 표시되어야 하므로, API 레벨 23에서 도입된 새로운 권한 시스템으로 인해 사용자가 이를 승인해야 합니다. 이를 위해 Activity의 onCreate() 메서드에 다음 코드를 추가합니다.

private final int OVERLAY_PERMISSION_REQ_CODE = 1;  // 임의의 값 선택

...

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    if (!Settings.canDrawOverlays(this)) {
        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                                   Uri.parse("package:" + getPackageName()));
        startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
    }
}

마지막으로 onActivityResult() 메서드를 재정의하여 권한이 승인되었는지 또는 거부되었는지를 처리합니다. 또한 startActivityForResult를 사용하는 네이티브 모듈을 통합하려면 ReactInstanceManager 인스턴스의 onActivityResult 메서드에 결과를 전달해야 합니다.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (!Settings.canDrawOverlays(this)) {
                // SYSTEM_ALERT_WINDOW permission not granted
            }
        }
    }
    mReactInstanceManager.onActivityResult( this, requestCode, resultCode, data );
}

이제 일부 네이티브 코드를 추가하여 react native 런타임을 시작하고 js 구성 요소를 렌더링하겠습니다. 이를 위해 ReactRootView를 생성하고, 그 안에 React 애플리케이션을 시작하고 이를 주 콘텐츠 뷰로 설정하는 Activity를 생성합니다. 

public class MyReactActivity extends Activity implements DefaultHardwareBackBtnHandler {
    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SoLoader.init(this, false);
		
        // ReactRootView는 네이티브 애플리케이션의 루트 역할을 하는 View입니다.
        mReactRootView = new ReactRootView(this);
        
        List<ReactPackage> packages = new PackageList(getApplication()).getPackages();
        // 아직 자동 연결되지 않은 패키지는 여기에 수동으로 추가할 수 있습니다:
        // packages.add(new MyReactNativePackage());
        // settings.gradle 및 app/build.gradle에도 포함해야 합니다.
		
        mReactInstanceManager = ReactInstanceManager.builder()
        		// 현재 애플리케이션을 설정합니다.
                .setApplication(getApplication())
                // 현재 액티비티를 설정합니다.
                .setCurrentActivity(this)
                // javascript 번들 파일의 asset 이름을 설정합니다.
                .setBundleAssetName("index.android.bundle")
                // javascript 메인 모듈 경로 설정합니다.
                .setJSMainModulePath("index")
                // 추가적인 패키지들을 등록합니다.
                .addPackages(packages)
                // 개발자 지원 모드를 설정합니다.
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                // 초기 생명주기 상태를 설정합니다.
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
                
        // 여기의 문자열 (예: "MyReactNativeApp")은
        // index.js의 AppRegistry.registerComponent()와 일치해야 합니다.
        mReactRootView.startReactApplication(mReactInstanceManager, "MyReactNativeApp", null);
		
        // 현재 액티비티의 컨텐츠 뷰로 설정합니다.
        setContentView(mReactRootView);
    }

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }
}

React Native 번들 생성하는 법

react native gradle 플러그인을 사용하지 않는 경우 android studio 릴리스 빌드마다 수행해야 하는 추가 단계로 다음 명령을 실행하여 react native 번들을 생성 후 android 앱에 포함시켜야 합니다.

$ npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/com/your-company-name/app-package-name/src/main/assets/index.android.bundle --assets-dest android/com/your-company-name/app-package-name/src/main/res/

만약 src/main 밑에 assets 폴더가 없으면 생성해 줍니다. 그러면 번들링이 완료되면서 index.android.bundle의 파일이 생성됩니다.