네트워크 게임을 플레이할 때 게임의 정보가 저장되어 있는 경우를 많이 보았을 것이다. 계정 정보에 따라 플레이어의 레벨, 스텟, 장비 등이 저장되어 있고 다른 환경에서 접속하여도 게임의 진행 상황 및 데이터가 저장되어 있을 것이다. 이러한 역할을 하는 것이 데이터베이스이다.
Firebase는 어플리케이션 개발에 필요한 백엔드 플랫폼이다.
게임의 경우 클라이언트 뿐만 아니라 게임을 구성하기 위한 다양한 백엔드 플랫폼 기능들이 필요하며, Firebase는 다양한 기능의 백엔드 기능을 제공하여 개발자에서 클라이언트 개발에 집중할 수 있도록 한다.
Firebase의 기능은 상당히 많으며 이 중에서 원하는 기능을 포함하여 구성할 수 있다.
많은 기능 중 일부 기능에 대해 알아보자
사용자가 해당 앱에 접속하는 기기의 종류가 다양하고, 비정상적인 방식으로 접속할 수도 있다. 여기서 앱이 정상 작동하는지 유효성 검사를 돕는 기능이다.
앱에서 필요한 권한 요소를 유저에게 요구하고, 보안을 위한 요소를 확인할 수 있는 기능이다.
앱 내에 필요한 데이터베이스를 Realtime으로 관리할 수 있는 기능
파이어베이스를 사용하는 이유는 소규모 프로젝트에 쓰기에 적합한 방식이며, 사용 점유율 자체가 높기 때문이다. 저코스트로 사용할 수 있기 때문에 사용하는 방식이나, 다른 플랫폼에 대해서 알아보자
대규모 프로젝트에서 많이 쓰이는 서비스이다. 다양한 기능을 제공해주나 단점으로는 비용이 제법 들어간다.
쓰이는 경우가 꽤 있다고 한다. 장점으로는 무료로 사용하다가 정식 서비스 단계쯤 되었을 때 편하게 유료 전환을 하여 안정적으로 데이터베이스 운영이 가능하다.
라이브 게임을 위해 사용하기 좋은 데이터베이스다. 사용 비율이 높진 않지만 간혹 사용되는 경우가 있다.
게임에 특화되어 있는 간단한 데이터베이스 플랫폼이다. 특히 한국 인디게임 등에 사용하는 경우가 많다.
구글 계정 그대로 시작할 수 있으며 프로젝트를 만들어보자.
파이어베이스에 AI와 관련된 기능이 추가되었고, 해당 기능은 나중에 세팅하는 것은 불편하기 때문에 우선 사용하지 않더라도 넣어두고 진행하도록 한다.
이와 같이 생성하는데 조금 시간이 걸리고 나면 프로젝트가 생성된다.
여기서 유의해야 할 것은 요금제에 대한 부분이다.
기본적으로 Spark 요금제는 무료로 사용할 수 있는 요금제이지만, 간혹 추가 기능을 사용하기 위해서 Blaze 요금제로의 전환 및 카드 등록을 요구하는 경우가 있을 수 있다. 서비스를 하는 단계가 아니라면 이렇게 전환하지 않도록 유의하자.
파이어베이스로 팀 프로젝트를 진행 시에 이와 같이 구성원을 추가하여 진행할 수도 있다. 구성원은 전부 구글 계정으로만 초대할 수 있다는 특징이 있다.
우리는 파이어베이스를 유니티 프로젝트에 연동할 것이기 유니티를 선택하자. 여기서 유니티로 연동한다 할 때에도 안드로이드 혹은 애플 앱으로도 같이 등록할 수 있다. (둘 중 하나는 필수 등록)
이와 같이 안드로이드 앱으로 등록하는 것으로 했다. 여기서 패키지 이름을 등록할 때 유의할 점이 있다.
다음 단계로 넘어가면 Json 파일을 다운받게 된다.
위 사진과 같이 해당 파일을 유니티에 넣는 방식으로 등록이 가능하다. 다만 유의해야 할 점이 있다.
해당 파일은 파이어베이스의 이른 바 키 값을 가지고 있는 파일이다 보니, 해당 파일이 노출되면 해킹 및 파일 변조 등의 위험성이 커지게 된다. 따라서 해당 정보는 노출되지 않도록 하며, 깃허브에 등록하는 프로젝트의 경우 Firebase 파일을 ignore로 등록해야 한다.
다음으로 SDK파일이 나오는데, 파이어베이스의 경우에는 유니티 에셋스토어에 등록되어 있지 않기 때문에 해당 파일을 다운받아 사용해야 한다.
파일을 압축 해제 해 보면 이와 같이 다양한 패키지파일이 들어있는 것을 확인할 수 있으며, 이들 모두를 사용하는 것이 아니라 필요한 기능만 사용하면 된다.
이와 같은 작업을 완료하면 설정이 끝난다. 여기서 샘플 프로젝트 같은 경우도 확인할 수 있다.
기능을 사용한 샘플을 확인할 수 있고, 코드 같은 것을 참고할 때 유용할 것으로 보인다. 시간이 나면 공부해 보는 것도 나쁘지 않아 보인다.
설정이 완료되면 메인의 화면이 이렇게 바뀐다.
Authentication 을 시작해보자.
시작하면 기본적으로 아래와 같은 화면에서 시작하며, 로그인의 방식을 선택할 수 있다.
로그인의 방식은 다음과 같이 자체적으로 제공하는 기능과, 제공 업체를 통한 방식이 있다.
여기서 왼쪽의 기본 제공 업체의 경우 이와 같은 특성이 있다.
여기서는 기본제공 이메일/비밀번호를 선택하고 저장해보자.
이와 같이 뜨면 제대로 설정된 것이다.
탬플릿은 이메일 인증을 시도했을 때 실제로 날아오는 이메일 내용을 담은 탬플릿을 설정할 수 있다. 언어 선택도 가능하고, 원하는 내용으로 설정이 가능하다.
상황에 따라 신규 로그인을 막을 수도 있고 여러 정책에 관한 설정이 가능하다.
여기서 주목해야 할 것은 이메일 열거 보호라는 시스템이다.
이는 간단히 말하자면, 체크 상태일 때는 로그인 실패 사유를 띄우지 않는다는 점이다.
로그인의 실패 사유의 경우 여러가지로 발생할 수 있지만, 그 구체적인 내용을 띄우는 순간 악용될 가능성이 있다.
ex) 이메일의 비밀번호가 틀렸다고 출렸했을 경우, 해당 이메일이 있다는 것은 확인 가능하니 비밀번호를 맞출 때까지 입력할 수도 있음.
이와 같은 상황을 방지하기 위해 이메일 열거 보호를 설정하는 것이 좋다.
기본적으로 사용하는 방식은 파이어베이스 메뉴얼을 참고하도록 한다.
해야 할 일은 다음과 같다.
해당 파일을 전부 넣으면 아래와 같이 될 것이다.
다만 유의해야 할 것은, 여기에서 파일 위치가 불편하다고 해서 수정하지는 않도록 하자. 경로가 꼬이면 제대로 연동이 안되거나 하는 등의 문제가 생길 수 있다.
혹시나 SDK 파일이나 Json파일을 미처 다운받지 못했을 경우 다시 다운받을 수 있다.
여기서 Json파일의 경우 데이터베이스 등을 진행할 때 Json파일이 변화할 수 있으며, 이에 따라 변경해줘야 할 때가 있으니 참고하도록 하자.
해당 링크를 통해 과정을 그대로 따라하면 된다.
여기서 이와 같이 제공되는 스크립트를 그대로 사용하면 되는데, FirebaseManager라는 스크립트를 만들어서 조금만 수정하고 제대로 돌아가는지 확인해보자.
using UnityEngine;
using Firebase.Extensions;
public class FirebaseManager : MonoBehaviour
{
private void Start()
{
Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task => {
Firebase.DependencyStatus dependencyStatus = task.Result;
if (dependencyStatus == Firebase.DependencyStatus.Available)
{
Debug.Log("파이어 베이스 설정이 모두 충족되어 사용할 수 있는 상황");
}
else
{
Debug.LogError($"파이어 베이스 설정이 충족되지 않아 실패했습니다. 이유: {dependencyStatus}");
}
});
}
}
콘솔에 뜬 내용으로 제대로 설정된 것을 확인할 수 있다.
Assets에 들어가보면 이와 같이 External Dependency Manager가 뜬 것을 확인할 수 있다.
문제가 발생한 상황에 따라 dependency status를 확인하여 해당 문제를 해결해보도록 하자.
파이어베이스는 기본적으로 모바일을 기본으로 지원했던 기능이지만, 데스크톱에서도 지원을 할 수 있도록 기능이 확장되었다.
그러므로 데스크톱용 어플리케이션을 만들기 위해서는 Json 파일도 데스크톱용으로 만들어야 하는데, 일반적인 경우 파일이 자동생성된다.
안을 확인해보면 내용 자체는 똑같지만, google-service-desktop.json이라는 이름만 다른 파일이 들어 있다.
혹시나 자동생성되지 않을 경우, json 파일을 다운 받고 파일 이름만 -desktop을 붙여서 생성해서 넣어두면 된다.
그러면 회원가입 기능을 구현해보자. 우선 아래와 같이 UI를 만들었다.
그리고 해당 패널을 제어하는 스크립트를 만들자.
using UnityEngine;
using Firebase.Extensions;
using Firebase.Auth;
using Firebase;
public class FirebaseManager : MonoBehaviour
{
private static FirebaseManager instance;
public static FirebaseManager Instance { get { return instance; } }
private static FirebaseApp app;
public static FirebaseApp App { get { return app; } }
private static FirebaseAuth auth;
public static FirebaseAuth Auth { get { return auth; } }
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
private void Start()
{
Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task => {
Firebase.DependencyStatus dependencyStatus = task.Result;
if (dependencyStatus == Firebase.DependencyStatus.Available)
{
Debug.Log("파이어 베이스 설정이 모두 충족되어 사용할 수 있는 상황");
app = FirebaseApp.DefaultInstance;
auth = FirebaseAuth.DefaultInstance;
}
else
{
Debug.LogError($"파이어 베이스 설정이 충족되지 않아 실패했습니다. 이유: {dependencyStatus}");
app = null;
}
});
}
}
using Firebase.Auth;
using Firebase.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class SignUpPanel : MonoBehaviour
{
[SerializeField] GameObject loginPanel;
[SerializeField] TMP_InputField idInput;
[SerializeField] TMP_InputField passInput;
[SerializeField] TMP_InputField passConfirmInput;
[SerializeField] Button signUpButton;
[SerializeField] Button cancelButton;
private void Awake()
{
signUpButton.onClick.AddListener(SignUp);
cancelButton.onClick.AddListener(Cancel);
}
private void SignUp()
{
if(passInput.text != passConfirmInput.text)
{
Debug.LogError("패스워드가 일치하지 않습니다");
return;
}
FirebaseManager.Auth.CreateUserWithEmailAndPasswordAsync(idInput.text, passInput.text)
.ContinueWithOnMainThread(task =>
{
if(task.IsCanceled)
{
Debug.LogError("이메일 가입 취소됨");
return;
}
if(task.IsFaulted)
{
Debug.LogError($"이메일 가입 실패함. 이유 {task.Exception}");
return;
}
AuthResult result = task.Result;
Debug.Log("이메일 가입 성공!");
});
}
private void Cancel()
{
loginPanel.SetActive(true);
gameObject.SetActive(false);
}
}
여기서 특히 주목해야 하는 점은 ContinueWithOnMainThread라는 부분이다.
이 부분이 Firebase 메뉴얼에서는 ContinueWithOn으로 사용하고 있는데, 이렇게 사용할 경우 데스크톱 등의 플랫폼에서 문제가 발생할 수 있으니, ContinueWithOnMainThread로 사용하는 것을 권장한다.
인스펙터 세팅까지 완료하고 실행시켜보자.
비밀번호가 일치하지 않는 경우도 테스트해보고, 이메일 형식이 아닌 경우 및 다양한 시도를 해 보고, 이메일 계정 생성까지 해 보았다. 여기에서 특이점으로 알게 된 경우로는, 만약 Authentication 관련으로 설정을 변경하였을 경우 이게 실시간으로 반영되지는 않는 것으로 확인했다.
따라서 설정을 변경한 경우 Json 파일을 다시 받아서 진행하도록 하자.
위와 같이 생성한 계정을 확인해 보자.
Firebase에서 사용자를 확인해보면 계정이 등록된 것을 확인할 수 있다. 생성 날짜 및 최종 로그인 날짜를 확인할 수 있으며, 각 계정의 비밀번호는 아무리 관리자라고 해도 알 수 없는 것을 확인할 수 있다.
여기서 계정 관리에 대해 유의할 것은 계정 삭제 및 계정 사용 중지(밴)도 가능하며, 비밀번호도 재설정할 수 있다. 실제 사용자에게 불편을 주지 않도록 이런 관리를 하는 경우에서는 유의하도록 하자.
그러면 이 다음으로 생성한 계정으로 로그인을 하는 기능을 구현해보자.
방식은 회원가입 기능과 비슷하다.
using Firebase.Auth;
using Firebase.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class LoginPanel : MonoBehaviour
{
[SerializeField] GameObject signUpPanel;
[SerializeField] TMP_InputField idInput;
[SerializeField] TMP_InputField passInput;
[SerializeField] Button signUpButton;
[SerializeField] Button loginButton;
private void Awake()
{
signUpButton.onClick.AddListener(SignUp);
loginButton.onClick.AddListener(Login);
}
private void SignUp()
{
signUpPanel.SetActive(true);
gameObject.SetActive(false);
}
private void Login()
{
FirebaseManager.Auth.SignInWithEmailAndPasswordAsync(idInput.text, passInput.text)
.ContinueWithOnMainThread(task =>
{
if(task.IsCanceled)
{
Debug.LogError("로그인이 취소됨");
return;
}
if(task.IsFaulted)
{
Debug.LogError($"로그인이 실패함. 이유 : {task.Exception}");
return;
}
Debug.Log("로그인 성공");
AuthResult result = task.Result;
FirebaseUser user = result.User;
Debug.Log("---- 유저 정보 ----");
Debug.Log($"유저 이름 : {user.DisplayName}");
Debug.Log($"유저 아이디 : {user.UserId}");
Debug.Log($"이메일 : {user.Email}");
Debug.Log($"이메일 인증여부 : {user.IsEmailVerified}");
});
}
}
이와 같이 세팅하고 로그인 기능을 시험해보자.
여기서 일부러 여러가지 이유로 로그인 실패를 유도해봤는데, 출력되는 오류 멘트가 다른 것을 확인할 수 있다.
두 가지 오류가 발생하는 차이점은 다음과 같다.
The email adress is badly formatted
이메일 형식이 아닐 경우에 발생하는 오류이다.
An internal error has occured
아래와 같은 경우에서 발생한다.
앞의 단계에서 언급했던 이메일 열거 보호로 인해 모든 오류 사항을 모호하게 표현한 것이다.
기존에 등록된 아이디로 로그인에 성공한 것을 확인할 수 있다.
여기서 유저의 아이디 부분이 입력하지 않은 숫자와 문자열의 합으러 되어 있는 것을 확인할 수 있다.
이는 로그인 유저에게 부여되는 고유한 ID로 변경이 불가능하다. 로그인 유저를 식별하기 위한 코드로서 닉네임과는 다른 개념이다.
특정 게임에서는 닉네임이 중복되어도 문제가 없는 이유가 이 때문이다.
이제 로그인까지 구현했으니 로그아웃까지 구현해보도록 하자.
로그인을 한 다음 로비로 이동하고, 로그아웃을 하면 이전 화면으로 돌아가도록 해 보자.
로비 패널은 위와 같이 구성했고, 정보를 표시하면서 동시에 로그아웃까지의 기능을 구현해보고자 한다.
using Firebase.Auth;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class LobbyPanel : MonoBehaviour
{
[SerializeField] GameObject loginPanel;
[SerializeField] TMP_Text emailText;
[SerializeField] TMP_Text nameText;
[SerializeField] TMP_Text userIdText;
[SerializeField] Button logoutButton;
[SerializeField] Button editProfileButton;
private void Awake()
{
logoutButton.onClick.AddListener(Logout);
editProfileButton.onClick.AddListener(EditProfile);
}
private void OnEnable()
{
FirebaseUser user = FirebaseManager.Auth.CurrentUser;
emailText.text = user.Email;
nameText.text = user.DisplayName;
userIdText.text = user.UserId;
}
private void Logout()
{
FirebaseManager.Auth.SignOut();
loginPanel.SetActive(true);
gameObject.SetActive(false);
}
private void EditProfile()
{
// 추후 구현
}
}
이와 같이 세팅하고서 실행시켜보자.