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 개선
- 사용자 친화적인 로딩 상태 메세지
- 로딩 애니메이션(회전, 깜빡임)
- 적절한 피드백 제공