1. 개요

 

 

 

Firebase Authentication을 활용하여 구글 로그인과 게스트 로그인을 구현했습니다.

이를 통해 사용자 인증 시스템을 구축하고, 에디터 모드에서의 테스트를 위한 별도의 로직도 함께 구현했습니다.

처음해보는거라 2일이나 걸리긴 했지만, 정~~말 많은 버그를 직면하며 공부에 많은 도움이 되었습니다.

 

2. 구현내용

 

2.1 주요 기능

 

  • Firebase 초기화 및 인증 시스템 구축
  • 구글 로그인 구현
  • 게스트 로그인 구현
  • 에디터 전용 테스트 로그인
  • 로딩 UI 및 상태 표시

 

2.2 각 클래스의 역할

 

2.2.1 FirebaseManager

 

  • Firebase 초기화 및 인증 관리
  • 구글 로그인/게스트 로그인 처리
  • 에디터 모드 지원
using Firebase.Auth;
using Google;
using System;
using System.Threading.Tasks;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

/// <summary>
/// Firebase 인증 관련 기능을 관리하는 매니저 클래스
/// </summary>
public class FirebaseManager : IManager
{
    private FirebaseAuth auth;
    private GoogleSignInConfiguration googleSignInConfig;
    private bool isInitialized = false;

    public bool IsInitialized => isInitialized;
    public bool IsLoggedIn => auth?.CurrentUser != null;
    public string UserId => auth?.CurrentUser?.UserId;
    public GoogleSignInConfiguration GoogleSignInConfig => googleSignInConfig;

    /// <summary>
    /// Firebase 및 Google SignIn을 비동기로 초기화합니다.
    /// </summary>
    public async Task InitializeAsync()
    {
#if UNITY_EDITOR
        isInitialized = true;
        await Task.CompletedTask;
        return;
#else
        if (isInitialized) return;

        try
        {
            Logger.Log("Firebase 초기화 시작...");

            var dependencyStatus = await Firebase.FirebaseApp.CheckAndFixDependenciesAsync();
            if (dependencyStatus != Firebase.DependencyStatus.Available)
            {
                throw new Exception($"Firebase 의존성 문제 발생: {dependencyStatus}");
            }

            var app = Firebase.FirebaseApp.DefaultInstance;
            if (app == null)
            {
                app = Firebase.FirebaseApp.Create();
            }

            auth = FirebaseAuth.DefaultInstance;
            if (auth == null)
            {
                throw new Exception("Firebase Auth 인스턴스를 가져올 수 없습니다.");
            }

            // Google SignIn 설정
            googleSignInConfig = new GoogleSignInConfiguration
            {
                WebClientId = "624195215716-87brqui6v1okgoc65tkmbmhp9nlivged.apps.googleusercontent.com",
                RequestEmail = true,
                RequestIdToken = true,
                RequestProfile = true,
                UseGameSignIn = false
            };

            isInitialized = true;
            Logger.Log("Firebase 초기화 성공");
        }
        catch (Exception ex)
        {
            isInitialized = false;
            Logger.ErrorLog($"Firebase 초기화 실패: {ex.Message}\n{ex.StackTrace}");
            throw;
        }
#endif
    }

    /// <summary>
    /// 구글 로그인을 수행합니다
    /// </summary>
    public async Task<bool> GoogleSignInAsync()
    {
        if (!isInitialized)
        {
            Logger.ErrorLog("Firebase가 초기화되지 않았습니다.");
            AndroidLogger.ShowToast("연결에 실패했습니다.");
            return false;
        }

        try
        {
            AndroidLogger.ShowToast("로그인 중...");

            if (auth.CurrentUser != null)
            {
                auth.SignOut();
            }

            if (GoogleSignIn.Configuration == null)
            {
                GoogleSignIn.Configuration = googleSignInConfig;
            }

            var signInTask = GoogleSignIn.DefaultInstance.SignIn();

            try
            {
                var googleUser = await signInTask;

                if (googleUser == null)
                {
                    Logger.ErrorLog("구글 로그인 실패: 사용자 정보가 null입니다.");
                    AndroidLogger.ShowToast("로그인에 실패했습니다.");
                    return false;
                }

                AndroidLogger.ShowToast("인증 중...");

                if (string.IsNullOrEmpty(googleUser.IdToken))
                {
                    Logger.ErrorLog("IdToken이 null이거나 비어있습니다.");
                    AndroidLogger.ShowToast("인증에 실패했습니다.");
                    return false;
                }

                var credential = GoogleAuthProvider.GetCredential(googleUser.IdToken, null);
                var result = await auth.SignInWithCredentialAsync(credential);

                if (result == null)
                {
                    Logger.ErrorLog("Firebase 인증 결과가 null입니다.");
                    AndroidLogger.ShowToast("인증에 실패했습니다.");
                    return false;
                }

                AndroidLogger.ShowToast("로그인 성공!");
                return true;
            }
            catch (Exception signInEx)
            {
                Logger.ErrorLog($"Google SignIn 실패: {signInEx.Message}");
                AndroidLogger.ShowToast("로그인에 실패했습니다.");
                return false;
            }
        }
        catch (Exception ex)
        {
            Logger.ErrorLog($"Google 로그인 실패: {ex.Message}");
            AndroidLogger.ShowToast("로그인에 실패했습니다.");
            return false;
        }
    }

    /// <summary>
    /// 게스트 로그인을 수행합니다
    /// </summary>
    public async Task<bool> GuestSignInAsync()
    {
        if (!isInitialized)
        {
            Logger.ErrorLog("Firebase가 초기화되지 않았습니다.");
            return false;
        }

        try
        {
            Logger.Log("게스트 계정 생성 중...");
            var result = await auth.SignInAnonymouslyAsync();

            if (result == null)
            {
                Logger.ErrorLog("게스트 로그인 결과가 null입니다.");
                return false;
            }

            Logger.Log($"게스트 로그인 성공: {result.User.UserId}");
            return true;

        }
        catch (Exception ex)
        {
            Logger.ErrorLog($"게스트 로그인 실패: {ex.Message}\n{ex.StackTrace}");
            return false;
        }
    }

    /// <summary>
    /// 로그아웃을 수행합니다
    /// </summary>
    public void SignOut()
    {
        try
        {
            if (auth?.CurrentUser != null)
            {
                auth.SignOut();
            }

            if (GoogleSignIn.DefaultInstance != null)
            {
                GoogleSignIn.DefaultInstance.SignOut();
                Logger.Log("로그아웃 성공");
            }
        }
        catch (Exception ex)
        {
            Logger.ErrorLog($"로그아웃 중 오류 발생: {ex.Message}");
        }
    }

#if UNITY_EDITOR
    /// <summary>
    /// 에디터 전용 - 더미 사용자 ID 반환
    /// </summary>
    public string EditorUserId => "editor_user_" + System.DateTime.Now.Ticks;
#endif
}

 

2.3.2 UITitleScene

 

  • 로그인 UI 관리
  • 로딩 상태 표시
  • 플랫폼별 UI 분기 처리
using UnityEngine;
using UnityEngine.UI;
using System.Threading.Tasks;
using System;
using TMPro;
using Google;
#if UNITY_EDITOR
using UnityEditor;
#endif

/// <summary>
/// 타이틀 씬의 UI를 관리하는 클래스
/// </summary>
public class UITitleScene : UIBase
{
    [SerializeField] private Button googleSignInButton;
    [SerializeField] private Button guestSignInButton;

#if UNITY_EDITOR
    [Header("에디터 전용")]
    [SerializeField] private Button editorSignInButton;
#endif

    [Header("공통 UI")]
    [SerializeField] private GameObject loadingPanel;
    [SerializeField] private Image loadingImage;
    [SerializeField] private TextMeshProUGUI statusText;

    private float rotateSpeed = 180f; // 1초에 180도 회전
    private float blinkSpeed = 2f; // 1초에 2번 깜박임
    private float currentAlpha = 1f;

    protected override void Awake()
    {
        base.Awake();
        InitializeUI();
    }

    /// <summary>
    /// UI 초기 설정을 수행합니다.
    /// </summary>
    private void InitializeUI()
    {
#if UNITY_EDITOR
        // 에디터 모드 UI 설정
        googleSignInButton.gameObject.SetActive(false);
        guestSignInButton.gameObject.SetActive(false);
        editorSignInButton.gameObject.SetActive(true);
        editorSignInButton.onClick.AddListener(EditorSignIn);

        // 에디터 모드에서는 바로 버튼 활성화
        loadingPanel.SetActive(false);
        editorSignInButton.interactable = true;
#else
        // 빌드 모드 UI 설정
        googleSignInButton.gameObject.SetActive(true);
        guestSignInButton.gameObject.SetActive(true);
        googleSignInButton.onClick.AddListener(() => SignInAsync(true));
        guestSignInButton.onClick.AddListener(() => SignInAsync(false));

        loadingPanel.SetActive(true);
        statusText.text = "게임 준비 중...";
        CheckInitialization();
#endif
    }

    private void Update()
    {
        if (loadingPanel.activeSelf)
        {
            // 로딩 이미지 회전
            loadingImage.transform.Rotate(0f, 0f, -rotateSpeed * Time.deltaTime);

            // 깜박임 효과
            currentAlpha = Mathf.PingPong(Time.time * blinkSpeed, 1f);
            Color color = loadingImage.color;
            color.a = currentAlpha;
            loadingImage.color = color;
        }
    }

    /// <summary>
    /// Firebase 및 Google SignIn 초기화를 확인하고 수행합니다.
    /// </summary>
    private async void CheckInitialization()
    {
        try
        {
            statusText.text = "게임 초기화 중...";
            await Managers.Firebase.InitializeAsync();

            if (Managers.Firebase.IsInitialized)
            {
                GoogleSignIn.Configuration = Managers.Firebase.GoogleSignInConfig;
            }

            googleSignInButton.interactable = true;
            guestSignInButton.interactable = true;
            loadingPanel.SetActive(false);
        }
        catch (Exception ex)
        {
            statusText.text = "연결 실패. 게스트로 시작해주세요.";
            Logger.ErrorLog($"초기화 실패: {ex.Message}\n{ex.StackTrace}");

            googleSignInButton.interactable = false;
            guestSignInButton.interactable = true;
            loadingPanel.SetActive(false);
        }
    }

    private async void SignInAsync(bool isGoogle)
    {
        loadingPanel.SetActive(true);
        statusText.text = "로그인 중...";

        bool success;
        try
        {
            if (isGoogle)
            {
                success = await Managers.Firebase.GoogleSignInAsync();
            }
            else
            {
                success = await Managers.Firebase.GuestSignInAsync();
            }

            if (success)
            {
                statusText.text = "로그인 성공!";
                await Task.Delay(1000);
                Managers.Scene.LoadScene(EnumTypes.SceneType.GameScene);
            }
            else
            {
                statusText.text = "로그인에 실패했습니다.";
                await Task.Delay(1000);
                loadingPanel.SetActive(false);
            }
        }
        catch (Exception ex)
        {
            statusText.text = "로그인에 실패했습니다.";
            Logger.ErrorLog($"로그인 중 오류 발생: {ex.Message}");
            await Task.Delay(1000);
            loadingPanel.SetActive(false);
        }
    }

#if UNITY_EDITOR
    /// <summary>
    /// 에디터 전용 로그인 처리
    /// </summary>
    private async void EditorSignIn()
    {
        loadingPanel.SetActive(true);
        statusText.text = "에디터 모드로 진입 중...";

        await Task.Delay(1000);
        Managers.Scene.LoadScene(EnumTypes.SceneType.GameScene);
    }
#endif
}

 

2.3.3 TitleScene

 

  • 씬 초기화 및 기본 설정
  • 윈도우 설정
  • UI 표시 관리
#if UNITY_STANDALONE_WIN
using System;
using System.Runtime.InteropServices;
#endif
using UnityEngine;

/// <summary>
/// 타이틀 씬의 기본 동작을 관리하는 클래스
/// </summary>
public class TitleScene : SceneBase
{
    protected override void OnSceneLoad()
    {
        // 기본 설정
        SetupBasicSettings();
    }

    /// <summary>
    /// 기본 설정을 초기화합니다.
    /// </summary>
    private void SetupBasicSettings()
    {
        if (Application.platform == RuntimePlatform.WindowsPlayer)
        {
            Screen.SetResolution(720, 1280, FullScreenMode.Windowed);
        }

#if UNITY_STANDALONE_WIN
        UpdateWindowTitle();
#endif
        Application.targetFrameRate = 60;
        Managers.Locale.Init(LocaleManager.DEFAULT_TIME_ZONE);
    }

    protected override void OnSceneLoaded()
    {
        Managers.UI.Show<UITitleScene>();
    }
    protected override void OnSceneUnload()
    {
        Managers.UI.Hide<UITitleScene>();

    }
#if UNITY_STANDALONE_WIN
    // 윈도우 타이틀 변경 함수
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool SetWindowText(IntPtr hWnd, string lpString);

    // 현재 활성화된 윈도우의 핸들 가져오기
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetActiveWindow();

    private void UpdateWindowTitle()
    {
        // 활성화된 윈도우 핸들을 가져온 뒤, 원하는 타이틀로 변경
        IntPtr windowHandle = GetActiveWindow();
        SetWindowText(windowHandle, $"{Application.productName} (ver.{Application.version})");
    }
#endif
}

 

3. 주요 구현 포인트

 

3.1 Firebase 초기화

 

  • Firebase SDK 의존성 체크
  • 인증 인스턴스 초기화
  • 구글 로그인 설정

 

3.2 플랫폼별 분기 처리

 

  • 에디터 모드와 빌드 모드 구분
  • 조건부 컴파일 활용
  • 플랫폼별 적절한 UI 표시

 

3.3 비동기 처리

 

  • async/await 패턴 활용
  • 로딩 상태 처리
  • 예외 처리

 

3.4 UI 개선

 

  • 사용자 친화적인 로딩 상태 메세지
  • 로딩 애니메이션(회전, 깜빡임)
  • 적절한 피드백 제공