목차

     

     

    Turorial

    Firebase 에서 사용할 수 있는 DB는 2종류이다. 

    둘다 NoSQL DB이며 실시간으로 데이터가 동기화된다.

    새 프로젝트를 시작하는 단계의 개발자들은 Firebase DB들 중 Cloud Firestore 를 선택해서 진행하는 것이 좋다. 향후 더욱 강력한 기능을 지원할 예정이기 때문이다. 실제로 Firebase 에서 Cloud Firestore에 더 많은 지원을 해주고 있다.

    둘의 자세한 차이점은 데이터베이스 선택: Cloud Firestore 또는 실시간 데이터베이스 에 적혀있다.


    이 글에서는 Cloud Firestore 만 설명하며

    Unity3d <-> Node.js Server -> Cloud Firestore

    이런 방식으로 데이터를 저장할 예정이다.

     

    서버는 Firebase Hosting 을 이용한 Node.js를 사용한다.

    Firebase Setting

    1. 아래 링크에서 새로운 Firebase 프로젝트를 만들거나 기존의 Firebase 프로젝트를 사용한다.

    https://seonbicode.tistory.com/28

     

    2. 새로운 앱을 등록하거나 기존의 앱을 사용한다.

    클라이언트(Unity3d)에서는 데이터만 서버에 보내주고 서버에서 Database 기능을 사용할 예정으로 Web App 만 등록해준다.

    https://seonbicode.tistory.com/43#Firebase_Project_New_App_Add

     

    3. 아래 링크에서 Database 항목을 참고하여 Firebase 세팅을 한다.

    https://seonbicode.tistory.com/43#Database

     

    4. Firebase Hosting 활성화와 기본 개발 세팅을 한다.

    먼저 node 와 npm 와 express 3가지를 설치한다.

    https://seonbicode.tistory.com/38#Node.js_%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD_%EA%B5%AC%EC%B6%95

    그 후, Firebase Hosting 활성화를 해준다.

    Firebase login, init 같은 경우 차후 서버를 제작할 때 같이 세팅해준다. 일단 활성화를 목적으로 넘어간다.

    https://seonbicode.tistory.com/43#Hosting

     

    5. Keystore 세팅을 한다.

    https://seonbicode.tistory.com/43#SHA_%EC%9D%B8%EC%A6%9D%EC%84%9C_%EC%A7%80%EB%AC%B8

     

     

    Firebase Cloud Store 구조

    진행하기 전에 서버에서 사용할 DB(Cloud Firestore) 의 구조부터 파악하자.

    Cloud Firestore
    NoSQL 문서 중심의 데이터베이스이며 SQL 데이터베이스와 달리 테이블이나 행이 없으며, 컬렉션으로 정리되는 문서(Document)에 데이터를 저장한다. 각 문서에는 키-값들이 들어 있다.

    작은 문서가 많이 모인 컬렉션을 저장하는 데 최적화되어 있다.

    콜렉션과 문서는 Cloud Firestore에서 암시적으로 생성한다.

    사용자는 컬렉션 내의 문서에 데이터를 할당하기만 되고 컬렉션 또는 문서가 Firestore에 없다면 자동으로 생성한다.

     

    Document

    문서(Document)는 Cloud Firestore의 저장소 단위이다.

    문서는 값이 매핑되는 필드(Field)를 포함하며 각 문서는 이름으로 식별된다.

    사용자 alovelace 를 나타내는 문서는 다음과 같다.

    하위의 이미지처럼 문서의 복잡한 중첩 개체를 이라고 한다.

    JSON과 구조가 비슷해보이는데 사실 기본적으로 JSON을 사용한다. 문서가 추가적인 데이터 형식을 지원하고 크기가 1MB로 제한되는 등의 몇가지 차이점이 있지만 JSON 레코드로 취급해도 무방하다.

     

    지원되는 데이터 유형은 https://firebase.google.com/docs/firestore/manage-data/data-types 이 링크를 참조하자.

     

    Collection
    모든 문서들이 저장되는 위치이며 단순히 문서의 컨테이너이다.

     사용자 이라는 컬렉션 아래에 alovelace, aturing 이라는 문서가 존재한다.

     

     

     

     

     

     

     

     

     

     

    컬렉션은 값이 있는 원시 필드를 포함하거나 다른 컬렉션을 포함할 수 는 없다. 오직 문서만 포함하며 컬랙션 내의 문서 이름들은 고유하다. 사용자 ID (UID) 와 같은 고유한 키 나 Firestore에서 생성한 무작위 ID를 사용할 수 있다.

     

    만약 alovelace 문서에 참조하고 싶다면

    let alovelaceDocRef = firebaseAdmin.firestore().collection('users').doc('alovelace');

     

    alovelace 문서가 포함된 콜렉션을 참조하고 싶다면

    let userColRef = firebaseAdmin.firestore().collection('users');

     

    alovelace 문서를 경로로 지정하여 참조하고 싶다면

    let alovelaceDocRef = firebaseAdmin.firestore().doc('users/alovelace');

     

    계층적 데이터

    계층적 데이터를 쉽게 이해하기 위해, 채팅 앱을 예시로 든다. 

    여러 채팅방을 저장하는 rooms 라는 컬렉션이 있다.

    roomA 라는 채팅방이 생겼지만 메시지를 채팅방에 직접 저장하는 것은 좋지 않다. 문서는 가벼워야 하는데, 채팅방에 매우 많은 메시지가 포함될 수 있기 때문이다. 이러한 문제는 채팅방 문서 안에 하위 컬렉션을 추가로 만드는 것으로 해결할 수 있다.

    roomA 채팅방에 messages 라는 하위 컬렉션을 만들어 각 메세지를 문서로 저장할 수 있다.

     

    컬렉션 그룹 쿼리를 사용하면 컬렉션 ID가 동일한 하위 컬렉션 전체를 쿼리할 수 있다.
    쿼리의 공식 문서 : https://firebase.google.com/docs/firestore/query-data/queries?hl=ko

     

    message1 에 대한 참조는 아래와 같다.

    let messageRef = firebaseAdmin.firestore().collection('rooms').
    doc('roomA').collection('messages').doc(message1');

    위와 같이 컬렉션과 문서가 교대로 나오는 패턴을 따라야한다.

    collections('rooms').collections('messages') 이나 doc('roomA').doc('message1') 와 같은 패턴은 참조할 수 없다.

    roomA의 모든 메시지를 가져오려면 하위 컬렉션인 messages에 대한 컬렉션 참조를 만들고 다른 컬렉션 참조와 같은 방식으로 상호작용하면 된다.

     

    하위 컬렉션의 문서도 하위 컬렉션을 포함할 수 있으며 데이터를 더 중첩할 수 있다. 최대 깊이는 100개 수준이다.

     

    문서를 삭제해도 하위 컬렉션은 삭제되지 않으며 하위 컬렉션이 있는 문서를 삭제해도 하위 컬렉션은 삭제되지 않는다. 
    예를 들어 coll/doc 문서가 더 이상 존재하지 않더라도 coll/doc/subcoll/subdoc에는 문서가 있을 수 있다. 
    상위 문서를 삭제할 때 하위 컬렉션의 문서도 삭제하려면 컬렉션 삭제에 설명된 대로 직접 삭제해야 합니다.

     

     

    Server

    인제 직접 서버에 적용하여 사용하는 법을 보자. 하위 컬렉션은 사용하지 않으며

    set, get, update 에 대한 사용 예시이다.

     

    그리고 서버로써 구동해줄 새로운 Firebase Hosting Project 를 만들거나 기존의 Firebase Hosting Project 를 이용해야한다.

    만약 기존에 사용하던 Firebase Hosting Project 가 없다면 아래 링크를 참고하여 새롭게 만들어줘야한다.

    https://seonbicode.tistory.com/38#Firebase_Hosting_%EA%B8%B0%EB%B3%B8_%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8_%EA%B0%9C%EB%B0%9C_%ED%99%98%EA%B2%BD_%EA%B5%AC%EC%B6%95

     

    일단 이 글은 새로운 Firebase Hosting Project 를 기준으로 진행한다. 

     

    아래에 첨부된 db.js 파일을 main.js 로 이름을 변경하여 기본 Hosting Project/functions 에 있는 main.js에 붙여넣어도 되고 index.js 에 새로운 미들웨어로 선언해도 된다.

    새로운 미들웨어로 선언하고자 하면 Firebase Hosting Project 의 functions 폴더에 넣어주면 된다.

    db.js
    0.00MB
    // express 프레임워크
    const express = require('express');
    const router = express.Router();
    
    // firebase Admin 써드파티 미들웨어 
    const admin = require('firebase-admin'); 
    // firebase Admin 초기화 
    const firebaseAdmin = admin.initializeApp({ 
        credential: admin.credential.cert({ 
            // firebase admin sdk json 에 있는 키값들을 넣어준다. 
        }), 
        databaseURL: 
        storageBucket: 
    });
    
    // 클라이언트가 보낸 데이터를 받은 후 DB에 저장
    router.post('/postData', (req, res) => {
        const work = async () => {
            try{
                // 클라이언트에서 보낸 json 데이터
                var userData = req.body;
    
                console.log(userData.uid);
    
                // firestore 에서의 collection은 단순히 문서의 컨테이너이다.
                // db에 'user'라는 컬렉션이 없으면 자동으로 생성 후 사용하며
                // 있다면 그대로 사용한다.
                firebaseAdmin.firestore().collection('user')
                // doc() 이면 firestore가 자동으로 id로 문서가 생성한다.
                // doc('임의의 이름') 이면 사용자가 지정한 이름으로 문서가 생성된다.
                .doc(userData.uid)
                // JSON 데이터를 직접 넣는다.
                .set(userData)
                .then(() => {
                    // 데이터 입력 성공 시...
                    return res.send(true);
                }, (err) => {
                    // 데이터 입력 실패 시...
                    throw err;
                });
            } catch(err) {
                console.log(err);
                
                return res.send(false);
            }
        }
    
        work();
    });
    
    // 클라이언트가 보낸 UID 로 DB에서 검색 후 클라이언트에 JSON 되돌려준다.
    router.get('/getSearchData', (req, res) => {
        const work = async () => {
            try{
                var uid = req.query.uid;
    
                // 문서 한 개만 찾은 후 클라로 보낸다.
                firebaseAdmin.firestore().collection('user').doc(uid).get()
                .then((snapshot) => {
                    // 찾은 문서에서 데이터를 JSON 형식으로 얻는다.
                    var userData = snapshot.data();
    
                    return res.json(userData);
                }, (err) => {
                    throw err;
                });
    
                // 만약 
            } catch(err) {
                console.log(err);
    
                return res.send(false);
            }
        }
    
        work();
    });
    
    // 클라이언트가 보낸 userData 로 DB에서 검색 후 특정 키값을 갱신한다.
    router.post('/postRenewData', (req, res) => {
        const work = async () => {
            try{
                var user = req.body;
    
                firebaseAdmin.firestore().collection('user').doc(user.uid).update({
                    name: "유저 이름 변경",
                    // 특정 함수를 사용하여 수 타입의 데이터의 양을 지정된 수만큼 늘릴 수 있다.
                    // 나이가 3 으로 저장되어 있는데 아래 함수를 사용할 경우 나이가 한살 늘어나
                    // 4 로 수정된다.
                    age: admin.firestore.FieldValue.increment(1)
                }).then((snapshot) => {
                    // 성공 시...
                    return res.send(true);
                }, (err) => {
                    throw err;
                });
            } catch (err) {
                console.log(err);
    
                return res.send(false);
            }
        }
    });
    
    module.exports = router;

     

    기본 main.js에 붙여넣지 않고 따로 index.js 에 위에 다운로드 받은 db_iap를 새로운 미들웨어로 추가하는 방법이다.

    functions/index.js

    // 다른 미들웨어들 선언
    ...
    ...
    const functions = require('firebase-functions');
    // 다른 미들웨어들 선언 위치 아래에 선언하면 된다. 
    // js 파일을 불러온다. db_iap.js 를 불러온다. 
    var dbRouter = require('./db'); 
    
    // app 선언 
    var app = express(); 
    
    // app 선언 이후에 입력 
    // /db 라는 신호가 들어오면 dbRouter 로 넘겨준다. 
    app.use('/db', dbRouter);

     

    DB.js 에 비어있는 Firebase Admin SDK 키 값 부분 은 아래 링크에서 구한 후 initializeApp 을 채워준다.

    https://seonbicode.tistory.com/43#_Firebase_Admin_SDK

     

    Firebase Admin 키 값까지 넣었다면 터미널에서 firebase serve 명령어를 firebase hosting 프로젝트 폴더/functions 폴더에서 실행시켜 서버가 잘 실행되는지 테스트 한다.

     

    서버가 잘 열린다면 서버와 통신할 Unity3d 클라이언트를 제작한다.

     

    하지만 만약

    Error getting documents Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.

    와 비슷한 Google Cloud 에러가 발생할 시 아래의 방법으로 해결한다.

     

    1. 각자 자신의 OS 에 맞는 방법으로 https://cloud.google.com/sdk/docs/downloads-interactive 설치한다.
    2. 설치된 Google Cloud SDK CMD 열어준다. windows 의 경우 검색창에서 Google Cloud SDK CMD 를 검색해주면 된다.
    3. gcloud init --skip-diagnostics 실행시키고 현재 사용중이고 활성화된 Firebase Storage 가 있는 구글 아이디로 로그인 한다.
    4. gcloud auth application-default login 실행시키고 구글 아이디 로그인을 해준다.

     

    Client

    아래 링크를 참고하여 기본적인 Unity3d Client Setting을 한다.

    https://seonbicode.tistory.com/43#Unity3d_Client_Setting

     

    Unity3d 에서는 아래와 스크립트를 제작하여 Node.js 와 웹 통신을 하면 된다.

    Server.cs
    0.00MB
    using System.Collections;
    using UnityEngine;
    using UnityEngine.Networking;
    using System;
    
    public class Server : MonoBehaviour
    {
        // 서버에 보낼 유저 데이터
        public class user
        {
            public string uid;
            public string name;
            public int age;
        }
    
        ////////////////////////////////////////////////////////////////////////////
        // 현재 클라이언트에 접속된 유저 UID 로 서버 DB에 저장된 유저 데이터를 가지고 온다.         //
        // 여기서 UID는 사용자가 임의로 정하거나 Firebase 인증을 이용하여 얻은 UID를 사용하면 된다. //
        ////////////////////////////////////////////////////////////////////////////
        public void GetUserDataDB(string uid)
        {
            try
            {
                // DB.js 를 새로운 미들웨어로 추가했다면...
                StartCoroutine(UserDataGet("http://localhost:5000/DB/getSearchData?uid=", uid));
            }
            catch (Exception err)
            {
                Debug.Log(err);
            }
        }
        private IEnumerator UserDataGet(string url, string uid)
        {
            // UnityWebRequest 를 이용하여 웹 서버에 접속한다.
            using (UnityWebRequest www = UnityWebRequest.Get(url + uid))
            {
                yield return www.SendWebRequest();
    
                if (www.isNetworkError)
                {
                    Debug.Log("Error While Sending: " + www.error);
                }
                else
                {
                    Debug.LogFormat("Received: ({0}) {1}", url, www.downloadHandler.text);
    
                    // 서버가 보낸 JSON를 user클래스로 파싱한다.
                    var user = JsonUtility.FromJson<user>(www.downloadHandler.text);
    
                    Debug.Log("User의 나이는: " + user.age);
                }
            }
        }
    
        /////////////////////////////////////
        // 서버에 user 데이터를 보내 DB에 저장한다.//
        /////////////////////////////////////
        public void PostUserDataSave(string josn)
        {
            try
            {
                StartCoroutine(JsonDBSavePost("http://localhost:5000/DB/postData", json));
            }
            catch (Exception err)
            {
                Debug.Log(err);
            }
        }
    
        ///////////////////////////////////
        // DB에 저장된 user 데이터를 수정한다. //
        //////////////////////////////////
        public void PostUserDataRenewDB(user user)
        {
            try
            {
                string json = JsonUtility.ToJson(user);
    
                StartCoroutine(JsonDBSavePost("http://localhost:5000/DB/postRenewData", json));
            }
            catch (Exception err)
            {
                Debug.Log(err);
            }
        }
    
        // JSON 을 서버에 보낸다.
        private IEnumerator JsonDBSavePost(string url, string json)
        {
            using (var uwr = new UnityWebRequest(url, "POST"))
            {
                Debug.Log(json);
    
                byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(json);
                uwr.uploadHandler = (UploadHandler)new UploadHandlerRaw(jsonToSend);
                uwr.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
                uwr.SetRequestHeader("Content-Type", "application/json");
    
                yield return uwr.SendWebRequest();
    
                if (uwr.isNetworkError)
                {
                    Debug.Log("Error While Sending: " + uwr.error);
                }
                else
                {
                    Debug.LogFormat("Received: ({0}) {1}", url, uwr.downloadHandler.text);
    
                    receiptSave = bool.Parse(uwr.downloadHandler.text);
    
                    Debug.Log(receiptSave);
                }
            }
        }
    }

     

    GetUserDataDB, PostUserDataSave, PostUserDataRenewDB 3개의 함수를 사용하여 서버와 통신한다.

     

    차후 로컬 네트워크가 아닌 곳에서 테스트를 하기 위해서는 새로운 도메인을 url에 넣어줘야한다.

     

     

    Connect

    Unity3d 구성까지 끝냈다면 터미널에서 firebase serve 명령어를 firebase hosting 프로젝트 폴더/functions 폴더에서 실행시켜 서버를 실행시킨 후 Unity3d 와 서버와 통신을 한다.

     

    Unity3d 에서 PostUserDataSave 함수를 실행시켰을때 자신의 Firebase Console 에서 Database - Cloud Firestore에 아래와 같이 데이터가 들어온다면 잘 연결된 것이다.

     

    Firebase Hosting 에 배포를 하면 로컬 네트워크가 아니라 어디서든 서버와 통신하는 것이 가능해진다.

    도메인을 받거나 설정한 후 도메인 url를 Unity3d의 Server.cs 에 적용시켜줘야한다.

     

    아래 링크의 14번에 호스팅에 대해 적혀있으니 참고하여 배포하자.

    https://seonbicode.tistory.com/37

     

    • 네이버 블러그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기