-
파이어베이스 인증 데이터베이스 서버코드 백엔드 사용하기 / Unity Firebase Auth Firestore Functions Tutorial유니티 2024. 12. 30. 22:41
파이어베이스란?
구글에서 제공하는 클라우드 기반 백엔드 플랫폼입니다.
가격이 저렴하며 백엔드를 편하게 쓰는 장점이 커서 수많은 개발자에게 사랑받고 있습니다.
웹 개발에서 백엔드는 주로 파이어베이스를 기본으로 쓸 정도로 대중화 되어있습니다.
https://firebase.google.com/docs
공식문서를 참고하시면 더욱 좋습니다.
특징으로는
- 유니티에서 백엔드를 쓸 때 플레이팹, 뒤끝, 파이어베이스 등이 있는데 그중 파이어베이스 가격이 가장 저렴합니다.
- 인증과 데이터를 저장하는 파이어스토어는 물론 실시간 데이터로 채팅도 가능합니다.
- 클라우드 펑션을 쓰면 서버에서 함수가 실행되어 보안도 됩니다.
사용할 제품으로는 Authentication 인증, Firestore 데이터베이스, Functions 서버코드 입니다.
공통 설정
https://console.firebase.google.com/
파이어베이스 콘솔에 접속해 프로젝트 만들기 - 프로젝트 이름 입력 - 애널리틱스 사용 설정을 저는 체크해제 하겠습니다.
프로젝트 만들기 클릭하면 생성됩니다.
https://firebase.google.com/pricing
요금제가 두개가 있습니다.
Spark는 무료구간만 사용하지만, Blaze는 무료구간을 포함하며 그 이후 과금되는 시스템입니다.
Functions를 사용하려면 꼭 Blaze를 사용해야 하므로 미리 업그레이드 해 두겠습니다.
상단에 Spark 요금제를 Blaze로 바꿉니다.
Cloud Billing 결제 계정이 없다면 마스터 카드같은 카드로 계정을 만들고 연결하셔야 합니다.
요금제 변경이 완료되었습니다.
톱니바퀴 - 프로젝트 설정 - 내 앱 유니티 아이콘 클릭
앱 등록에 Apple 또는 Android 중 해당 타겟을 체크합니다. (PC 타겟이면 Android로 하겠습니다.)
유니티 프로젝트에서 Edit - Project Settings... - Player - Other Settings - Bundle Identifier
패키지 이름을 전부 소문자로 적고 사이트에도 쓰고 앱 등록 눌러줍니다.google-services.json 파일을 다운받아 Assets 경로중 원하는 폴더에 넣습니다.
Firebase Unity SDK 파일을 다운받고 압축을 풉니다.
각 유니티 패키지 파일에서 설명드릴것은 FirebaseAuth, FirebaseFirestore, FirebaseFunctions 입니다.
유니티에 임포트 합니다.
콘솔로 이동합니다.
https://goranitv.tistory.com/33
유니티에서 테스트를 편하게 하기 위해 퀵 커맨드(QuickCommand) 에셋을 받아주세요.
인증(Authentication)
사용자 고유 아이디(user id, uid)를 가진 인증을 담당합니다.
구글, 애플, 페이스북, 트위터, 깃허브 등으로 연동을 할 수 있지만 이메일 비밀번호 기반으로 시작할 것입니다.
콘솔 - 빌드 탭 - Authentication - 시작하기 - 이메일/비밀번호 - 사용설정 체크
사용자 추가를 눌러서 이메일(@와 .으로 구분해야함)과 비밀번호(6자 이상) 으로 만들면 사용자 uid가 나타납니다.
사용자 고유의 아이디로 사용자를 구분할 때 사용됩니다.
해당 아이디에 마우스를 올리면 점3개가 나오는데 계정삭제를 눌러 삭제하겠습니다.
FireAuthManager.cs
using System; using UnityEngine; using QuickCmd; using Firebase.Auth; using Firebase; public class FireAuthManager : MonoBehaviour { public string UserId { get => user.UserId; } FirebaseAuth auth; FirebaseUser user; [Command] public async Awaitable<bool> InitializeFireAuth() { FirebaseApp.LogLevel = LogLevel.Error; DependencyStatus dependencyStatus = await FirebaseApp.CheckAndFixDependenciesAsync(); if (dependencyStatus == DependencyStatus.Available) { auth = FirebaseAuth.DefaultInstance; Debug.Log("FireAuth initialized."); return true; } else { Debug.LogError($"InitializeFirebase failed: {dependencyStatus}"); return false; } } [Command] public async Awaitable<bool> SignUpWithEmail(string email, string password) { try { AuthResult authResult = await auth.CreateUserWithEmailAndPasswordAsync(email, password); if (authResult.User != null) { user = authResult.User; Debug.Log($"SignUpWithEmail success. UserId: {UserId}"); return true; } else { Debug.LogError("SignUpWithEmail failed."); return false; } } catch (Exception e) { Debug.LogError($"SignUpWithEmail failed: {e.Message}"); return false; } } [Command] public async Awaitable<bool> LoginWithEmail(string email, string password) { try { AuthResult authResult = await auth.SignInWithEmailAndPasswordAsync(email, password); if (authResult.User != null) { user = authResult.User; Debug.Log($"LoginWithEmail success. UserId: {UserId}"); return true; } else { Debug.LogError("LoginWithEmail failed."); return false; } } catch (Exception e) { Debug.LogError($"LoginWithEmail failed: {e.Message}"); return false; } } [Command] public void Logout() { auth.SignOut(); Debug.Log("Logout success."); } [Command] public async Awaitable<bool> UpdateDisplayName(string displayName) { try { if (auth.CurrentUser != null) { UserProfile profile = new UserProfile { DisplayName = displayName }; await auth.CurrentUser.UpdateUserProfileAsync(profile); Debug.Log($"UpdateDisplayName success: {displayName}"); return true; } else { Debug.LogError("No user is currently signed in"); return false; } } catch (Exception e) { Debug.LogError($"UpdateDisplayName failed: {e.Message}"); return false; } } [Command] public string GetDisplayName() { if (auth.CurrentUser != null) { string displayName = auth.CurrentUser.DisplayName; Debug.Log($"DisplayName: {displayName}"); return displayName; } else { Debug.LogError("No user is currently signed in"); return null; } } async void Start() { await InitializeFireAuth(); } void OnApplicationQuit() { Logout(); } }
- string UserId
유저 아이디를 가져옵니다. - Awaitable<bool> InitializeFireAuth()
로그 레벨을 Error로 하여 에러일 때만 나오도록 하겠습니다.
Start에서 초기화하도록 하였습니다. - Awaitable<bool> SignUpWithEmail(string email, string password)
이메일과 비밀번호로 회원가입을 합니다.
이메일은 @와 .으로 구분해야하며 비밀번호는 6자 이상입니다.
회원가입을 하면 로그인을 한 상태입니다. - Awaitable<bool> LoginWithEmail(string email, string password)
이메일과 비밀번호로 로그인 합니다. - void Logout()
로그아웃 합니다.
OnApplicationQuit에서 로그아웃하도록 하였습니다. - Awaitable<bool> UpdateDisplayName(string displayName)
표시 이름을 바꿉니다. - string GetDisplayName()
표시 이름을 가져옵니다.
데이터베이스(Firestore)
파이어스토어는 데이터베이스(db)를 담당합니다.
공유 데이터는 물론 uid로 개인만 접근하는 데이터도 읽고 쓸 수 있습니다.
콘솔 - 빌드 탭 - Firestore Database - 데이터베이스 만들기 - 위치는 멀티 리전 중 nam5 (United States)로 하겠습니다.
프로덕션 모드에서 시작 - 만들기 클릭
데이터 탭에서 하나의 컬렉션을 만들어 구성합니다.
users 컬렉션 - {uid} 문서 - {ItemA : string Sword}, {Level : number 1} 필드로 구성될 것입니다.
즉 모든 인증된 사용자는 문서에서 uid와 일치하는 필드를 읽고 쓰도록 할 것입니다.
임시로 만든 문서는 삭제하겠습니다.
board 컬렉션 - view 문서 - {Content : string 내용}, {Title : string 제목} 필드로 구성합니다.
board 컬렉션 - updated 문서 - {Date : string MyDate} 필드로 구성합니다.
규칙 탭
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{uid} { allow read, write: if request.auth != null && request.auth.uid == uid; } match /board/{document=**} { allow read: if request.auth != null; allow write: if false; } } }
users 컬렉션에 있는 uid 문서가 일치할 때만 읽기 쓰기 권한을 허용해 주도록 바꿔줍니다.
board 컬렉션에 있는 모든 하위 쓰기는 누구도 쓸 수 없고, 로그인 되어있으면 누구나 읽을 수 있습니다.
FirestoreManager.cs
using System; using System.Collections.Generic; using UnityEngine; using QuickCmd; using Firebase.Firestore; public class FirestoreManager : MonoBehaviour { [SerializeField] FireAuthManager fireAuthManager; FirebaseFirestore db; [Command] public void InitializeFirestore() { db = FirebaseFirestore.DefaultInstance; Debug.Log("Firestore Initialized"); } public async Awaitable<bool> PostUserData(Dictionary<string, object> userDic) { try { DocumentReference docRef = db.Collection("users").Document(fireAuthManager.UserId); await docRef.SetAsync(userDic); Debug.Log("PostData Success."); return true; } catch (Exception e) { Debug.LogError($"PostUserData Failed. {e.Message}"); return false; } } [Command] public async Awaitable<Dictionary<string, object>> GetUserData() { try { DocumentReference docRef = db.Collection("users").Document(fireAuthManager.UserId); DocumentSnapshot snapshot = await docRef.GetSnapshotAsync(); Dictionary<string, object> documentDic = snapshot.ToDictionary(); Debug.Log("GetUserData Success."); if (documentDic != null) { foreach (var (key, value) in documentDic) { Debug.Log($"{key} : {value}"); } } return documentDic; } catch (Exception e) { Debug.LogError($"GetUserData Failed. {e.Message}"); return null; } } [Command] public async Awaitable<Dictionary<string, Dictionary<string, object>>> GetAllBoardDocuments() { try { CollectionReference boardCollection = db.Collection("board"); QuerySnapshot querySnapshot = await boardCollection.GetSnapshotAsync(); Dictionary<string, Dictionary<string, object>> result = new(); foreach (DocumentSnapshot document in querySnapshot.Documents) { if (document.Exists) { result[document.Id] = document.ToDictionary(); } else { Debug.LogWarning($"Document {document.Id} does not exist."); } } if (result != null) { foreach (var (docName, fields) in result) { Debug.Log($"Document: {docName}"); foreach (var (key, value) in fields) { Debug.Log($"{key}: {value}"); } } } return result; } catch (Exception e) { Debug.LogError($"GetAllBoardDocuments failed. {e.Message}"); return null; } } [Command] public async void FastPostUserData() { Dictionary<string, object> userDic = new() { { "Level", UnityEngine.Random.Range(1, 10) }, { "ItemA", $"Item{UnityEngine.Random.Range(0, 100)}" }, }; await PostUserData(userDic); } void Start() { InitializeFirestore(); } }
인증이 완료된 UserId를 가져와야 하므로 인스펙터에서는 FireAuthManager를 참조합니다.
- void InitializeFirestore()
파이어스토어를 초기화합니다.
Start에서 초기화하도록 하였습니다. - Awaitable<bool> PostUserData(Dictionary<string, object> userDic)
users 컬렉션의 userId 문서에 접근해 딕셔너리 키 값 쌍을 업로드합니다. - async Awaitable<Dictionary<string, object>> GetUserData()
users 컬렉션의 userId 문서에 접근해 딕셔너리 키 값 쌍을 가져옵니다. - Awaitable<Dictionary<string, Dictionary<string, object>>> GetAllBoardDocuments()
board 컬렉션의 모든 문서를 쿼리를 돌려 여러개를 한번에 가져옵니다. - void FastPostUserData()
임시로 Level과 ItemA를 키로하는 값들로 PostUserData에 업로드합니다
https://firebase.google.com/docs/firestore/query-data/queries
에서 쿼리를 어떻게 사용하는지 자세히 알 수 있습니다.
서버코드(Functions)
서버에서 함수를 실행하여 클라이언트에서 중요한 데이터를 변경하는 함수를 서버에게 맡깁니다.
클라이언트에서 데이터에 직접 접근하는 게 해킹에 취약할 수 있습니다.
그래서 데이터의 값을 클라이언트에서 직접 수정하는 게 아닌 서버에서 함수를 실행하게 되면
로그로 기록도 남고 이상한 행동이 감지될 때 실행이 안되게 하는 등 보안에 좋습니다.
로컬에서 자바스크립트로 만들어 서버에 올려야 하는 구조입니다.
Visual Studio Code(https://code.visualstudio.com/)와 Node JS(https://nodejs.org/en) LTS를 설치합니다.
콘솔 - 빌드 탭 - Functions - 시작하기를 누릅니다.
빈 폴더를 원하는 경로에 하나 만들고 비주얼 스튜디오 코드에서 파일 - 폴더 열기로 해당 폴더를 엽니다.
터미널에 다음을 입력합니다.
npm init -y npm i firebase-tools -D npx firebase-tools login npx firebase init functions
로그인은 여러분 구글 로그인을 해줍니다.
만약 새 아이디로 로그인 하고 싶다면 npx firebase-tools login:use new@gmail.com을 합니다.
질문이 뜨는데
Are you ready to proceed?
> Y
First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add,
but for now we'll just set up a default project.> Use an existing project
Select a default Firebase project for this directory
> 콘솔 - 설정 버튼 - 프로젝트 ID와 동일한 것을 선택합니다.
What language would you like to use to write Cloud Functions?
> JavaScript
Do you want to use ESLint to catch probable bugs and enforce style?
> N
Do you want to install dependencies with npm now?
> Y
연동이 완료되었으면 해당 파일 구조로 나타납니다.
functions 폴더에 index.js 파일을 엽니다.
functions/index.js
const {logger} = require("firebase-functions"); const {onRequest} = require("firebase-functions/v2/https"); const {onDocumentCreated} = require("firebase-functions/v2/firestore"); const {initializeApp} = require("firebase-admin/app"); const {getFirestore} = require("firebase-admin/firestore"); initializeApp(); exports.sendJson = onRequest((req, res) => { res.json({ message: "Hello, Firebase!", success: true }); }); exports.addLevel = onRequest(async (req, res) => { const userId = req.query.userId; if (!userId) { res.status(400).json({error: "Missing 'userId' parameter"}); return; } try { const userRef = getFirestore().collection("users").doc(userId); const userDoc = await userRef.get(); const currentLevel = userDoc.data().Level || 0; await userRef.update({ Level: currentLevel + 1, }); msg = `User ID: ${userId}, Level increased to ${currentLevel + 1}.`; console.log(msg) res.json({result: msg}); } catch (error) { res.status(500).json({error: error.message}); } });
initializeApp와 윗 부분은 필요한 파일들을 요구하고 초기화 하는 부분입니다.
sendJson은 GET 통신을 하면 "Hello, Firebase!" 메시지가 담긴 단순한 JSON을 반환합니다.
addLevel은 userId를 담아 요청하면 파이어스토어에 접근해 맞는 user 문서의 Level을 1을 증가시킵니다.
공식문서의 Functions을 보시면 2세대 함수의 더 다양한 사용법을 볼 수 있습니다.
firebase deploy --only functions
저장하고 터미널에서 위 코드를 입력해 파이어베이스 서버에 올립니다.
사이트를 새로고침하면 두가지 함수가 제대로 올라간 걸 볼 수 있습니다.
FireFuncManager.cs
using UnityEngine; using UnityEngine.Networking; using QuickCmd; public class FireFuncManager : MonoBehaviour { [SerializeField] FireAuthManager fireAuthManager; public async Awaitable<string> Fetch(string url) { UnityWebRequest request = UnityWebRequest.Get(url); await request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"Failed to send function: {request.error}"); return null; } return request.downloadHandler.text; } [Command] public async void SendJson() { string url = "https://YOUR_URL.a.run.app"; string response = await Fetch(url); if (response != null) { Debug.Log($"SendJson response: {response}"); } else { Debug.LogError("SendJson failed."); } } [Command] public async void AddLevel() { string url = "https://YOUR_URL.a.run.app"; string urlWithParams = $"{url}?userId={UnityWebRequest.EscapeURL(fireAuthManager.UserId)}"; string response = await Fetch(urlWithParams); if (response != null) { Debug.Log($"AddLevel response: {response}"); } else { Debug.LogError("AddLevel failed."); } } }
인증이 완료된 UserId를 가져와야 하므로 인스펙터에서는 FireAuthManager를 참조합니다.
각 url에는 사이트에 나타난 트리거 요청 url을 복붙합니다.
- Awaitable<string> Fetch(string url)
Get통신을 합니다. - void SendJson()
응답이 JSON으로 들어옵니다. {"message":"Hello, Firebase!","success":true} - void AddLevel()
데이터가 있어야 Level을 올릴 수 있으므로 최초 FirestoreManager에서 만든
FastPostData를 한번 호출해줘야 정상 작동합니다.
또는 커스텀으로 서버에서 객체를 생성하는 방법도 있겠습니다.
파라미터는 ?userId={UnityWebRequest.EscapeURL(fireAuthManager.UserId)}같이 이어 써줘야합니다.
응답이 {"result":"User ID: YOUR_USERID, Level increased to 4."}으로 들어옵니다.
'유니티' 카테고리의 다른 글