🐦 Flutter

Flutter Unity 통합하기 2

ji-hyun 2024. 12. 18. 23:45

2024.11.30 - [🐦 Flutter] - Flutter Unity 통합하기

Flutter Unity 통합 과정에 대한 글을 보고 싶다면 위 글을 참고해주세요!

 


1. 만 대가 넘는 전화기기 지원 중단

Flutter Unity 통합한 앱을 개발 완성하였고 앱 스토어에 심사 받고 배포할 일만 남았었다.

이젠 정말 개발 끝인줄 알았는데 또 다시 난관에 봉착했다.

 

 

 

Google Play Console 에 해당 앱을 제출하게 되면 바로 아래와 같은 경고가 뜬다.

 

 

 

 

위 경고는 이 버전 앱을 출시하면 기기의 10,696대가 지원 중단될 것이고 설치수 986건에 영향을 미치게 될 것이니 검토해달란 경고이다.

비록 우리 앱이 AR 관련 기능을 추가했지만 AR 기능은 우리 앱의 극히 일부분일 뿐 이것이 우리 앱에 설치 영향을 끼치는 것은 부당(?)하다고 생각했다. 그리고 기존 앱 유저는 우리 앱을 잘 사용하고 있었는데 갑자기 업데이트 후 앱 사용을 못하게 되면 화가 날 것이다.

 

 

 

이것을 해결할 방법이 있을까?

 

 

 

유니티 개발자의 말씀으로는 Unity 에서 AR 을 이용하기 위한 최소 API level은 24로 android 7.0부터 가능하다고 말했다.

flutter_unity_widget 패키지의 공식 문서를 보아도 minSdkVersion 24 부터 지원된다고 써있다.

근데 원래 프로젝트의 minSdkVersion 설정은 이미 API 24 이었다..?

 

그렇다면 왜 갑자기 만 대가 넘는 전화 기기 중단 경고가 뜬 걸까?

 

 

 

 

 

 

2. minSdkVersion 24 이어도 AR 기능이 지원되지 않는 이유

아래 "기기가 지원되지 않는 이유"를 유심히 살펴보았다.

그리고 열심히 검색해봤고 결국 원인을 찾아냈다.

 

 

 

바로 API 24 이상이어도 AR 기능을 지원할 수 없는 기기가 있다는 것이다. 지금 생각해보니 기기에 따라 지원 안되는 기기가 있는건 당연한건데 그때 당시에는 공식문서의 minSdkVersion 24 설정을 하란대로 설정하면 다 지원되는 줄 알았다.

아래 문서를 살펴보면 minSdkVersion 을 24 로 설정해두어도 지원 안되는 기기 목록이 표시되어 있다.

 

또한 Depth API 에도 아래와 같은 설명이 써있었다.

  • Depth API: 2024년 11월 기준으로 활성 기기의 약 90% 가 Depth API를 지원합니다.

 

https://developers.google.com/ar/devices?hl=ko

 

ARCore 지원 기기  |  Google for Developers

ARCore를 지원하는 Android 및 iOS 기기의 목록을 가져오거나 CSV 파일을 다운로드합니다.

developers.google.com

 

 

 

 

 

 

 

3. 해결방법

다시 말하지만 AR 기능은 우리 앱에서 필수적인 기능이 아니다.

위의 이미지에서 "기기가 지원되지 않는 이유"에 나열되어 있는 권한을 찾아보니 android > unityLibrary > src > main > AndroidManifest.xml 파일에 아래와 같이 써있었다.

 

<meta-data android:name="com.google.ar.core" android:value="required" />
<uses-feature android:name="android.hardware.camera.ar" android:required="true" />
<uses-feature android:name="com.google.ar.core.depth" android:required="true" />

 

모두 required 가 true 로 되어 있었다.

그렇다면 AR 기능을 선택적(Optional)으로 하면 되지 않을까 생각했다. 이에 대해 찾아보았고 아래 설명 문서를 찾을 수 있었다.

 

https://developers.google.com/ar/develop/java/enable-arcore?hl=ko#ar-optional

 

Android 앱에서 AR 사용 설정하기  |  ARCore  |  Google for Developers

신규 또는 기존 앱에서 AR 기능을 구성하고 사용하는 방법을 알아보세요.

developers.google.com

 

위 문서를 요약하면 다음과 같은 방법으로 해결할 수 있다고 써있다.

 

 

1) AR 선택으로 변경하기

 

AR 선택적 앱은 ARCore를 지원하지 않는 기기에 설치하고 실행할 수 있습니다.

 

 

2) 앱의 build.gradle 파일에 최신 ARCore 라이브러리를 종속 항목으로 추가

 

dependencies {
    …
    implementation 'com.google.ar:core:1.33.0'
}

 

 

3) 런타임 검사 실행

 

AR 필수 앱과 AR 선택적 앱 모두 ArCoreApk.checkAvailability() 또는 ArCoreApk.checkAvailabilityAsync() 를 사용하여 현재 기기가 ARCore를 지원하는지 확인해야 합니다. ARCore를 지원하지 않는 기기에서는 앱이 AR 관련 기능을 사용 중지하고 연결된 UI 요소를 숨겨야 합니다.

 

즉, 2)에서 추가한 ARCore 종속성으로 앱 런타임 시에 지원하는지 안하는지 체크를 하면 된다고 한다.

 

 

 

 

 

 

 

4. 유니티 빌드 설정 변경

안드로이드에서 ARCore 를 선택적으로 적용하려면 안드로이드에서만 ARCore 에 대한 Build Setting 을 변경해주어야 한다.

기본적으로 유니티에서는 ARCore 에 대한 Build Setting 이 Required(필수)로 설정되어 있다.

 

아래와 같이 따라해서 설정을 변경해보자.

 

  • 안드로이드 빌드 이전, ARCore 관련 Build Setting 에서 모두 Optional 로 바꿔서 적용해서 빌드하기
    • 이때 Depth 관련 설정도 무조건 Optional 로 설정해주어야 한다. → Depth 필수 설정 또한 기기 설치 수에 영향을 미치기 때문이다. 전화 기기 지원 중단 이유 중에 "com.google.ar.core.depth" 가 있었다.

  • iOS 빌드 이전, ARCore 관련 Build Setting 에서 모두 Required 로 바꿔서 적용해서 빌드하기

 

 

사진 따라 1 > 2 > 3 > 4 순

 

 

 

위처럼 설정하고 빌드 후 안드로이드 관련해서는 따로 아래와 같이 적용한다.

 

1. android > unityLibrary > build.gradle 에서 아래 주석 처리

// implementation(name: 'arcore_client', ext:'aar')

 

 

2. android > unityLibrary > src > main > AndroidManifest.xml 에서 다음과 같이 적용 확인
(이렇게 적용 안되어 있으면 추가해서 적도록 한다)

 

<meta-data android:name="com.google.ar.core" android:value="optional" />
<uses-feature android:name="android.hardware.camera.ar" android:required="false" />
<uses-feature android:name="com.google.ar.core.depth" android:required="false" />

 

 

 

 

 

 

 

5. ARCore 를 지원하는 기기인지 체크 코드 작성

4번에서 안드로이드의 ARCore 기능을 필수적이 아니라 옵셔널로 바꾸었다.

이제 런타임 환경에서 ARCore 를 지원하는 기기인지 체크하고 지원하지 않는 기기이면 "지원하지 않는 기기입니다" 팝업을 띄울 계획이다.

 

 

ARCore 를 지원하는 기기인지 검사하려면 안드로이드 네티이브 코드를 추가해야 한다.

MainAcitivity.kt 파일에서 아래와 같이 작성해보자.

 

 

 

 

1) MethodChannel 연결

 

메서드명은 isARCoreSupported 로 지정하였다.

그리고 이 메서드의 반환값이 true 를 반환하면 지원하는 기기, false 를 반환하면 지원하지 않는 기기, null 을 반환하면 네트워크 등 알 수 없는 오류로 처리하였다.
(네트워크를 통해서 지원 기기 여부가 체크 되는건가 싶다)

 

nativeChannel.setMethodCallHandler { call, result ->
            when (call.method) {
                "isARCoreSupported" -> {
                    val isARCoreSupported = isARCoreSupported();
                    result.success(isARCoreSupported)
                }
            }
        }

 

 

 

 

2) isARCoreSupported 로직 작성

 

https://developers.google.com/ar/develop/java/session-config?hl=ko#kotlin

ARCore 지원 검사 코드는 위 코드를 참조하여 작성하였다.

그런데 사실 나는 지원하지 않는 안드로이드 기기를 갖고 있지 않아서 테스트를 하지 못했다.

테스트를 하지 못했기 때문에 불안감이 있어서 아래에서 SUPPORTED_APK_TOO_OLD, SUPPORTED_NOT_INSTALLED 의 경우에는 공식문서대로의 ARCore 를 설치하지 않고 그냥 false 로 처리하였다. (아래 코드에서 주석 처리한 부분이다)

 

 

private fun isARCoreSupported(): Boolean? {
        return when (ArCoreApk.getInstance().checkAvailability(this)) {
            ArCoreApk.Availability.SUPPORTED_INSTALLED -> true
            ArCoreApk.Availability.SUPPORTED_APK_TOO_OLD, ArCoreApk.Availability.SUPPORTED_NOT_INSTALLED -> {
//                try {
//                    // Request ARCore installation or update if needed.
//                    when (ArCoreApk.getInstance().requestInstall(this, true)) {
//                        ArCoreApk.InstallStatus.INSTALL_REQUESTED -> {
//                            Log.i("ARCore", "ARCore installation requested.")
//                            false
//                        }
//                        ArCoreApk.InstallStatus.INSTALLED -> true
//                    }
//                } catch (e: UnavailableException) {
//                    Log.e("ARCore", "ARCore not installed", e)
//                    false
//                }
                false
            }

            ArCoreApk.Availability.UNSUPPORTED_DEVICE_NOT_CAPABLE ->
                // This device is not supported for AR.
                false

            ArCoreApk.Availability.UNKNOWN_CHECKING -> {
                // ARCore is checking the availability with a remote query.
                // This function should be called again after waiting 200 ms to determine the query result.
                Log.i("ARCore", "ARCore availability is being checked. Retrying in 200ms...")
                Handler(Looper.getMainLooper()).postDelayed({
                    isARCoreSupported() // 함수를 다시 호출하여 상태 확인
                }, 200) // 200ms 대기 후 재시도
            }
            ArCoreApk.Availability.UNKNOWN_ERROR, ArCoreApk.Availability.UNKNOWN_TIMED_OUT -> {
                // There was an error checking for AR availability. This may be due to the device being offline.
                // Handle the error appropriately.
                null
            }
        }
    }

 

UNKNOWN_CHECKING 의 경우, 공식문서에 쓰여 있듯이 200ms 후에 재시도를 해야 한다고 써있다. 

UNKNOWN_ERROR, UNKOWN_TIMED_OUT 의 경우, 지원 기기 체크 도중 오류가 발생한 것으로, null 로 반환하여 Flutter 에서 "잠시 후 다시 시도해주세요" 라는 팝업을 띄워두도록 할 것이다.

 

 

 

 

 

 

3) Depth API 지원 체크 로직 작성

 

우리 앱에서는 ARCore 기능을 사용할 뿐만 아니라 Depth API 도 사용하고 있었다. (만약 Depth API 를 사용하지 않는다면 이 과정은 넘어가도 된다)

 

https://developers.google.com/ar/develop/java/depth/developer-guide?hl=ko#kotlin

Depth API 를 지원하는지 체크 코드는 위 문서를 참고하였다.

 

 

  • isARCoreDepthSupported 메서드 작성

 

nativeChannel.setMethodCallHandler { call, result ->
            when (call.method) {
                "isARCoreDepthSupported" -> {
                    val isARCoreDepthSupported = isARCoreDepthSupported();
                    result.success(isARCoreDepthSupported)
                }
            }
        }

 

 

 

  • isARCoreDepthSupported 로직

 

private fun isARCoreDepthSupported(): Boolean {
        return try {
            val session = Session(this)
            val isDepthSupported = session.isDepthModeSupported(Config.DepthMode.AUTOMATIC)
            session.close()

            isDepthSupported
        } catch (e: UnavailableArcoreNotInstalledException) {
            false
        } catch (e: UnavailableDeviceNotCompatibleException) {
            false
        } catch (e: Exception) {
            false
        }
    }

 

위 코드에서 보면 session 을 close 하는 모습을 볼 수 있는데 session 을 close 하지 않으면 메모리 문제가 발생할 수 있기 때문이다.

자세한 내용은 아래 공식 문서를 참고하면 된다.

 

Session는 상당한 양의 네이티브 힙 메모리를 소유합니다. 명시적으로 실패한 경우 세션을 닫으면 앱의 네이티브 메모리가 부족해지고 비정상 종료될 수 있습니다. 날짜 AR 세션이 더 이상 필요하지 않다면 출시까지 close() 남음 리소스를 배포합니다 앱에 AR 지원 활동이 하나만 포함된 경우 close()를 호출합니다. 활동의 onDestroy() 메서드에서 가져올 수 있습니다.
https://developers.google.com/ar/develop/java/session-config?hl=ko#close_a_session

 

 

 

 

 

6. Flutter 처리 코드

ARCore 에 대한 지원 여부 코드는 모두 작성하였다.

Flutter 앱에서는 이제 아래와 같이 안드로이드 경우에서만 체크하도록 작성해준다.

또 하나 유의해야 할 점은 ARCore 지원 여부를 확인하기 전에 카메라 권한이 부여되었는지 확인해야 한다. 이에 대한 코드는 생략하겠다.

 

Future<bool> isARSupported() async {
    try {
      if (Platform.isIOS) {
        return true;
      }
		
      // 1. 카메라 권한 먼저 확인
      if (!await DeviceUtils().checkCameraPermission()) {
        await Get.dialog(
          GoblinConfirmPopup(text: '카메라 권한을 허용해주세요.'),
          barrierDismissible: false,
        );
        return false;
      }
		
      // 2. ARCore 지원 여부 체크 
      final isARCoreSupported = await MethodchannelService().isARCoreSupported();
      if (isARCoreSupported == null) {
        await Get.dialog(
          GoblinConfirmPopup(text: '잠시 후 다시 시도해주세요.'),
          barrierDismissible: false,
        );
        return false;
      }

      if (!isARCoreSupported) {
        await Get.dialog(
          GoblinConfirmPopup(text: '지원하지 않는 기기입니다.'),
          barrierDismissible: false,
        );
        return false;
      }
		
      // 3. Depth API 지원 여부 체크
      final isARCoreDepthSupported = await MethodchannelService().isARCoreDepthSupported();
      if (!isARCoreDepthSupported) {
        await Get.dialog(
          GoblinConfirmPopup(text: '지원하지 않는 기기입니다.'),
          barrierDismissible: false,
        );
        return false;
      }

      return true;
    } catch (e) {
      await Get.dialog(
        GoblinConfirmPopup(text: '일시적인 문제가 발생했습니다.'),
        barrierDismissible: false,
      );
      return false;
    }
  }

 

위의 코드에서 true 로 반환되면 아래와 같이 AR 화면을 진입하게 되고 런타임 크래시는 나지 않을 것이다.

(테스트 기기가 없어서 확인 안해봤는데 그럴 것이라는 추측..)

 

 

 

 

 

느낀점

Flutter Unity 통합 과정을 해보면서 정말 많은 오류를 맞닿뜨렸는데

그래도 이런 오류를 하나씩 해결해나가는 재미가 있는 것 같다. Flutter Unity 연동 사례가 많이 없는데 이번 글도 많은 도움이 되었으면 좋겠다.

Flutter 앱 개발자들 홧팅!!