RN Android Activity → Fragment로 변환해서 View처럼 사용하기

박은정·2023년 9월 14일
0

리액트네이티브

목록 보기
22/27
post-custom-banner

어느날 안드로이드의 액티비티 화면을 리액트 네이티브 화면 안에서 띄울 일이 있었는데
리액트 네이티브에서 보여주는 화면이 MainActivity이고 리액트의 SPA를 활용해서
액티비티의 컨텐츠 내용만 갈아끼우는걸로 알고있었습니다.

그래서 그동안은 액티비티를 보여주려면 MainActivity 위에 새로운 액티비티를 덮어씌우는 식으로 했었는데
이번엔 리액트 네이티브 화면 내부에서 일정한 크기를 가진 View형태로 보여줘야 해서.. 챗 지피티에게 물어봤습니다.

Android Activity를 React Native 화면의 일부로 임베드 할 수 있는 방법

임베드 Embed

해당 글에서는 다른 이의 게시물을 퍼다 나를 수 있는 기능이 임베드라고 합니다.
여기서는, 액티비티의 내용을 리액트 네이티브의 화면의 일부로 가져와 보여줄 수 있는 방법이라 이해하면 될 것 같습니다.

전체 안드로이드 액티비티를 리액트 네이티브 화면 내에 직접 임베드하는 것은 리액트 네이티브와 안드로이드 액티비티의 근본적인 작동방식의 차이로 간단한 문제가 아닙니다.
리액트 네이티브는 자바스크립트로 관리되는 네이티브 컴포넌트를 사용하는 다른 렌더링 모델에서 작동하는 반면에,
안드로이드 액티비티는 안드로이드 앱 라이프사이클을 따르는 무거운 컴포넌트입니다.

하지만 비슷한 결과를 얻기 위해서 다른 접근 방식이 있긴 합니다.

접근방법 1. React Native Module

전체 안드로이드 액티비티를 임베드하는 대신, 안드로이드 액티비티의 기능을 감싸고 리액트 네이티브 컴포넌트 인터페이스를 제공하는 사용자 정의 리액트 네이티브 모듈을 만들 수 있습니다.
이 모듈은 네이티브 안드로이드 액티비티를 내부적으로 관리할 수 있지만, 리액트 네이티브 친화적인 인터페이스를 노출할 수 있습니다.
이렇게 하면 리액트 네이티브에서 네이티브 액티비티를 제어하고 상호 작용할 수 있지만, 리액트 네이티브 컴포넌트 내부에 직접 포함되지는 않습니다.

접근방법 2. Native UI Component

UIManager를 사용해서 리액트 네이티브를 위한 사용자 정의 네이티브 뷰나 컴포넌트를 만들 수 있습니다.
여기에는 안드로이드에서 View나 ViewGroup을 확장하는 것처럼, 원하는 안드로이드 액티비티의 기능과 렌더링을 캡슐화하는 네이티브 뷰 컴포넌트를 생성하는 것이 포함됩니다.
이 접근 방식을 사용하면 리액트 네이티브 앱 내부에서 보다 통합된 네이티브 UI 컴포넌트를 가질 수 있습니다.

접근방법 3. Hybrid App

복잡한 안드로이드 UI를 리액트 네이티브 앱에 통합해야 하는 경우, 하이브리드 앱을 빌드하는 것을 고려할 수 있습니다.
리액트 네이티브 뷰와 네이티브 안드로이드 액티비티 사이를 전환하는 별도의 화면을 가질 수 있습니다.
이러한 부분 간의 통신은 딥링크나 인텐드와 같은 탐색 메커니즘을 사용해서 용이하게 할 수 있습니다.

접근 방식 선택은 안드로이드 액티비티의 복잡성, 원하는 통합 수준, 달성하고자 하는 전반적인 사용자 경험에 따라 달라집니다.
네이티브 안드로이드 컴포넌트와 리액트 네이티브를 혼합하면 일관된 UI와 사용자 흐름을 유지하는 데 어려움을 겪을 수 있기 때문에 조심해야 합니다.


이전에 Android Native UI Components 내용이 떠올라 SimpleViewManager를 사용하면 되겠다 생각했습니다.

React Native의 SimpleViewManager로 Android Activity를 보여주는 방법

리액트 네이티브의 커스텀 SimpleViewManager를 사용해서 안드로이드 액티비티를 열려면 안드로이드 액티비티 launching (시작) 기능을 감싸는 네이티브 모듈을 만들어주면 됩니다.

1. xml 파일 생성

android/app/src/main/res/layout/native_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
		android:layout_width="match_parent"
		android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:orientation="vertical">

        <!-- 아이콘 및 제목 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:layout_marginBottom="12dp"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/imageIcon"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_gravity="center_vertical"
                android:scaleType="centerCrop" />

            <TextView
                android:id="@+id/textTitle"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="8dp"
                android:ellipsize="end"
                android:maxLines="1"
                android:textColor="@android:color/black"
                android:textSize="14sp" />
        </LinearLayout>

        <!-- 설명 -->
        <TextView
            android:id="@+id/textDescription"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:ellipsize="end"
            android:lines="2"
            android:textColor="@android:color/black" />
    </LinearLayout>

    <!-- 로딩중 -->
    <LinearLayout
        android:id="@+id/loadingView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#A000"
        android:gravity="center"
        android:orientation="vertical"
        android:visibility="gone"
        tools:visibility="visible">

        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="16dp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="로딩중"
            android:textColor="#FFF" />
    </LinearLayout>
</FrameLayout>

2. ViewManager 만들기

android/app/src/main/java/com/cordova/projectName/folder/MyCustomViewManager.java

SimpleViewManager<FrameLayout> 를 확장한 ViewManager 클래스를 통해
액티비티 화면을 리액트 네이티브 앱에서 사용하기 위한 커스텀 뷰 매니저로 정의합니다.

package com.cordova.projectName.folder;

import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;

import com.cordova.projectName.MainApplication;
import com.cordova.projectName.R; // 위에서 만든 native_layout.xml
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;

public class CustomViewManager extends SimpleViewManager<FrameLayout> {
    private static final String REACT_CLASS = "CustomViewManager"; // RN에서 커스텀 뷰 매니저를 식별할때 사용함
    private static final String unitId = MainApplication.UNIT_ID_NATIVE_AD;

    @NonNull
    @Override
    public String getName(){ return REACT_CLASS; } // RN에서 해당 뷰 매니저의 이름을 가져오는 메서드를 오버라이드함

    @NonNull
    @Override
    protected FrameLayout createViewInstance(@NonNull ThemedReactContext reactContext) {
		// xml 레이아웃 파일을 FrameLayout으로 확장한 frameLayout 객체를 생성함
        LayoutInflater inflater = (LayoutInflater) reactContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        FrameLayout frameLayout = (FrameLayout) inflater.inflate(R.layout.layout_name, null);

		// == 여기부터 == 
        TextView stateTextView = frameLayout.findViewById(R.id.stateTextView);
        TextView eventTextView = frameLayout.findViewById(R.id.eventTextView);
        View loadingView = frameLayout.findViewById(R.id.loadingView);

        MediaView mediaView = frameLayout.findViewById(R.id.mediaView);
        TextView titleTextView = frameLayout.findViewById(R.id.textTitle);
        ImageView iconImageView = frameLayout.findViewById(R.id.imageIcon); 
        TextView descriptionTextView = frameLayout.findViewById(R.id.textDescription);

        // 레이아웃설정
        NativeAd2ViewBinder binder = new NativeAd2ViewBinder.Builder()
                .nativeAd2View(nativeAd2View)
                .mediaView(mediaView)
                .titleTextView(titleTextView)
                .iconImageView(iconImageView)
                .descriptionTextView(descriptionTextView)
                .ctaView(ctaView)
                .build(unitId);

        // 요청상태에 따른 UI
        binder.addNativeAd2StateChangedListener(new NativeAd2StateChangedListener() {
            @Override
            public void onRequested() {}

            @Override
            public void onNext(NativeAd2 nativeAd2) {}

            @Override
            public void onComplete() {}

            @Override
            public void onError(@NonNull AdError adError) {}
        });

        binder.addNativeAd2EventListener(new NativeAd2EventListener() {});
		// 광고를 표시하고 상호작용 가능한 상태로 만듬
        binder.bind();
		// == 여기까진 Activity에서의 코드와 동일하다 == 
		
		// frameLayout를 반환해서 RN에서 CustomViewManager를 사용하는 컴포넌트에 표시될 View를 생성한다
        return frameLayout;
    }
}

3. ViewManager를 감싸는 Package만들기

android/app/src/main/java/com/cordova/projectName/folder/MyModulePackage.java

package com.cordova.projectName.folder;

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.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Collections;

public class MyModulePackage implements ReactPackage {
    @NonNull
    @Override
    public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
		// 모듈을 추가할땐 여기로
		return Collections.emptyList();
    }

    @NonNull
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        // 화면을 추가할땐 여기로
        return Arrays.<ViewManager>asList(new CustomViewManager());
    }
}

4. React Native에서 CustomViewManager import해서 사용하기

src/CustomView.js

import { requireNativeComponent } from 'react-native';
const CustomView = requireNativeComponent('CustomViewManager');
export default CustomView;

주의점!

위처럼 별도의 자바스크립트 파일로 CustomView를 지정하지 않고, 아래처럼 바로 requireNativeComponent 메서드로 불러와서 사용한다면

// Page.js
import { requireNativeComponent } from 'react-native';
const CustomView = requireNativeComponent('CustomViewManager');

export default () => {
	return (
		<View>
		<Text>다른페이지에서 사용한다면</Text>
		<CustomView/>
		</View>
	)

}

이러한 에러를 볼 수 있는데,

25140-25279/com.cordova.mileverse E/ReactNativeJS: Invariant Violation: Tried to register two views with the same name CustomView

같은 이름의 뷰를 두 개 등록하려고 했습니다. 라는 뜻으로 CustomView 라는 이름으로 두 번 이상 등록해서 발생하는 오류가 발생합니다.
챗지피티는 다른 이유를 설명해줬지만, requireNativeComponent() 부분을 별도의 파일로 분류해서 한번만 호출되게 해서인지.. 오류가 발생하지 않게 됩니다.

profile
새로운 것을 도전하고 노력한다
post-custom-banner

0개의 댓글