Firebase_친구_기능_(4)_ _실시간_업데이트_UI_개선 - boostcampwm-2024/and04-Nature-Album GitHub Wiki

✅ 진행한 기능

  1. 친구 목록 및 요청 목록 실시간 업데이트 구현
    • Firestore의 addSnapshotListener를 활용하여 친구 목록 및 친구 요청 목록의 변경 사항을 실시간으로 감지하고 UI에 반영.
    • TAB 버튼 클릭 없이 데이터 변경 사항이 자동 업데이트되도록 개선.
  2. 친구 요청 알림 배지 표시 기능 추가
    • Social Alarm 탭에 친구 요청 개수를 알림 배지로 표시.
    • 새로운 친구 요청 발생 시 즉각적으로 사용자에게 피드백 제공.
  3. FriendViewModel의 UID 관리 리팩토링
    • UserManager를 통해 가져온 UID를 ViewModel 내부 필드로 저장하여 재사용.
  4. 검색 관련 검색어 실시간 반영 기능 유지 및 최적화
    • 검색 쿼리에 따라 실시간으로 결과를 가져오고 검색 결과 UI에 즉시 반영되도록 개선
  5. 코드 정리 및 최적화
    • 불필요한 함수 및 파일 삭제.
    • 중복 코드 제거 및 UI 업데이트 로직 가독성 개선.
    • FriendViewModel 및 FireBaseRepository 관련 로직 정리.

💡 문제 해결 과정 기록


❓ 의문 사항

  1. Firestore 데이터를 실시간으로 UI에 반영하는 방법

    • 기존에는 Firestore에서 데이터를 get() 메서드로 가져와서 UI를 갱신했으나, 실시간 업데이트가 지원되지 않아 불편함이 있었다.
    • 그리고 이에 대해 지난 PR 리뷰에서 도윤님 덕분에 Firestore의 addSnapshotListener를 활용하면 데이터 변경을 실시간으로 감지할 수 있다는 점을 알게 되었다.

    https://github.com/boostcampwm-2024/and04-Nature-Album/pull/147

    image

  2. 친구 요청 목록과 친구 목록을 실시간으로 가져오면서도 검색 기능을 유지하는 방법

    • 실시간 업데이트와 검색 필터링 로직이 동시에 작동할 수 있는 구조를 설계해야 했다.
  3. Compose UI에서 실시간 데이터를 효율적으로 반영하는 방법

    • StateFlow와 Compose의 collectAsStateWithLifecycle을 결합하여 UI 변경 사항이 자연스럽게 반영되도록 구현 가능성을 검토했다.

💡 의문 해결

  1. Firestore 실시간 데이터 감지

    https://firebase.google.com/docs/firestore/query-data/listen?hl=ko

    • Firestore의 addSnapshotListener를 활용해 실시간 데이터를 감지하고 Flow로 변환.
    • Flow에서 StateFlow로 변환한 데이터를 Compose UI와 연동했다.
  2. 실시간 업데이트와 검색 기능의 통합

    • 실시간 데이터 업데이트는 Firestore 리스너를 활용하고, 검색 결과는 StateFlow로 유지하여 Compose UI에 반영.
  3. Compose와 StateFlow 연동

    • Compose에서 collectAsStateWithLifecycle로 StateFlow를 관찰하고, 데이터 변경 시 UI를 자동으로 재구성하도록 했다.

💥 트러블 슈팅

  1. TAB 버튼 클릭 없이 데이터 갱신
    • 문제: 기존 구현에서는 TAB 버튼을 클릭해야 데이터가 새로 Fetch되고 UI가 갱신되었다.
      • TAB 버튼 클릭 → ViewModel의 Fetch 함수 호출 → Firestore의 get() 요청 흐름
    • 해결: Firestore 리스너를 사용해 변경 사항 발생 시 자동으로 Flow에 전달되도록 했다.
      • Firestore SnapshotListener → Flow 변환 → StateFlow에 업데이트 → Compose UI 관찰 흐름
  2. 배지 숫자 표시
    • 문제: 친구 요청 개수를 알림 배지로 표시가 테스트 용도로 고정되어 있었다.
    • 해결: 친구 요청 StateFlow를 관찰해 개수를 계산하고 배지로 표시했다.
    • 추가적으로 Flow로 바꾸는 과정을 거치다가 친구 요청 필터링 로직에서 FriendStatus.RECEIVED을 하는 것을 잠시 놓치기도 했다. 열심히 코드 보고 놓친 것 발견 후 해결했다.
      • 친구 요청은 받은 요청(RECEIVED)과 보낸 요청(SENT)으로 나뉜다.
      • 알림 배지에는 사용자에게 온 친구 요청만 표시해야 하므로, FriendStatus.RECEIVED 상태를 필터링.
  3. UID 관리 리팩토링
    • 문제: UID를 여러 곳에서 호출해 중복 코드와 관리 문제 발생.
    • 해결: UID를 ViewModel의 필드로 저장하고 모든 호출에서 재사용하도록 리팩토링.

💡 Firestore 실시간 업데이트 데이터 흐름

  • 아래는 Firestore 데이터를 실시간으로 가져와 UI에 도달하기까지의 과정을 UML 다이어그램으로 표현한 것이다.
  • callbackFlow로 생성된 Flow는 콜드이고, 이를 StateFlow로 변환하면서 핫 Flow로 변화한다
+--------------------+       +--------------------------+
| Firestore          |       | FireBaseRepository      |
| (Friends Collection)|       | (getFriendsAsFlow)      |
+--------------------+       +--------------------------+
          |                               |
          | Real-time Snapshot Listener   |
          +------------------------------>|
          |                               |
          |         Emits Cold Flow       |
          +------------------------------>|
                                    +----------------------------+
                                    | FriendViewModel            |
                                    | (listenToFriends)          |
                                    |----------------------------|
                                    | Cold Flow -> Hot Flow      |
                                    | private val _friends       |
                                    | public val friends         |
                                    | (StateFlow)                |
                                    +----------------------------+
                                              |
                                              | Observes Hot Flow
                                              v
                                   +-----------------------------+
                                   | Compose UI                  |
                                   | (collectAsStateWithLifecycle)|
                                   |-----------------------------|
                                   | Observes StateFlow          |
                                   | Updates UI                  |
                                   +-----------------------------+


🌊 상세 흐름

  1. Firestore (Friends Collection):
    • addSnapshotListener를 사용해 실시간 데이터를 감지.
    • 변경된 데이터를 Snapshot 형태로 리스너에 전달.
  2. FireBaseRepository:
    • addSnapshotListenercallbackFlow로 래핑해 Flow를 생성.
    • trySend로 Flow에 변경 사항 전달.
  3. FriendViewModel:
    • Flow 데이터를 collect로 수집하고, StateFlow로 변환해 관리.
    • _friends StateFlow가 Compose UI와 연결.
  4. Compose UI:
    • ViewModel의 friends StateFlow를 관찰하고 데이터 변경 시 자동으로 재구성.

💡 실제 코드 적용 예시

1️⃣ Firestore 데이터를 Flow로 변환

override fun getFriendsAsFlow(uid: String): Flow<List<FirebaseFriend>> = callbackFlow {
        val listener = fireStore.collection(USER).document(uid).collection(FRIENDS)
            .addSnapshotListener { snapshot, e ->
                if (e != null) {
                    Log.e("getFriendsAsFlow", "Listen failed: ${e.message}")
                    trySend(emptyList())
                    return@addSnapshotListener
                }
                Log.d("getFriendsAsFlow", "Snapshot size: ${snapshot?.size() ?: 0}")
                val friendList = snapshot?.documents?.mapNotNull { documentSnapshot ->
                    try {
                        documentSnapshot.toObject(FirebaseFriend::class.java)
                    } catch (e: Exception) {
                        Log.e("getFriendsAsFlow", "Mapping failed: ${e.message}")
                        null
                    }

                } ?: emptyList()
                Log.d("getFriendsAsFlow", "Mapped friends: $friendList")
                trySend(friendList) // 데이터가 변경되면 Flow로 보냄
            }
        awaitClose { listener.remove() } // Flow가 닫힐 때 리스너 제거
    }

2️⃣ ViewModel에서 Flow 수집 및 StateFlow 저장

private val _friends = MutableStateFlow<List<FirebaseFriend>>(emptyList())
val friends: StateFlow<List<FirebaseFriend>> = _friends

private fun listenToFriends() {
    uid?.let { currentUid ->
        viewModelScope.launch {
            fireBaseRepository.getFriendsAsFlow(currentUid).collect { friends ->
                _friends.value = friends
            }
        }
    }
}


3️⃣ Compose UI에서 StateFlow 관찰

@Composable
fun MyPageSocialList(friendViewModel: FriendViewModel = hiltViewModel()) {
    val friends by friendViewModel.friends.collectAsStateWithLifecycle()

    LazyColumn {
        items(items = friends, key = { it.user.uid }) { friend ->
            MyPageSocialItem(friend)
        }
    }
}


✅ 구현한 기능 요약

  1. Firestore 실시간 업데이트 적용:
    • Firestore의 addSnapshotListener를 Flow로 변환해 StateFlow로 관리.
  2. TAB 버튼 클릭 없이 데이터 갱신:
    • 실시간 데이터 변경 사항이 StateFlow를 통해 UI에 자동 반영되도록 구현.
  3. 친구 요청 알림 배지 표시:
    • 친구 요청 개수를 계산해 Social Alarm 탭에 배지로 표시.
  4. UID 관리 리팩토링:
    • UID를 ViewModel 필드로 저장하고 모든 호출에서 재사용.

🎥 동작 영상

[친구기능4.친구 요청 수락 UI 바로 업데이트]

https://github.com/user-attachments/assets/3540ac86-491a-44ab-9f70-735c7dde7938

(나머지 영상은 촬영하지 못하였으나 정상 작동 확인했습니다!)

💭 느낀 점

  1. Flow와 Firebase의 결합은 어렵다. (사실 둘 다 처음이라 둘 다 어렵다)
    • callbackFlow를 처음 사용할 때 데이터 흐름이 익숙하지 않아서 이해하는 데 시간이 걸렸다.
    • 특히, Cold Flow와 Hot Flow의 차이, emitcollect의 동작 방식, 그리고 StateFlow를 활용해 Compose UI와 연결하는 과정이 복잡하게 느껴졌다.
    • Firebase의 addSnapshotListener로 실시간 데이터를 감지하고 이를 Flow로 변환해 관리하는 방식이 아직도 쉽지는 않다.
  2. 하지만 데이터의 흐름이 느껴지는 것 같았다.
    • Flow를 사용하면서 데이터의 변경 사항이 자연스럽게 UI에 반영되는 흐름이 느껴졌다.
    • 정확히 모든 흐름을 이해했다고는 할 수 없지만, Flow를 사용하면서 데이터가 어떻게 움직이는지 조금씩 느껴지는 점이 흥미로웠다.
  3. 더 학습해야 할 과제가 많다.
    • Flow와 Compose의 연동 방식을 더 깊이 학습할 필요가 있다.
    • 특히, 다른 방식과 비교해 Flow를 사용하는 것이 어떤 장점과 차이점을 가지는지 구체적으로 이해하고 싶다.
    • 앞으로 다양한 방식으로 데이터를 관리해보면서 Flow의 진정한 강점을 체감하고 싶다.
  4. 결과물을 통해 성취감을 느꼈다.
    • Firestore 데이터를 Flow로 변환하고, Compose와 StateFlow를 연동해 실시간 UI 업데이트를 구현한 점은 뿌듯했다.
    • TAB 버튼 클릭 없이도 실시간으로 친구 목록과 요청 목록이 자동으로 업데이트되는 걸 확인하면서 성취감을 느꼈다.
    • 특히, 친구 요청 알림 배지가 즉각적으로 업데이트되는 모습을 보며 노력의 결과를 체감할 수 있었다.
  5. Firebase의 실시간 업데이트는 강력하다.
    • Firebase의 addSnapshotListener와 Flow를 결합하니, 사용자 경험(UX) 측면에서 큰 개선이 이루어졌다.
    • 단순히 데이터를 가져오는 get() 방식과는 비교할 수 없을 정도로 효율적인 실시간 데이터 처리 방식을 직접 체감했다.

✨ 나의 한 줄 소감

"Flow를 통해 데이터의 흐름을 조금씩 느끼기 시작했고, 이를 더 잘 활용하기 위해 학습을 이어가야겠다는 동기를 얻었다!"