고급 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 편집기에서 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 프로젝트를 열고 Assets(애셋) > Import Package(패키지 가져오기) > Custom Package(커스텀 패키지)로 이동합니다.
  4. Import package 대화상자에서 압축을 푼 SDK가 포함된 디렉터리로 이동하여 FirebaseAnalytics.unitypackage를 선택한 다음 Open을 클릭합니다.
  5. Import Unity Package 대화상자가 표시되면 Import를 클릭합니다.
  6. 이전 단계를 반복하여 FirebaseCrashlytics.unitypackage를 가져옵니다.
  7. Firebase Console로 돌아가 설정 워크플로에서 다음을 클릭합니다.

Firebase SDK를 Unity 프로젝트에 추가하는 방법에 관한 자세한 내용은 추가 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()를 호출하여 Firebase 초기화를 기존 Start() 메서드에 추가합니다.
    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 플랫폼의 경우 Firebase Unity 편집기 플러그인이 Xcode 프로젝트를 자동으로 구성하여 각 빌드에 대해 Crashlytics 호환 기호 파일을 생성하고 Firebase 서버에 업로드합니다. Crashlytics 대시보드에서 기호화된 스택 트레이스를 보려면 이 기호 정보가 필요합니다.

Android

  1. (각 빌드가 아닌 초기 설정 중에만) 빌드를 설정합니다.
    1. 프로젝트 디렉터리의 루트 (즉, Assets 디렉터리의 동위)에 Builds라는 새 폴더를 만든 다음 Android라는 하위 폴더를 만듭니다.
    2. File > Build Settings > Player Settings > Configuration에서 Scripting Backend를 IL2CPP로 설정합니다.
      • IL2CPP를 사용하면 일반적으로 빌드가 더 작고 성능이 향상됩니다.
      • IL2CPP는 iOS에서 유일하게 사용할 수 있는 옵션이며 여기에서 선택하면 두 플랫폼의 패리티가 향상되고 두 플랫폼 간의 디버깅 차이를 더 간단하게 만들 수 있습니다.
  2. 앱을 빌드합니다. File > Build Settings에서 다음을 완료합니다.
    1. CreateSYMBOL.zip이 선택되어 있는지 확인합니다 (또는 드롭다운이 표시되면 디버깅을 선택함).
    2. Unity Editor에서 방금 만든 Builds/Android 하위 폴더로 APK를 직접 빌드합니다.
  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. MainGameSceneHierarchy 편집기에서 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. 디버그 메뉴 사용 설정 및 이해하기

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

디버그 메뉴 사용 설정

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

  1. Unity 편집기에서 MainMenu라는 프리패브를 엽니다.4148538cbe9f36c5.png
  2. prefab 계층 구조에서 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 시뮬레이터:

  • 시뮬레이터에서 Xcode를 분리하는 경우에만 Crashlytics 정보가 보고됩니다. 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. Crash Now 버튼을 탭하고 이 Codelab의 다음 단계로 진행하여 비정상 종료 보고서를 보고 해석하는 방법을 알아봅니다.

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

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

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

    다음과 같은 스크린샷이 표시됩니다. Event summary에 비정상 종료를 야기한 호출의 스택 트레이스가 어떻게 눈에 띄게 표시되는지 확인하세요.40c96abe7f90c3aa.png

추가 메타데이터

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

이 탭의 정보가 유용할 수 있는 예를 소개합니다.
게임에서 특정 모양을 얻기 위해 셰이더를 많이 사용하지만 모든 휴대전화에 이 기능을 렌더링할 수 있는 GPU가 있는 것은 아니라고 가정해 보겠습니다. Unity Metadata 탭의 정보를 참고하면 어떤 기능을 자동으로 사용 설정하거나 완전히 사용 중지할지 결정할 때 앱에서 테스트해야 하는 하드웨어를 파악할 수 있습니다.

기기에서는 버그나 비정상 종료가 절대 발생하지 않지만 실제로 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. 심각하지 않은 오류 로그 버튼을 탭하고 앱을 다시 시작합니다.
  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 대시보드로 돌아가서 Issues 테이블에 나열된 최신 문제를 클릭합니다. 이전 문제와 유사한 내용이 다시 표시됩니다.
    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 대시보드로 돌아가서 Issues 테이블에 나열된 최신 문제를 클릭합니다. 이전 문제와 유사한 내용이 다시 표시됩니다.
  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 대시보드로 돌아간 다음 Issues 테이블에서 새 문제를 클릭하여 이벤트 요약을 확인합니다. 호출이 제대로 진행되면 다음과 같은 내용이 표시됩니다.
    876c3cff7037bd07.png

    보시다시피 Firebase는 앱이 ANR을 트리거한 주된 이유로 스레드의 바쁨 대기를 정확히 지정했습니다.
  6. 이벤트 요약로그 탭에서 로그를 보면 완료로 기록된 마지막 메서드는 DoSevereWork입니다.
    5a4bec1cf06f6984.png

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

    89d86d5f598ecf3a.png

Why do this?

  • ANR을 재현하는 것은 매우 어려운 일이므로 코드 영역과 측정항목에 관한 풍부한 정보를 얻을 수 있는 것은 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 테스팅을 사용하는 방법을 알아봅니다.