본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
https://fastcampus.info/4n8ztzq
Part.3 Firebase를 활용하여 구축하는 클라이언트 네트워크
CH 04. 사용자 순위 구현하기
01. 사용자 순위 (leaderboard) 구현하기 (2)
FirebaseInitializer 는 안드로이드 앱을 제작을 할 때 구글 플레이에 대한 초기화를 해주는 함수라고 보면 됩니다.
FirebaseInitializer는 static class 로 함수 호출과 이벤트만 처리를 하겠습니다.

저희가 필요한 것은 (static으로 추가)
DependencyStatus 를 인자로 가지는 Action들의 리스트 `initializedCallback`
마찬가지로 Action 들의 리스트 `activateFetchCallbacks`
그리고 `dependencyStatus`를 변수로 추가하겠습니다.
각 상태를 확인할 수 있는 bool 변수들 추가 `initialized`, `fetching`, `activateFetched`
이제 초기화를 위한 Initialize 함수를 생성하겠습니다.

일단 lock으로 initializedCallbacks 들을 임시로 잠금니다.
만일 초기화가 완료가 되어 있는 상황이라면, 콜백을 호출해줍니다.
그렇지 않다면, initializedCallbacks 에 콜백을 추가해주고, FirebaseApp의 CheckAndFixDependenciesAsync 를 호출하겠습니다. ContinueWith (task) 로 작업을 처리할 것이고, lock initializedCallbacks 를 다시 잠그고 dependency status는 task의 result로 할당. 그리고 초기화가 되었다는 표시를 하고 CallInitializedCallbacks 함수를 호출하겠습니다.
CallInitializedCallbacks 함수는

initialized 가 더 이상 변경되지 않도록 먼저 잠궈주시고, foreach 문을 통해서 callbacks를 단순히 호출하면 됩니다.
초기화가 되었다는 것을 한번 호출하면, 클리어로 초기화 함수들이 더 이상 호출되지 않도록 삭제를 하겠습니다.
이렇게 구글 플레이에 대한 초기화 오브젝트를 구현해보았습니다.
이 부분들은 설명하기 애매한게 파이어베이스 앱에서 구글 플레이에 대한 의존성이 있는 것들이 제대로 정리가 되었는지를 확인해서, 정리가 되었다 안 되었다라는 것을 단순하게 이벤트로 알려주는 오브젝트 입니다.
이렇게 생성된 것을 가지고 리더보드 컨트롤러에서 초기화를 진행해주도록 하겠습니다.

만일 dependencyStatus 가 사용이 가능하다면, initialized 가 준비가 된 상황입니다.
그렇지 않다면 구글 플레이 설정이 잘못된 것이므로 에러를 발생시킴니다.

이렇게 초기화가 준비가 되면 이제 실제 데이터베이스를 초기화시켜주게 됩니다. (InitializeDatabase)
InitializeDatabase는 유니티 에디터에서 데이터베이스에 접근을 구성하기 위해서 SetEditorDatabaseUrl 을 설정을 해줘야합니다.
(최신 SDK 에서는 이를 자동으로 해줌 ⇒ SetEditorDatabaseUrl 삭제)
`FirebaseApp.DefulatInstance.Options.DatabaseUrl` 은
google service 라는 구성파일을 에셋 폴더에 포함을 시키고, sdk를 임포트하게 되면 google-service-desktop 이라는 설정 파일이 자동으로 생성이 됩니다. 이 설정 파일에 있는 정보들이 FirebaseApp에 자동으로 추가가 됩니다.
databaseRef 에 FirebaseDatabase.DefulatInstance.RootReference 할당
상태 처리 & 초기화 이벤트 발생시키기
(리더보드 컨드롤러는 이벤트를 발생키기는 부분이 많이 추가가 될 것입니다. 이유는 리더보더 컨트롤러는 단순히 firebase의 데이터베이스와 연동되는 기능만 담당하기 위함. 유니티의 UI나 게임 로직에는 영향을 미치지 않기 위해서 다른 부분들은 유니티 모노비헤이비어나 게임로직에서 이벤트를 등록해서 어떠한 이벤트가 끝났다라는 것이 발생됐다는 것을 처리해주도록 이벤트 핸들러가 꽤 많이 추가가 됩니다.)
이렇게 해서 시작을 할 떄, 파이어베이스 데이터베이스가 초기화되는 처리를 했습니다.
이제 유저가 스코어를 데이터베이스에 쓰기 작업하는 구현을 해보겠습니다. (AddScore)

ask 로 진행이되고, 매개변수로 Userscore 의 변수 값들을 넣어줌.
TicksPerSecond로 현재 시간을 나누는 이유는 유닉스에서의 ticktime과 C#에서의 ticktime이 틀리기 때문에, 따라서 그 부분을 맞춰주기 위함임. 데이터베이스에서 얻어올 떄는 TicksPerSecond 를 곱해주면 된다.
(데이터베이스의 ticktime은 유닉스 기반의 ticktime)
UserScore 오브젝트를 생성하고,
그리고 이 UserScore를 바로 추가할 수 있는 두 번째 addscore 함수를 생성하겠습니다.

이 AddScore 과정도 thread를 사용하기 떄문에 지금 현재 addscore 과정에 있는지 확인을 하기 위해서 bool 형으로 `addingUserScore` 변수를 추가.
유저 점수를 쓰고 있는 중이라면, 더 이상ㅇ 진행하지 않고 바로 에러 메시지를 발생시켰습니다.
userScore를 Dictionary로 반환하고, addingUserScore 의 플래그를 true 설정 (현재 데이터베이스 쓰기 작업이 시작이 되었다는 것을 flag 설정)

데이터 베이스 쓰기 작업은 Thread를 사용한 작업으로 진행.

newEntry는 databaseRef 즉 루트에서 AllScoreDataPath를 push 해줍니다.
이렇게 하면 newEntry는 all_scores 데이터패스 경로로 이동을 한 상태가 됩니다.
이런 다음 newEntry! 즉 all_socres path 에 SetValueAsync 로 데이터베이스 값을 쓰겠습니다. scoreDictionary로 json 형식이 되도록 할 것이고, ContinueWith (task ⇒ {}) 로 작업이 어떻게 진행됐는지 확인합니다.

만일 task의 exception이 null 이 아니면, 에러가 발생한 것이기 때문에, Debug.LogWarning 으로 오류메시지를 출력하겠습니다.

만일 작업이 완료되지 않았다는 것(이거는 조금 특수한 상황)은 데이터베이스 쓰기 중에 네트워크가 끊겨서 완료에 대한 반환을 받지 못했다던가 그런부분에 대한 예외상황이 발생할 수 있습니다.

만일 exception이 존재하지 않고, complete 가 완료되었다면, addingUserScore는 처리가 끝난것입니다. (addingUserScore = false;)
addedScoreArgs 변수에 UserScoreArgs 를 새롭게 생성해서 userScore와 메시지를 추가하여 이벤트 변수 이벤트 파라미터로 넘길것입니다.
sendAddedScoreEvent 플래그도 true 로 설정.

addscore에 대한 처리가 끝났다는 것을 이벤트로 알려주기 위해서, 이벤트 Argument를 추가해주도록 하겠습빈다.
이렇게 `sendAddedScoreEvent` 를 true로 설정을 하게되면 이것을 업데이트에서 확인하여야 합니다.

유저가 점수를 데이터베이스에 쓰기를 완료했다고 판단했기 때문에, OnAddedScore 라는 이벤트를 발생시킵니다.
다시 한번 정리하면,
task.Run 으로 쓰레드로 유저 점수를 데이터베이스에 쓰고, 데이터베이스에 쓴 작업이 완료가 된 이후에는 유니티 메인 쓰레드가 그 부분을 알 수 있도록 업데이트에서 지속적으로 SendAddedScoreEvent 가 true로 설정이 되었는지를 확인해서, true 로 설정이 되었다면 현재 점수가 등록이 되었다는 이벤트를 다른 게임 로직으로 알려주는 구현입니다.
질문하기
✅ 첫 번째 질문:
ContinueWithOnMainThread()는 2021년에 있었을까?
그리고 강의처럼 Update()에서 처리하는 방식과 어떤 게 더 일반적인가?
🔹 1. ContinueWithOnMainThread()는 2021년에 존재했나요?
- 있었습니다.
- 이 메서드는 Firebase Unity SDK의 Task Extension으로 2020년경부터 이미 포함되어 있었어요.
- 다만, 강의에서 이 API를 사용하지 않은 이유는 다음 중 하나일 수 있습니다:
| 가능성 | 설명 |
| 🔧 강의 당시 Firebase SDK 버전이 오래되었거나 | Firebase.Extensions.TaskExtension 네임스페이스를 명시적으로 임포트하지 않아서 못 쓴 경우 |
| 💡 실습의 목적상 Update 방식으로 로직을 분리해서 보여주기 위해 | 구조적인 흐름 학습을 위해 강사가 의도적으로 선택했을 가능성 |
| 📄 Firebase 공식 문서가 ContinueWithOnMainThread를 강조하지 않았던 시절 | 초창기 문서에서는 일반 ContinueWith 기반 예제가 많았음 |
🔹 2. 게임 개발자들은 어떤 방식을 더 선호하나?
| 방식 | 설명 | 장점 | 단점 |
| ✅ ContinueWithOnMainThread() | Firebase SDK의 기본 권장 방식(메인 스레드에서 안전하게 실행됨) | 간결하고 직관적추가 관리 불필요 | Firebase SDK에 강하게 의존 |
| 🛠️ Update()에서 상태 플래그 감지 | 직접 bool flag로 비동기 완료 여부 감지Update에서 처리 | Firebase 의존성 없음게임 시스템에 맞춰 자유도 높음 | 코드가 복잡해지고 상태 관리 필요 |
✅ 요약:
- 빠르게 구현하고 단순한 UI 연결이면 → ContinueWithOnMainThread()가 권장
- 복잡한 상태 전환이 있고, 여러 비동기 흐름이 섞이는 구조라면 → Update() 방식도 유효
✅ 두 번째 질문:
왜 Unity는 메인 스레드에서만 GameObject 등 접근을 허용했는가?
🔹 Unity가 메인 스레드에서만 GameObject 등을 조작하도록 설계한 이유:
| 이유 | 설명 |
| ⚙️ 렌더링과 물리 연산의 일관성 유지 | Unity 엔진 내부의 렌더링 시스템(그래픽), 물리 엔진(PhysX) 등은 단일 흐름에서 일관되게 처리되어야 오류가 없음 |
| 🧵 동기화 비용 감소 | 다중 스레드로 GameObject에 접근하면 동기화(Mutex, Lock) 처리해야 하는데, 이는 성능 저하와 데이터 충돌 가능성을 증가시킴 |
| 🛡️ 안정성과 예측 가능성 | 싱글 스레드 기반의 게임 로직은 디버깅이 쉬우며, 예측 가능한 순서로 실행되기 때문에 초보자나 중소규모 게임 개발에 유리 |
| 🔧 엔진 내부 구현 복잡도 감소 | 멀티스레드 안전을 고려하지 않아도 되므로, 엔진 자체의 복잡도와 비용을 낮출 수 있음 |
🔸 그래서 Unity는 이렇게 디자인함:
- 렌더링 / 물리 / 오디오 / GameObject 접근 = 오직 Main Thread
- AI, 백엔드 통신, 계산 처리 등은 필요에 따라 **Worker Thread(Task 등)**로 나눔
✅ 정리하면
| 질문 요약 | 답변 |
| ContinueWithOnMainThread()는 2021년 있었나? | 있었음. 다만 버전 또는 의도적 이유로 강의에선 안 썼을 수 있음 |
| 어떤 방식이 더 좋나? | 간단한 UI 처리에는 ContinueWithOnMainThread() 권장복잡한 구조면 Update 방식도 유효 |
| 왜 Unity는 메인 스레드만 허용하나? | 일관성과 성능, 디버깅 편의성 때문. 다중 스레드 접근은 위험성과 성능 저하가 큼 |
다른 부분들도 마찬가지로 구현될것입니다. (Update, Get)

GetUserScore 라고 마찬가지로 thread 가 현재 진행이 되고 있는지를 확인해주고,
데이터베이스 레퍼런스에서 AllScoreDataPath 로 이동을 한 이후에, OrderByChild를 사용합니다. (UserIdPath를 기준으로)
즉 User Id를 기준으로 자식들을 정렬할 것이고,
StartAt, EndAt 을 userId로 넣어줍니다. 이 부분은 이제 userIdPath에서 현재 입력된 user id를 처음과 끝에서 비교를 하면서 같은 user id 끼리 취합을 하는것입니다.
그리고 GetvalueAsync로 값을 얻어오겠습니다.
다음 시간에 남은 구현들을 마무리짓겠습니다.
학습 인증 사진
① 오늘자 날짜, 공부 시작 시각 포함 사진 1장
② 오늘자 날짜, 공부 종료 시각 포함 사진 1장


③ 1개 클립 수강 인증 사진 (강의장 목록 캡쳐, 강의 내용이 담긴 수강화면이 보이지 않도록) 1장

④ 학습 인증샷 1장 이상 (ex. 필기 촬영, 작업물, 등)


⑤ 마이페이지 - 학습통계 - 챌린지 대상 강의 완강률 캡쳐 사진 1장 (완료 클립 수가 보이도록)
