Jetpack Compose 앱에 Firebase Crashlytics 추가하기

안드로이드 앱에 Firebase 를 추가할 때 겪은 두 가지 문제를 해결하기 위해 build variant 마다 별도의 프로젝트로 추가하고, AndroidManifest.xml 을 수정하였다. Jetpack Compose 앱도 설정은 똑같고, 단지 Crashlytics 구현 테스트 버튼 만드는 방식이 다를 뿐이다.

Firebase 추가하기

채팅방에서 다른 개발자들이 Firebase 를 권한다. 어떤 분은 Supabase 를 쓴다고 한다. 이런 걸 BaaS (backend as a service)라고 부르는 것도 이번에 알았다.

현재 내 앱은 비공개 테스트 중이다. 사실 난 이미 여러 날 전에 내 앱에 Firebase 를 추가하려고 했다. Play Console 에 이런 말이 있다.

사전 출시 보고서는 Firebase Test Lab에서 제공합니다. Test Lab을 사용하여 언제든지 더 다양한 기기에서 맞춤설정된 테스트를 실행하세요.

이 말을 읽고 Test Lab 에 욕심이 생겨서 Firebase 추가에 도전하였다. 그런데 그 후에 앱 컴파일이 제대로 되지 않아서 없던 일로 하고 나중을 기약하였었다.

다시 Firebase 추가에 도전함

다른 개발자들이 쓰라고 권하는 데에는 다 그만한 이유가 있을 터이다. 앱이 정식으로 출시하고 나면 새로운 기능을 추가하기가 더 조심스러워질지도 모른다.

어제저녁에 내 앱의 새 버전을 만들어 게시한 후에, 지금이 Firebase 에 다시 도전할 좋은 기회라고 생각했다. 그래서 지난번에 왜 실패했는지 다시 그 길을 찬찬히 걸어갔다. 길은 지난번과 똑같았다.

Firebase Console 에 가보니 지난번에 추가한 프로젝트가 그대로 있었다.
https://console.firebase.google.com/
프로젝트 추가하는 과정은 그리 어렵지 않다. 예제 답변이 있어서 그거 참고하고, 자신의 앱 정보를 입력해 나가면 된다. Google 애널리틱스 계정은 블로그 하느라고 만들어 두었던 걸 그대로 선택했다.

google-services.json

구성 파일 다운로드 후 추가

다운로드한 google-services.json 파일을
모듈(앱 수준) 루트 디렉터리로 이동합니다.

설정 중에 위의 안내에 따라 app/ 아래에 google-services.json 을 갖다 놓았다.

또한 두 build.gradle.kts 의 여기저기를 수정해야 했고 필요한 라이브러리를 추가하라길래 이런저런 라이브러리를 추가했었다. 그 후에, 어제저녁에 다음 링크를 참고하여 Crashlytics 에 당장 필요하지 않아 보이는 것은 일단 빼버리고 필요한 것만 남겼다.
https://firebase.google.com/docs/crashlytics/get-started?hl=ko&platform=android

Build variants: debug vs release

다 설정하고 난 후에 앱을 컴파일하였다. 지난번과 똑같은 오류를 내면서 실패했다(applicationId 는 내가 임의로 수정함).

Execution failed for task ':app:processDebugGoogleServices'.
> No matching client found for package name 'com.example.testapp.debug' in /home/.../app/google-services.json

지난번에는 이 오류 메시지를 깊이 들여다보지 않고 그냥 여기서 포기했었다.

이제 이 문제를 해결하려고 한다. 현재 출시를 앞두고 있는 내 앱에는 debug build variant 와 release build variant 가 있다. 저 메시지의 의미를 따져 보니 이런 결론이 나왔다.

내가 Firebase Console 에 추가한 프로젝트는 release build variant 인데, 지금 컴파일을 시도한 것은 debug build variant 이다. 그래서 저런 오류를 내면서 실패했다.

실제로 google-services.json 파일을 열어보면 내 앱의 release build variant 의 패키지 이름(applicationId) 가 적혀 있다.

Manifest merger failed

만약 내가 release build variant 를 컴파일해 본다면 적어도 저 첫 번째 오류는 더 이상 생기지 않을 것이다. 진짜 그랬다. 그런데 여기에서는 다른 오류가 발생했다.

Manifest merger failed : Attribute property#android.adservices.AD_SERVICES_CONFIG@resource value=(@xml/gma_ad_services_config) from [com.google.android.gms:play-services-ads-lite:23.1.0] AndroidManifest.xml:92:13-59
is also present at [com.google.android.gms:play-services-measurement-api:22.0.2] AndroidManifest.xml:32:13-58 value=(@xml/ga_ad_services_config).
Suggestion: add 'tools:replace="android:resource"' to <property> element at AndroidManifest.xml to override.

이 메시지 안에 해결 방법이 제안되어 있는데 이것만으로는 어떻게 해야 할지 잘 모르겠다. 검색을 해보니 이 문제는 내 AndroidManifest.xml 때문이 아니고 바깥에 있는 어떤 라이브러리 때문이라고 한다. 결국 AGP 버전을 낮춰야 한단다.

나는 지금까지 안드로이드 스튜디오가 업데이트를 권하기만 하면 AGP 버전을 올리곤 했다. 현재 버전은 8.5.0 이다. 그러고도 지금까지 아무 문제가 없었다. 그런데 지금에 와서 다운그레이드하려고 하니까 마음이 내키지 않는다.

나처럼 AGP 버전을 낮추지 않고 문제를 해결하고 싶은 사람을 위해서 다음과 같이 되도록 AndroidManifest.xml 을 수정하라고 그분이 조언한다. 저 위의 오류 메시지 안에 나오는 내용이 더 구체화되었다.

<manifest
...

<application
...

<property
android:name="android.adservices.AD_SERVICES_CONFIG"
android:resource="@xml/gma_ad_services_config"
tools:replace="android:resource" />

...
</application>

...
</manifest>

https://stackoverflow.com/questions/78085458/manifest-merger-failed-with-agp-8-3-0

이 방법으로 문제를 회피할 수 있더라도 AGP 버전을 낮추는 게 더 바람직하겠다고 그분은 강력히 더 권고하였지만 나는 이 방법으로 해보기로 했다.

Crashlytics, release build variant 감지함

그 후에는 release build variant 가 잘 컴파일되었다. 기기에서 실행해 보니 아무 문제없었다. 그 직후에 Firebase Console 에 가보았다. Crashlytics 에 다음과 같이 나와 있었다.

Firebase Crashlytics

이제 Firebase 설정이 제대로 된 것은 확인한 셈이다.

그렇다고 매번 release build variant 를 컴파일할 수는 없다. 이건 컴파일 시간이 훨씬 더 걸린다. release build variant 컴파일은 출시하기 전에 한두 번만 하면 족하다. 따라서 평상시를 위해서

debug build variant 도 Firebase Console 에 별도의 프로젝트로 추가해야 한다.

라는 결론이 나온다.

google-services.json 파일 2개인데?

debug build variant 를 Firebase Console 에 별도의 프로젝트로 추가하려고 생각해 보니 한 가지 문제가 있다.

Firebase 에 프로젝트를 하나 더 추가하면 google-services.json 파일이 하나 더 생길 텐데 이걸 어디에 두어야 하지?

이 문제를 다른 사람들도 겪었을 텐데 어떻게 해결했는지 궁금했다. 검색해서 비슷한 다음 두 내용을 찾았다.

Create a second Firebase project
Download its google-services.json
Android Setup
Create two folders inside the src directory: debug and release;
Copy the respective google-services.json file on each one.
That’s it, Android will pick the correct one automatically on build.
https://brunolemos.medium.com/how-to-setup-a-different-firebase-project-for-debug-and-release-environments-157b40512164

Once done, your configuration would be organized like this:

app/
src/debug/google-services.json
src/release/google-services.json
https://medium.com/@pranathipellakuru/using-build-types-to-the-most-android-08e4b7b0d48c

원래 app/src/debug/, app/src/release/ 디렉터리는 존재하지 않는데 그냥 만들어서 그 아래에 해당 google-services.json 파일을 갖다 놓으면 된다는 말이다. 그렇게 해놓으면 시스템이 알아서 잘 찾아서 수행한다. 즉,

app/ 아래에 두었던 release build variant 의 google-services.json 파일은 app/src/release/ 아래로 옮기고, 새로 받아온 debug build variant 의 google-services.json 파일은 app/src/debug/ 아래에 두었다.

이렇게 해서 debug build variant 컴파일에 성공하였다. Firebase Console 의 이 프로젝트에서도 위의 스크린샷에서와 마찬가지로 “앱이 감지되었으며 비정상 종료를 기다리는 중입니다.”라고 나오는 모습을 또 볼 수 있었다.

Jetpack Compose 에서 Firebase Crashlytics 구현 테스트 하기

이제 실제로 Firebase Console 이 앱의 비정상 종료를 잘 포착하는지 알아보아야 한다. 저 스크린샷 속의 링크를 따라 가면 다음과 같은 내용이 나온다.

val crashButton = Button(this)
crashButton.text = "Test Crash"
crashButton.setOnClickListener {
throw RuntimeException("Test Crash") // Force a crash
}

addContentView(crashButton, ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT))

https://firebase.google.com/docs/crashlytics/test-implementation?platform=android&hl=ko

이런 코드를 어딘가에서 본 듯하기는 한데 나는 잘 이해하지는 못한다. 지금 자세히 보니 throw RuntimeException 이 눈에 띈다. 위의 코드는 아마 Views 방식으로 버튼을 구현하는 코드인 것 같다. 나는 처음부터 Jetpack Compose 만 사용해서 UI 를 구성했다. 지금에 와서 보니 그리 어렵게 생각할 건 아니었는데 처음에는 Views 코드가 나와서 당황했다. 그래서 검색해서 찾은 아래의 링크에 나오는 내용과 위의 내용을 참고하여 내 앱의 ViewModel 클래스에 다음의 멤버 함수를 만들었다.

fun generateCrash() {
throw RuntimeException("Test Crash") // Force a crash
}

참고: https://firebase.blog/posts/2022/06/adding-crashlytics-to-jetpack-compose-app/

이제 이 함수를 어느 Composable 함수의 onClick 람다에서 호출하면 된다. Button() 함수를 쓰면 보통이겠다. 그런데 새로 버튼을 만들기는 귀찮아서 앱에 들어있는 드롭다운 메뉴에 메뉴아이템을 하나 더 만들었다.

DropdownMenuItem(
text = {
Text("Generate a crash to test Firebase Crashlytics")
},
onClick = { viewModel.generateCrash() },
leadingIcon = {
Icon(
Icons.Default.Error,
contentDescription = "Error"
)
}
)

앱을 컴파일한 후에 Firebase Console 을 확인해 보니 아무 일 없고 여전히 저 위의 스크린샷과 같은 모습이었다.

새로 만든 드롭다운 메뉴 아이템을 클릭했다. 앱이 즉시 다운되었다.

아직 앱을 재시작하지 않은 상태에서 Firebase Console 에 뭐라고 나올 건지 궁금했다. 몇 초 지난 후에 Firebase Console 화면을 새로고침하였다. Crashlytics 에 다음과 같은 그래프가 나타났다.

Firebase Crashlytics

나는 한참 지나야 Firebase Console 이 반응할 줄 알았는데 생각보다 빨리 반응해서 좀 놀랐다.

오른쪽 그래프는 전체 사용자가 1명인데 비정상 종료가 1명 발생했음을 의미한다. 따라서 왼쪽 그래프 위에 비정상 종료가 발생하지 않은 사용자가 0%라고 나왔다. 전체 세션 4 회 중에 한 번 비정상 종료가 발생했으므로 비정상 종료가 발생하지 않은 세션은 3/4 = 0.75 = 75% 가 나왔다.

앱을 몇 번 재시작하면서 Firebase Console 화면을 새로고침하니까 비정상 종료가 발생하지 않은 세션 수치가 점점 커진다.

Firebase Crashlytics

그 후 한 12 시간이 지난 후에 좀 전까지도 Firebase Console 에 비정상 종료 1 이라고 나온 걸 확인하고 나서, 다시 비정상 종료를 일으키고 단 몇 초 후에 화면을 새로 고치자, 다음 그림이 나타났다.

Firebase Crashlytics

바로 반응이 온다. 내 눈으로 Crashlytics 의 성능을 확인하고 나니 재미있고 Firebase 에 더 믿음이 생긴다.

Leave a Comment