고급 Crashlytics 기능을 사용하여 Unity 게임의 비정상 종료 이해하기

1. 소개

이 Codelab에서는 Crashlytics의 고급 기능을 사용하여 비정상 종료와 비정상 종료를 일으킬 수 있는 상황을 더 잘 파악하는 방법을 알아봅니다.

샘플 게임인 MechaHamster: Level Up with Firebase Edition에 새로운 기능을 추가합니다. 이 샘플 게임은 기존 Firebase 게임인 MechaHamster의 새 버전으로, 내장된 Firebase 기능의 대부분을 삭제하여 그 대신 Firebase의 새로운 용도를 구현할 수 있습니다.

게임에 디버그 메뉴를 추가합니다. 이 디버그 메뉴는 개발자가 만든 메서드를 호출하고 Crashlytics의 다양한 기능을 실행할 수 있도록 합니다. 이 메서드는 맞춤 키, 맞춤 로그, 심각하지 않은 오류 등으로 자동 비정상 종료 보고서에 주석을 추가하는 방법을 보여줍니다.

게임을 빌드한 후 디버그 메뉴를 사용하고 결과를 검사하여 게임이 실제 실행되는 방식에 대한 고유한 뷰를 파악합니다.

학습할 내용

  • Crashlytics에서 자동으로 포착하는 오류 유형입니다.
  • 의도적으로 기록할 수 있는 추가 오류입니다.
  • 이러한 오류를 더 쉽게 이해할 수 있도록 더 많은 정보를 추가하는 방법

필요한 사항

  • 다음 중 하나 또는 둘 다를 포함하는 Unity (최소 권장 버전 2019 이상)
    • iOS 빌드 지원
    • Android 빌드 지원
  • (Android만 해당) Firebase CLI(비정상 종료 보고용 기호를 업로드하는 데 사용됨)

2. 개발 환경 설정

다음 섹션에서는 Firebase로 레벨 업 코드를 다운로드하고 Unity에서 여는 방법을 설명합니다.

Firebase로 레벨업하기 샘플 게임은 다른 여러 Firebase + Unity Codelab에서 사용되므로 이 섹션의 작업을 이미 완료했을 수도 있습니다. 그런 경우 이 페이지의 마지막 단계인 'Unity용 Firebase SDK 추가'로 바로 이동할 수 있습니다.

코드 다운로드

이 Codelab의 GitHub 저장소를 명령줄에서 클론합니다.

git clone https://github.com/firebase/level-up-with-firebase.git

또는 git을 설치하지 않은 경우 저장소를 ZIP 파일로 다운로드할 수 있습니다.

Unity 편집기에서 Level Up with Firebase(Firebase로 레벨 업)를 엽니다.

  1. Unity Hub를 실행하고 Projects 탭에서 Open 옆에 있는 드롭다운 화살표를 클릭합니다.
  2. 디스크에서 프로젝트 추가를 클릭합니다.
  3. 코드가 있는 디렉터리로 이동한 후 OK를 클릭합니다.
  4. 메시지가 표시되면 사용할 Unity 편집기 버전과 대상 플랫폼 (Android 또는 iOS)을 선택합니다.
  5. 프로젝트 이름인 level-up-with-firebase를 클릭하면 프로젝트가 Unity 편집기에서 열립니다.
  6. 편집기가 자동으로 열리지 않으면 Unity 편집기의 Project 탭에서 Assets > Hamster에서 MainGameScene를 엽니다.
    ff4ea3f3c0d29379.png

Unity 설치 및 사용에 관한 자세한 내용은 Unity에서 작업을 참조하세요.

3. Unity 프로젝트에 Firebase 추가

Firebase 프로젝트 만들기

  1. Firebase Console에서 프로젝트 추가를 클릭합니다.
  2. 새 프로젝트를 만들려면 원하는 프로젝트 이름을 입력합니다.
    이렇게 하면 프로젝트 ID(프로젝트 이름 아래에 표시됨)도 프로젝트 이름을 기반으로 설정됩니다. 필요한 경우 프로젝트 ID에서 수정 아이콘을 클릭하여 추가로 맞춤설정할 수 있습니다.
  3. 메시지가 표시되면 Firebase 약관을 검토하고 이에 동의합니다.
  4. 계속을 클릭합니다.
  5. 이 프로젝트에 Google 애널리틱스 사용 설정 옵션을 선택한 다음 계속을 클릭합니다.
  6. 사용할 기존 Google 애널리틱스 계정을 선택하거나 새 계정 만들기를 선택하여 새 계정을 만듭니다.
  7. 프로젝트 만들기를 클릭합니다.
  8. 프로젝트가 생성되면 계속을 클릭합니다.

Firebase에 앱 등록

  1. Firebase Console에서 프로젝트 개요 페이지 중앙의 Unity 아이콘을 클릭하여 설정 워크플로를 시작합니다. Firebase 프로젝트에 이미 앱을 추가한 경우 앱 추가를 클릭하여 플랫폼 옵션을 표시합니다.
  2. Apple(iOS) 및 Android 빌드 대상을 모두 등록하도록 선택합니다.
  3. Unity 프로젝트의 플랫폼별 ID를 입력합니다. 이 Codelab에서는 다음을 입력합니다.
    • Apple (iOS): iOS 번들 ID 필드에 com.google.firebase.level-up를 입력합니다.
    • Android의 경우: Android 패키지 이름 필드에 com.google.firebase.level_up를 입력합니다.
  4. (선택사항) Unity 프로젝트의 플랫폼별 닉네임을 입력합니다.
  5. 앱 등록을 클릭한 다음 구성 파일 다운로드 섹션으로 이동합니다.

Firebase 구성 파일 추가

앱 등록을 클릭하면 두 구성 파일(빌드 타겟당 하나의 구성 파일)을 다운로드하라는 메시지가 표시됩니다. Unity 프로젝트에서 Firebase와 연결하려면 이러한 파일에 Firebase 메타데이터가 필요합니다.

  1. 사용 가능한 두 구성 파일을 모두 다운로드합니다.
    • Apple (iOS): GoogleService-Info.plist를 다운로드합니다.
    • Android: google-services.json을 다운로드합니다.
  2. Unity 프로젝트의 프로젝트 창을 연 다음 두 구성 파일을 모두 Assets 폴더로 이동합니다.
  3. Firebase Console로 돌아가 설정 워크플로에서 다음을 클릭하고 Unity용 Firebase SDK 추가로 진행합니다.

Unity용 Firebase SDK 추가

  1. Firebase Console에서 Firebase Unity SDK 다운로드를 클릭합니다.
  2. 편리한 위치에 SDK의 압축을 풉니다.
  3. 열려 있는 Unity 프로젝트에서 애셋 > 패키지 가져오기 > 커스텀 패키지.
  4. Import package 대화상자에서 압축을 푼 SDK가 포함된 디렉터리로 이동하여 FirebaseAnalytics.unitypackage를 선택한 후 Open을 클릭합니다.
  5. 표시된 Import Unity Package(Unity 패키지 가져오기) 대화상자에서 Import(가져오기)를 클릭합니다.
  6. 이전 단계를 반복하여 FirebaseCrashlytics.unitypackage를 가져옵니다.
  7. Firebase Console로 돌아가 설정 워크플로에서 다음을 클릭합니다.

Unity 프로젝트에 Firebase SDK를 추가하는 방법에 대한 자세한 내용은 추가 Unity 설치 옵션을 참조하세요.

4. Unity 프로젝트에서 Crashlytics 설정

Unity 프로젝트에서 Crashlytics를 사용하려면 몇 가지 설정 단계를 더 수행해야 합니다. 물론 SDK를 초기화해야 합니다. 또한 Firebase Console에서 기호화된 스택 트레이스를 볼 수 있도록 기호를 업로드해야 하며, 테스트 비정상 종료를 강제로 실행하여 Firebase에서 비정상 종료 이벤트를 수신하는지 확인해야 합니다.

Crashlytics SDK 초기화

  1. Assets/Hamster/Scripts/MainGame.cs에서 다음 using 문을 추가합니다.
    using Firebase.Crashlytics;
    using Firebase.Extensions;
    
    첫 번째 모듈을 사용하면 Crashlytics SDK의 메서드를 사용할 수 있고 두 번째 모듈에는 C# Tasks API의 일부 확장 프로그램이 포함되어 있습니다. using 문이 모두 없으면 다음 코드가 작동하지 않습니다.
  2. 여전히 MainGame.cs에서 InitializeFirebaseAndStartGame()를 호출하여 기존 Start() 메서드에 Firebase 초기화를 추가합니다.
    void Start()
    {
      Screen.SetResolution(Screen.width / 2, Screen.height / 2, true);
      InitializeFirebaseAndStartGame();
    }
    
  3. 다시 MainGame.cs에서 InitializeFirebaseAndStartGame()를 찾아 앱 변수를 선언한 후 다음과 같이 메서드의 구현을 덮어씁니다.
    public Firebase.FirebaseApp app = null;
    
    // Begins the firebase initialization process and afterwards, opens the main menu.
    private void InitializeFirebaseAndStartGame()
    {
      Firebase.FirebaseApp.CheckAndFixDependenciesAsync()
      .ContinueWithOnMainThread(
        previousTask => 
        {
          var dependencyStatus = previousTask.Result;
          if (dependencyStatus == Firebase.DependencyStatus.Available) {
            // Create and hold a reference to your FirebaseApp,
            app = Firebase.FirebaseApp.DefaultInstance;
            // Set the recommended Crashlytics uncaught exception behavior.
            Crashlytics.ReportUncaughtExceptionsAsFatal = true;
            InitializeCommonDataAndStartGame();
          } else {
            UnityEngine.Debug.LogError(
              $"Could not resolve all Firebase dependencies: {dependencyStatus}\n" +
              "Firebase Unity SDK is not safe to use here");
          }
        });
    }
    

여기에 초기화 로직을 배치하면 Firebase 종속 항목이 초기화되기 전에 플레이어 상호작용이 방지됩니다.

처리되지 않은 예외를 심각한 문제로 보고할 때의 이점과 효과는 Crashlytics FAQ에서 설명합니다.

프로젝트 빌드 및 기호 업로드

기호를 빌드하고 업로드하는 단계는 iOS 앱과 Android 앱에서 다릅니다.

iOS+ (Apple 플랫폼)

  1. Build Settings(빌드 설정) 대화상자에서 프로젝트를 Xcode 작업공간으로 내보냅니다.
  2. 앱을 빌드합니다.
    Apple 플랫폼의 경우 Crashlytics 호환 기호 파일을 생성한 후 각 빌드에 해당하는 Firebase 서버에 업로드하도록 Firebase Unity 편집기 플러그인이 Xcode 프로젝트를 자동으로 구성합니다. 이 기호 정보는 Crashlytics 대시보드에서 기호화된 스택 트레이스를 보려면 필요합니다.

Android

  1. (각 빌드가 아닌 초기 설정 중만 해당) 빌드를 설정합니다.
    1. 프로젝트 디렉터리 (즉, Assets 디렉터리의 동위 요소로서) 루트에 Builds라는 새 폴더를 만든 후 Android라는 하위 폴더를 만듭니다.
    2. 파일 > 빌드 설정 > 플레이어 설정 > 구성, 스크립팅 백엔드를 IL2CPP로 설정합니다.
      • IL2CPP를 사용하면 일반적으로 빌드가 더 작고 성능이 향상됩니다.
      • IL2CPP는 iOS에서 유일하게 사용할 수 있는 옵션이기도 하며, 여기에서 IL2CPP를 선택하면 두 플랫폼이 더 패리티가 되며 두 플랫폼 간의 디버깅 차이를 더 간단하게 만들 수 있습니다.
  2. 앱을 빌드합니다. File > Build Settings에서 다음을 완료합니다.
    1. Create symbols.zip이 선택되어 있는지 확인합니다(또는 드롭다운이 표시되면 Debugging을 선택합니다).
    2. Unity 편집기에서 바로 APK를 방금 만든 Builds/Android 하위 폴더에 빌드합니다.
  3. 빌드가 완료되면 Crashlytics 호환 기호 파일을 생성하여 Firebase 서버에 업로드해야 합니다. 이 기호 정보는 Crashlytics 대시보드에서 네이티브 라이브러리 비정상 종료에 대한 기호화된 스택 트레이스를 보려면 필요합니다.

    다음 Firebase CLI 명령어를 실행하여 이 기호 파일을 생성하고 업로드합니다.
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
    
    • FIREBASE_APP_ID: 패키지 이름이 아닌 Firebase Android 앱 ID입니다. 이전에 다운로드한 google-services.json 파일에서 이 값을 찾습니다. mobilesdk_app_id 값입니다.
      Firebase Android 앱 ID 예시: 1:567383003300:android:17104a2ced0c9b9b
    • PATH/TO/SYMBOLS: 빌드가 완료될 때 Builds/Android 디렉터리에 생성되는 압축된 기호 파일의 경로입니다 (예: Builds/Android/myapp-1.0-v100.symbols.zip).

테스트 비정상 종료를 강제로 적용하여 설정 완료

Crashlytics 설정을 완료하고 Firebase Console의 Crashlytics 대시보드에 초기 데이터를 표시하려면 테스트 비정상 종료를 강제로 실행해야 합니다.

  1. MainGameScene에서 편집기 계층 구조에서 EmptyObject GameObject를 찾아 다음 스크립트를 추가한 후 장면을 저장합니다. 이 스크립트는 앱을 실행하고 몇 초 후에 테스트 비정상 종료를 일으킵니다.
    using System;
    using UnityEngine;
    
    public class CrashlyticsTester : MonoBehaviour {
        // Update is called once per frame
        void Update()
        {
            // Tests your Crashlytics implementation by
            // throwing an exception every 60 frames.
            // You should see reports in the Firebase console
            // a few minutes after running your app with this method.
            if(Time.frameCount >0 && (Time.frameCount%60) == 0)
            {
                throw new System.Exception("Test exception; please ignore");
            }
        }
    }
    
  2. 앱을 빌드하고 빌드가 완료된 후 기호 정보를 업로드합니다.
    • iOS: Firebase Unity 편집기 플러그인이 기호 파일을 업로드하도록 Xcode 프로젝트를 자동으로 구성해 줍니다.
    • Android: Firebase CLI crashlytics:symbols:upload 명령어를 실행하여 기호 파일을 업로드합니다.
  3. 앱을 실행합니다. 앱이 실행되면 기기 로그를 확인하고 CrashlyticsTester에서 예외가 트리거될 때까지 기다립니다.
    • iOS: Xcode 하단 창에서 로그를 확인합니다.
    • Android: 터미널에서 adb logcat 명령어를 실행하여 로그를 확인합니다.
  4. Crashlytics 대시보드에서 예외를 확인하세요. 이 문제는 대시보드 하단의 문제 표에 표시됩니다. Codelab의 후반부에서 이러한 보고서를 살펴보는 방법을 자세히 알아봅니다.
  5. 이벤트가 Crashlytics에 업로드되었는지 확인한 후 이벤트를 연결한 EmptyObject GameObject를 선택하고 CrashlyticsTester 구성요소만 삭제한 다음 장면을 저장하여 원래 상태로 복원합니다.

5. 디버그 메뉴 사용 설정 및 이해

지금까지 Unity 프로젝트에 Crashlytics를 추가하고 설정을 완료했으며 Crashlytics SDK가 Firebase에 이벤트를 업로드하고 있음을 확인했습니다. 이제 Unity 프로젝트에서 게임에서 고급 Crashlytics 기능을 사용하는 방법을 보여주는 메뉴를 만듭니다. Firebase로 레벨업 Unity 프로젝트에는 표시하고 기능을 작성할 수 있는 숨겨진 디버그 메뉴가 이미 있습니다.

디버그 메뉴 사용 설정

디버그 메뉴에 액세스하는 버튼이 Unity 프로젝트에 있지만 현재 사용 설정되어 있지 않습니다. MainMenu 프리팹에서 액세스하려면 버튼을 사용 설정해야 합니다.

  1. Unity 편집기에서 MainMenu이라는 prefab을 엽니다.4148538cbe9f36c5.png
  2. 프리팹 계층 구조에서 이름이 DebugMenuButton인 사용 중지된 하위 객체를 찾아 선택합니다.816f8f9366280f6c.png
  3. DebugMenuButton이(가) 포함된 텍스트 입력란의 왼쪽 상단 모서리에 있는 체크박스를 선택하여 DebugMenuButton을(를) 사용 설정하세요.8a8089d2b4886da2.png
  4. 프리탸를 저장합니다.
  5. 편집기 또는 기기에서 게임을 실행합니다. 이제 메뉴에 액세스할 수 있습니다.

디버그 메뉴의 메서드 본문 미리보기 및 이해

이 Codelab의 후반부에서는 사전 구성된 일부 디버그 Crashlytics 메서드의 메서드 본문을 작성합니다. 하지만 Firebase로 한 단계 업그레이드 Unity 프로젝트에서는 메서드가 DebugMenu.cs에서 정의되고 여기에서 호출됩니다.

이러한 메서드 중 일부는 Crashlytics 메서드를 호출하고 오류를 발생시키지만, Crashlytics에서 이러한 오류를 포착하는 기능은 이러한 메서드를 먼저 호출하는 것에 의존하지 않습니다. 대신 오류를 자동으로 포착하여 생성된 비정상 종료 보고서는 이러한 메서드에 의해 추가된 정보로 개선됩니다.

DebugMenu.cs를 열고 다음 메서드를 찾습니다.

Crashlytics 문제를 생성하고 주석을 추가하는 방법:

  • CrashNow
  • LogNonfatalError
  • LogStringsAndCrashNow
  • SetAndOverwriteCustomKeyThenCrash
  • SetLogsAndKeysBeforeANR

디버깅에 도움이 되도록 애널리틱스 이벤트를 로깅하는 방법:

  • LogProgressEventWithStringLiterals
  • LogIntScoreWithBuiltInEventAndParams

이 Codelab의 후반부에서는 이러한 메서드를 구현하고 게임 개발 시 발생할 수 있는 특정 상황을 해결하는 데 도움이 되는 방법을 알아봅니다.

6. 개발 시 비정상 종료 보고서 전송 보장

이러한 디버그 메서드를 구현하고 비정상 종료 보고서에 미치는 영향을 확인하기 전에 이벤트가 Crashlytics에 보고되는 방식을 이해해야 합니다.

Unity 프로젝트의 경우 게임의 비정상 종료 및 예외 이벤트가 즉시 디스크에 기록됩니다. 포착되지 않은 예외 (예: 게임 로직에서 포착되지 않은 C# 예외)의 경우 Unity 프로젝트에서 Crashlytics를 초기화할 때 Crashlytics.ReportUncaughtExceptionsAsFatal 속성을 true로 설정하여 Crashlytics SDK가 심각한 이벤트로 보고하도록 할 수 있습니다. 이러한 이벤트는 최종 사용자가 게임을 다시 시작할 필요 없이 실시간으로 Crashlytics에 보고됩니다. 네이티브 비정상 종료는 항상 치명적인 이벤트로 보고되며 최종 사용자가 게임을 다시 시작할 때 함께 전송됩니다.

또한 런타임 환경마다 Crashlytics 정보를 Firebase로 전송하는 방식은 다음과 같이 작지만 중대한 차이점에 유의해야 합니다.

iOS 시뮬레이터:

  • Crashlytics 정보는 시뮬레이터에서 Xcode를 분리한 경우에만 보고됩니다. Xcode가 연결되면 업스트림 오류를 포착하여 정보 전송을 방지합니다.

휴대기기(Android 및 iOS):

  • Android 관련: ANR은 Android 11 이상에서만 보고됩니다. ANR 및 심각하지 않은 이벤트는 다음 실행에서 보고됩니다.

Unity 편집기:

CrashNow()에서 버튼을 터치하여 게임 비정상 종료 테스트

게임에 Crashlytics를 설정하면 Crashlytics SDK가 자동으로 비정상 종료 및 포착되지 않은 예외를 기록하고 분석을 위해 Firebase에 업로드합니다. 보고서는 Firebase Console의 Crashlytics 대시보드에 표시됩니다.

  1. 실제로 자동임을 보여주려면 DebugMenu.cs를 연 다음 다음과 같이 CrashNow() 메서드를 덮어씁니다.
    void CrashNow()
    {
        TestCrash();
    }
    
  2. 앱을 빌드합니다.
  3. (Android만 해당) 다음 Firebase CLI 명령어를 실행하여 기호를 업로드합니다.
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
    
  4. 지금 비정상 종료 버튼을 탭하고 이 Codelab의 다음 단계로 진행하여 비정상 종료 보고서를 보고 해석하는 방법을 알아봅니다.

7. Firebase Console의 문제 보고서 이해

비정상 종료 보고서를 최대한 활용하는 방법에 대해 알아야 할 몇 가지 사항이 더 있습니다. 작성하는 각 메서드는 Crashlytics 보고서에 다양한 유형의 정보를 추가하는 방법을 보여줍니다.

  1. 지금 비정상 종료 버튼을 탭한 다음 앱을 다시 시작합니다.
  2. Crashlytics 대시보드로 이동합니다. 대시보드 하단에 있는 Issues 표까지 아래로 스크롤합니다. 이 표에서 Crashlytics는 근본 원인이 모두 동일한 이벤트를 '문제'로 그룹화합니다.
  3. 문제 표에 표시된 새 문제를 클릭합니다. 이렇게 하면 Firebase로 전송된 각 개별 이벤트에 대한 이벤트 요약이 표시됩니다.

    다음과 같은 스크린샷이 표시됩니다. 이벤트 요약에 비정상 종료의 원인이 된 호출의 스택 트레이스가 눈에 띄게 표시됩니다.40c96abe7f90c3aa.png

추가 메타데이터

또 다른 유용한 탭은 Unity 메타데이터 탭입니다. 이 섹션에서는 물리적 특징, CPU 모델/사양 및 모든 종류의 GPU 측정항목을 비롯하여 이벤트가 발생한 기기의 속성에 대해 알려줍니다.

다음은 이 탭의 정보가 유용할 수 있는 예입니다.
게임에서 특정 모양을 위해 셰이더를 많이 사용하지만 모든 휴대전화에 이 기능을 렌더링할 수 있는 GPU가 있지는 않다고 상상해 보세요. Unity 메타데이터 탭에 있는 정보를 사용하면 자동으로 사용하거나 완전히 사용 중지할 기능을 결정할 때 앱에서 테스트해야 하는 하드웨어를 더 잘 파악할 수 있습니다.

기기에서는 버그나 비정상 종료가 발생하지 않을 수도 있지만, 실제로 사용 중인 Android 기기의 다양성이 매우 크기 때문에 잠재고객 기기의 특정 '핫스팟'을 더 잘 이해하는 데 도움이 됩니다.

41d8d7feaa87454d.png

8. 예외 발생, 포착, 로깅

종종 개발자는 코드가 런타임 예외를 올바르게 포착하고 처리하더라도 그와 어떤 상황에서 그러한 예외가 발생했음을 확인하는 것이 좋습니다. Crashlytics.LogException는 바로 이 목적으로 사용할 수 있습니다. 즉, Firebase Console에서 문제를 추가로 디버그할 수 있도록 예외 이벤트를 Firebase에 전송합니다.

  1. Assets/Hamster/Scripts/States/DebugMenu.cs에서 using 문에 다음을 추가합니다.
    // Import Firebase
    using Firebase.Crashlytics;
    
  2. 계속 DebugMenu.cs에서 다음과 같이 LogNonfatalError()를 덮어씁니다.
    void LogNonfatalError()
    {
        try
        {
            throw new System.Exception($"Test exception thrown in {nameof(LogNonfatalError)}");
        }
        catch(System.Exception exception)
        {
            Crashlytics.LogException(exception);
        }
    }
    
  3. 앱을 빌드합니다.
  4. (Android만 해당) 다음 Firebase CLI 명령어를 실행하여 기호를 업로드합니다.
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
    
  5. Log Nonfatal Error(비심각한 오류 로깅) 버튼을 탭한 다음 앱을 다시 시작합니다.
  6. Crashlytics 대시보드로 이동하면 이 Codelab의 마지막 단계에서 본 것과 유사한 내용이 표시됩니다.
  7. 하지만 이번에는 이벤트 유형 필터를 심각하지 않은 문제로 제한하여 방금 로깅한 것과 같은 심각하지 않은 오류만 표시되도록 합니다.
    a39ea8d9944cbbd9.png

9. Crashlytics에 문자열을 로깅하여 프로그램 실행 흐름을 더 잘 이해합니다.

세션당 수백 번은 아니더라도 여러 경로에서 호출되는 코드 줄이 갑자기 예외나 충돌을 일으킬 수 있는 이유를 알아내려고 해 본 적이 있나요? IDE에서 코드를 단계별로 살펴보고 값을 자세히 살펴보는 것이 좋지만, 이러한 문제가 극소수의 사용자에게만 발생하는 경우는 어떻게 해야 하나요? 게다가 어떤 행동을 하든 이 비정상 종료를 재현할 수 없다면 어떻게 해야 할까요?

이와 같은 상황에서는 어느 정도 맥락을 알고 있으면 큰 차이를 만들 수 있습니다. Crashlytics.Log를 사용하면 필요한 컨텍스트를 작성할 수 있습니다. 이러한 메시지는 미래의 나에게 어떤 일이 일어나고 있는지 알려주는 힌트라고 생각해 보세요.

로그는 다양한 방식으로 사용할 수 있지만 일반적으로 호출의 순서 또는 부재가 매우 중요한 정보인 상황에서 기록하는 데 가장 유용합니다.

  1. Assets/Hamster/Scripts/States/DebugMenu.cs에서 다음과 같이 LogStringsAndCrashNow()를 덮어씁니다.
    void LogStringsAndCrashNow()
    {
        Crashlytics.Log($"This is the first of two descriptive strings in {nameof(LogStringsAndCrashNow)}");
        const bool RUN_OPTIONAL_PATH = false;
        if(RUN_OPTIONAL_PATH)
        {
            Crashlytics.Log(" As it stands, this log should not appear in your records because it will never be called.");
        }
        else
        {
            Crashlytics.Log(" A log that will simply inform you which path of logic was taken. Akin to print debugging.");
        }
        Crashlytics.Log($"This is the second of two descriptive strings in {nameof(LogStringsAndCrashNow)}");
        TestCrash();
    }
    
  2. 앱을 빌드합니다.
  3. (Android만 해당) 다음 Firebase CLI 명령어를 실행하여 기호를 업로드합니다.
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
    
  4. Log Strings and Crash Now(문자열 로깅 및 지금 비정상 종료) 버튼을 탭한 다음 앱을 다시 시작합니다.
  5. Crashlytics 대시보드로 돌아가 문제 표에 나열된 최신 문제를 클릭합니다. 이전 문제와 유사한 내용이 다시 표시됩니다.
    7aabe103b8589cc7.png
  6. 하지만 이벤트 요약 내에서 로그 탭을 클릭하면 다음과 같은 보기가 표시됩니다.
    4e27aa407b7571cf.png

10. 맞춤 키 쓰기 및 덮어쓰기

소수의 값 또는 구성으로 설정된 변수에 해당하는 비정상 종료를 더 잘 파악하고 싶다고 가정해 보겠습니다. 특정 시점에 보고 있는 변수와 가능한 값의 조합을 기반으로 필터링할 수 있으면 좋을 것입니다.

Crashlytics는 임의의 문자열을 로깅하는 것 외에도 프로그램이 비정상 종료될 때 프로그램의 정확한 상태를 파악하는 것이 유용한 경우 또 다른 디버깅 형식인 커스텀 키를 제공합니다.

세션에 설정할 수 있는 키-값 쌍입니다. 누적되며 완전히 추가되는 로그와 달리, 변수 또는 조건의 최신 상태만 반영하도록 키를 덮어쓸 수 있습니다.

이러한 키는 프로그램의 마지막으로 기록된 상태의 원장일 뿐만 아니라 Crashlytics 문제의 강력한 필터로 사용할 수도 있습니다.

  1. Assets/Hamster/Scripts/States/DebugMenu.cs에서 다음과 같이 SetAndOverwriteCustomKeyThenCrash()를 덮어씁니다.
    void SetAndOverwriteCustomKeyThenCrash()
    {
        const string CURRENT_TIME_KEY = "Current Time";
        System.TimeSpan currentTime = System.DateTime.Now.TimeOfDay;
        Crashlytics.SetCustomKey(
            CURRENT_TIME_KEY,
            DayDivision.GetPartOfDay(currentTime).ToString() // Values must be strings
            );
    
        // Time Passes
        currentTime += DayDivision.DURATION_THAT_ENSURES_PHASE_CHANGE;
    
        Crashlytics.SetCustomKey(
            CURRENT_TIME_KEY,
            DayDivision.GetPartOfDay(currentTime).ToString()
            );
        TestCrash();
    }
    
  2. 앱을 빌드합니다.
  3. (Android만 해당) 다음 Firebase CLI 명령어를 실행하여 기호를 업로드합니다.
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
    
  4. 맞춤 키 및 비정상 종료 설정 버튼을 탭한 후 앱을 다시 시작합니다.
  5. Crashlytics 대시보드로 돌아가 문제 표에 나열된 최신 문제를 클릭합니다. 다시 이전 문제와 비슷한 내용이 표시됩니다.
  6. 하지만 이번에는 이벤트 요약에서 탭을 클릭하면 Current Time:
    7dbe1eb00566af98.png을 포함한 키 값을 확인할 수 있습니다.

맞춤 로그 대신 맞춤 키를 사용해야 하는 이유는 무엇인가요?

  • 로그는 순차 데이터를 저장하는 데 적합하지만 최신 값만 원하는 경우 맞춤 키를 사용하는 것이 좋습니다.
  • Firebase Console에서 문제 테이블 검색창의 키 값을 기준으로 문제를 쉽게 필터링할 수 있습니다.

하지만 로그와 마찬가지로 커스텀 키에는 한도가 있습니다. Crashlytics는 최대 64개의 키-값 쌍을 지원합니다. 이 기준에 도달한 후에는 추가 값이 저장되지 않습니다. 각 키-값 쌍의 최대 크기는 1KB입니다.

11. (Android만 해당) 맞춤 키와 로그를 사용하여 ANR 이해 및 진단

Android 개발자가 디버그하기 가장 어려운 문제 클래스 중 하나는 애플리케이션 응답 없음 (ANR) 오류입니다. ANR은 앱이 5초 이상 입력에 응답하지 않을 때 발생합니다. 이 경우 앱이 정지되었거나 매우 느리게 실행되고 있는 것입니다. 사용자에게 대화상자가 표시되며 사용자는 '대기' 여부를 선택할 수 있습니다. 또는 '앱 닫기'를 선택합니다.

ANR은 좋지 않은 사용자 환경이며 (위의 ANR 링크에서 설명했듯이) Google Play 스토어에서 앱의 검색 가능 여부에 영향을 줄 수 있습니다. ANR은 복잡하고 휴대전화 모델마다 동작이 매우 다른 멀티스레드 코드로 인해 발생하는 경우가 많기 때문에 디버깅 중에 ANR을 재현하기가 거의 불가능하지는 않지만 매우 어려운 경우가 많습니다. 따라서 분석적이고 추론적인 방식으로 접근하는 것이 일반적으로 가장 좋습니다.

이 메서드에서는 Crashlytics.LogException, Crashlytics.Log, Crashlytics.SetCustomKey를 조합하여 자동 문제 로깅을 보완하고 더 많은 정보를 제공합니다.

  1. Assets/Hamster/Scripts/States/DebugMenu.cs에서 다음과 같이 SetLogsAndKeysBeforeANR()를 덮어씁니다.
    void SetLogsAndKeysBeforeANR()
    {
        System.Action<string,long> WaitAndRecord =
        (string methodName, long targetCallLength)=>
        {
            System.Diagnostics.Stopwatch stopWatch = new System.Diagnostics.Stopwatch();
            const string CURRENT_FUNCTION = "Current Async Function";
    
            // Initialize key and start timing
            Crashlytics.SetCustomKey(CURRENT_FUNCTION, methodName);
            stopWatch.Start();
    
            // The actual (simulated) work being timed.
            BusyWaitSimulator.WaitOnSimulatedBlockingWork(targetCallLength);
    
            // Stop timing
            stopWatch.Stop();
    
            if(stopWatch.ElapsedMilliseconds>=BusyWaitSimulator.EXTREME_DURATION_MILLIS)
            {
              Crashlytics.Log($"'{methodName}' is long enough to cause an ANR.");
            }
            else if(stopWatch.ElapsedMilliseconds>=BusyWaitSimulator.SEVERE_DURATION_MILLIS)
            {
              Crashlytics.Log($"'{methodName}' is long enough it may cause an ANR");
            }
        };
    
        WaitAndRecord("DoSafeWork",1000L);
        WaitAndRecord("DoSevereWork",BusyWaitSimulator.SEVERE_DURATION_MILLIS);
        WaitAndRecord("DoExtremeWork",2*BusyWaitSimulator.EXTREME_DURATION_MILLIS);
    }
    
  2. 앱을 빌드합니다.
  3. 다음 Firebase CLI 명령어를 실행하여 기호를 업로드합니다.
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
    
  4. 로그 및 키 설정 → ANR 버튼을 탭한 다음 앱을 다시 시작합니다.
  5. Crashlytics 대시보드로 돌아간 다음 문제 표에서 새로운 문제를 클릭하여 이벤트 요약을 확인합니다. 호출이 제대로 진행되면 다음과 같이 표시됩니다.
    876c3cff7037bd07.png

    보시다시피 Firebase는 스레드의 사용량이 많은 대기를 앱에서 ANR을 트리거한 주된 원인으로 포착했습니다.
  6. 이벤트 요약로그 탭에서 로그를 보면 완료로 기록된 마지막 메서드가 DoSevereWork임을 알 수 있습니다.
    5a4bec1cf06f6984.png

    반면 시작으로 표시된 마지막 메서드는 DoExtremeWork입니다. 이는 이 메서드 중에 ANR이 발생했으며 DoExtremeWork를 기록하기 전에 게임이 종료되었음을 나타냅니다.

    89d86d5f598ecf3a.png

Why do this?

  • ANR을 재현하는 것은 매우 어렵기 때문에 코드 영역과 측정항목에 관한 풍부한 정보를 얻을 수 있으면 이를 추론적으로 파악하는 데 매우 중요합니다.
  • 이제 맞춤 키에 저장된 정보를 통해 실행 시간이 가장 긴 비동기 스레드와 ANR을 트리거할 위험이 있는 비동기 스레드를 파악할 수 있습니다. 이러한 종류의 관련 논리적 및 숫자 데이터는 최적화가 가장 필요한 코드 위치를 보여줍니다.

12. 애널리틱스 이벤트를 삽입하여 보고서를 더욱 풍부하게 만들기

다음 메서드는 디버그 메뉴에서도 호출할 수 있지만, 문제를 자체적으로 생성하는 대신 Google 애널리틱스를 다른 정보 소스로 사용하여 게임 작동을 더 잘 이해할 수 있습니다.

이 Codelab에서 작성한 다른 메서드와 달리 이러한 메서드는 다른 메서드와 함께 사용해야 합니다. 다른 메서드 중 하나를 실행하기 전에 원하는 임의의 순서로 디버그 메뉴에서 해당 버튼을 눌러 이러한 메서드를 호출합니다. 그런 다음 특정 Crashlytics 문제의 정보를 검토하면 정렬된 애널리틱스 이벤트 로그가 표시됩니다. 이 데이터는 앱을 계측한 방식에 따라 게임에서 프로그램 흐름 또는 사용자 입력의 조합을 더 잘 이해하는 데 사용할 수 있습니다.

  1. Assets/Hamster/Scripts/States/DebugMenu.cs에서 다음 메서드의 기존 구현을 덮어씁니다.
    public void LogProgressEventWithStringLiterals()
    {
          Firebase.Analytics.FirebaseAnalytics.LogEvent("progress", "percent", 0.4f);
    }
    
    public void LogIntScoreWithBuiltInEventAndParams()
    {
          Firebase.Analytics.FirebaseAnalytics
            .LogEvent(
              Firebase.Analytics.FirebaseAnalytics.EventPostScore,
              Firebase.Analytics.FirebaseAnalytics.ParameterScore,
              42
            );
    }
    
  2. 게임을 빌드 및 배포한 다음 디버그 메뉴를 입력합니다.
  3. (Android만 해당) 다음 Firebase CLI 명령어를 실행하여 기호를 업로드합니다.
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
    
  4. 다음 버튼 중 하나 이상을 한 번 이상 눌러 위 함수를 호출합니다.
    • 로그 문자열 이벤트
    • 로그인 이벤트
  5. 지금 비정상 종료 버튼을 누릅니다.
  6. 비정상 종료 이벤트를 Firebase에 업로드하려면 게임을 다시 시작합니다.
  7. 애널리틱스 이벤트의 임의의 다양한 시퀀스를 로깅한 다음 게임에서 Crashlytics가 보고서를 생성하는 이벤트를 생성하면(방금 수행한 것처럼) 다음과 같이 Crashlytics 이벤트 요약로그 탭에 추가됩니다.
    d3b16d78f76bfb04.png

13. 앞으로의 전망

이를 통해 자동으로 생성된 비정상 종료 보고서를 보완할 더 나은 이론적 근거를 마련할 수 있게 되었습니다. 이 새로운 정보를 사용하면 현재 상태, 이전 이벤트 기록, 기존 Google 애널리틱스 이벤트를 통해 결과로 이어진 이벤트 시퀀스와 로직을 더 잘 분석할 수 있습니다.

앱이 Android 11(API 수준 30) 이상을 타겟팅하는 경우 use-after-freeheap-buffer-overflow 버그와 같은 네이티브 메모리 오류로 인한 비정상 종료를 디버깅하는 데 유용한 네이티브 메모리 할당자 기능인 GWP-ASan을 통합해 보세요. 이 디버깅 기능을 활용하려면 GWP-ASan을 명시적으로 사용 설정하세요.

다음 단계

원격 구성으로 Unity 게임 계측 Codelab으로 이동하여 Unity에서 원격 구성 및 A/B 테스팅을 사용하는 방법을 알아봅니다.