친구_검색_실시간_UI_STATUS_반영_문제_해결 - boostcampwm-2024/and04-Nature-Album GitHub Wiki

💥 친구 검색 실시간 UI STATUS 반영 문제 해결

관련 PR: 친구 기능 탭 내용 UI 실시간 업데이트 및 배지 표시 기능 추가 #177

image

배경

  • 문제: 친구 검색 화면에서 친구 요청 버튼을 누르거나, 누군가 요청을 수락했을 때 STATUS가 변경된다. 변경된 STATUS가 즉시 UI에 반영되기를 원했지만, Compose와 Firestore 간 데이터 처리 방식에서 STATUS 변경이 실시간으로 반영되지 않아 쿼리를 다시 요청하는 방식으로 임시 대응하고 있었다.
  • 이슈 원인:
    • FirestoreaddSnapshotListener는 기본적으로 상위 컬렉션(USER)의 변경 사항만 감지하며, 하위 컬렉션(FRIEND_REQUESTS 등)의 변경 사항은 자동으로 감지하지 않는다.
    • 결과적으로 하위 항목인 FRIEND_REQUESTSstatus 필드가 변경되어도 이를 감지할 수 없었다.

원인 분석

  • 도윤님과 함께 어떠한 곳에서 문제 원인이 있는지 함께 살펴보았다. status 변경 시 리스너 작동이 안되는 것 같았는데 도윤님께서 아래와 같은 원인을 발견했다.

  • 기존 코드: searchUsersAsFlow 함수에서 USER 컬렉션의 변경 사항만 감지하고 있었으며, FRIEND_REQUESTS 하위 컬렉션의 변경 사항은 감지하지 않았다.

    val listener = fireStore.collection(USER)
        .whereGreaterThanOrEqualTo(EMAIL, query)
        .whereLessThanOrEqualTo(EMAIL, query + QUERY_SUFFIX)
        .addSnapshotListener { snapshot, e ->
            ...
            // 하위 FRIEND_REQUESTS 변경 사항 감지 불가
        }
    
    

해결 방안 검토 및 최종 결론

해결 방안 검토

  1. 하위 컬렉션에 리스너 추가
    • 각 문서의 하위 컬렉션(FRIEND_REQUESTS)에 addSnapshotListener를 추가하여 STATUS 변경 사항을 실시간으로 감지.
    • 장점: STATUS 변경 사항을 즉각적으로 감지하여 UI에 반영 가능.
    • 단점:
      • 문서별로 리스너가 추가되어 네트워크 비용 및 Firestore 읽기 횟수가 크게 증가.
      • 리소스 관리 및 복잡도 증가로 인해 유지보수 어려움.
  2. UI와 데이터 처리를 분리하여 사용자 경험 개선
    • STATUS 변경을 실시간으로 반영하지 않고, 버튼 클릭 시 UI에서 "보냈음"과 같은 시각적 피드백을 제공.
    • 화면 새로고침 또는 다른 화면 전환 시 최신 데이터를 로드하도록 설계.
    • 장점:
      • 네트워크 비용 절약 및 리소스 효율성 증가.
      • UI에서 상태를 즉각적으로 반영하여 사용자 경험 유지 가능.
    • 단점: STATUS 실시간 반영 대신 사용자 경험을 위한 시각적 처리로 타협.

팀 논의 및 결론

  • 도윤님 제안: STATUS 변경 사항의 실시간 반영 대신 UI에서 버튼 상태를 변경하여 STATUS 변경을 시각적으로 표현.
    • 버튼 클릭 시 "보냈음" 상태 표시 및 비활성화 처리.
    • 화면 전환이나 새로고침 시 최신 STATUS 데이터를 불러와 동기화.
  • 최종 결정:
    • STATUS 실시간 반영을 강제하지 않고, UI에서 적절한 피드백 제공으로 문제를 해결.
    • 네트워크 비용을 줄이고, UI 상태 관리로 사용자 경험을 유지하는 방식 선택.

최종 결론

STATUS 변경 사항을 실시간으로 반영하려는 복잡한 처리를 하지 않고, UI에서 "보냈음" 상태를 표시하고 버튼을 비활성화하여 사용자 경험을 유지한다.


구체적인 해결 방안

  1. UI에서 버튼 비활화 처리
    • 친구 요청 버튼을 눌렀을 때, UI에서 해당 버튼을 비활성화하고 "보냈음"과 같은 메시지를 표시한다.
    • STATUS가 업데이트된 최신 데이터는 사용자가 화면을 새로고침하거나, 다른 화면에서 돌아올 때 자동으로 반영되도록 처리한다.
  2. Compose에서 UI 변경
    • 버튼 상태에 따라 "보냄" 텍스트 표시 및 클릭 비활성화.

진행하던 중 추가 문제 상황

친구 요청 화면에서 "친구 요청" 버튼을 눌렀을 때 STATUS를 업데이트하고 UI에서 이를 즉시 반영하고자 하였다. 기존에는 검색된 결과 리스트에서 targetUid를 찾기 위해 리스트 전체를 순회하며 STATUS를 업데이트하였는데, 검색 결과가 많아질 경우 성능 문제가 우려되었다.

순회 과정이 오래 걸릴 경우 사용자가 느끼는 반응 속도가 저하될 수 있으니 개선할 방법이 필요하였다. 이를 해결하기 위해 도윤님께 조언을 구하였고, 간단하면서도 효과적인 해결책을 제시받았다.


도윤님과의 대화

도윤님께 "검색 결과가 많아지면 리스트를 순회하는 방식이 오래 걸리지 않을까요? 어떻게 해결할 수 있을까요?"라고 여쭈었다. 그러자 도윤님이 "Map으로 바꾸면 됩니다. Key 값으로 바로 찾으면 성능 문제가 줄어요."라고 알려주셨다.

"와, 이런 해결책을 바로 생각하시다니 어떻게 하셨죠?"라고 감탄하며 물었더니, 도윤님이 "코딩 테스트 풀다 보면 자연스럽게 이런 생각이 떠오릅니다."라고 답해주셨다. 이 대화로 Map의 장점을 활용해 문제를 해결할 방향성을 잡을 수 있었다.


문제 해결 과정

1. 데이터 구조 개선: Map으로 전환

  • 문제: 기존에 검색 결과를 List<FirestoreUserWithStatus>로 관리하여 친구 요청 STATUS를 업데이트할 때 리스트를 순회하며 데이터를 수정했다. 검색 결과가 많아질 경우 리스트 순회에 시간이 오래 걸릴 수 있어 성능 문제가 우려되었다.
  • 해결: Map<String, FirestoreUserWithStatus>로 구조를 변경하여 Key 값으로 targetUid를 사용, 데이터 접근을 O(1) 시간 복잡도로 최적화하였다.
// ViewModel에서 StateFlow의 데이터 구조를 Map으로 변경
private val _searchResults = MutableStateFlow<Map<String, FirestoreUserWithStatus>>(emptyMap())
val searchResults: StateFlow<Map<String, FirestoreUserWithStatus>> = _searchResults

2. Firestore에서 Map 데이터 반환

  • Firestore 쿼리 결과를 Map<String, FirestoreUserWithStatus>로 변환하여 반환하도록 searchUsersAsFlow 함수를 수정하였다.
  • 쿼리 결과를 반복하면서 사용자 데이터와 STATUS를 Map에 추가하며, STATUS 변경이 발생할 때 즉시 UI에 반영할 수 있도록 trySend를 호출하였다.
override fun searchUsersAsFlow(
    uid: String,
    query: String
): Flow<Map<String, FirestoreUserWithStatus>> = callbackFlow {
   ....
            val userMap = mutableMapOf<String, FirestoreUserWithStatus>()
...
                    userMap[userDoc.id] = FirestoreUserWithStatus(user, friendStatus)
                    trySend(userMap).isSuccess
...
}

3. UI 상태 업데이트 최적화

  • 친구 요청 버튼 클릭 후 STATUS를 업데이트할 때 _searchResults에서 Key 값으로 targetUid를 바로 검색하여 업데이트하도록 수정하였다.
  • 이를 통해 리스트 전체를 순회하는 비효율성을 제거하였다.
fun sendFriendRequest(uid: String, targetUid: String) {
    viewModelScope.launch {
        val success = fireBaseRepository.sendFriendRequest(uid, targetUid)
        if (success) {
            _searchResults.value = _searchResults.value.toMutableMap().apply {
                this[targetUid] = this[targetUid]?.copy(status = FriendStatus.SENT) ?: return@launch
            }
        }
    }
}

4. UI에서 Map 기반으로 데이터 접근 및 렌더링

  • Compose UI에서 검색 결과를 Map으로 받아와 LazyColumn에서 forEach를 사용해 각 사용자 데이터를 렌더링하도록 변경하였다.
  • 친구 요청 버튼은 STATUS가 NORMAL일 때만 활성화되도록 처리하였다.
@Composable
fun RequestedList(
    userWithStatusList: Map<String, FirestoreUserWithStatus>,
    currentUid: String,
    sendFriendRequest: (String, String) -> Unit,
) {
    LazyColumn {
        userWithStatusList.forEach { (uid, userWithStatus) ->
            item(key = uid) {
                RequestedItem(
                    userWithStatus = userWithStatus,
                    currentUid = currentUid,
                    sendFriendRequest = sendFriendRequest
                )
            }
        }
    }
}

@Composable
fun RequestedItem(
    userWithStatus: FirestoreUserWithStatus,
    currentUid: String,
    sendFriendRequest: (String, String) -> Unit,
) {
    ...
        SuggestionChip(
            onClick = {
                if (userWithStatus.status == FriendStatus.NORMAL) {
                    sendFriendRequest(currentUid, userWithStatus.user.uid)
                }
            },
            enabled = userWithStatus.status == FriendStatus.NORMAL,
            label = {
                val text = when (userWithStatus.status) {
                    FriendStatus.SENT -> "보냈음"
                    FriendStatus.RECEIVED -> "수락 대기"
                    FriendStatus.FRIEND -> "친구"
                    else -> "친구 요청"
                }
                Text(text = text)
            }
        )
    }
}


결론

image

  • STATUS 변경 사항을 실시간으로 반영하려는 복잡한 로직 대신, UI에서 버튼 상태를 변경하여 사용자가 "요청 보냄" 상태를 명확히 알 수 있도록 처리하였다. STATUS가 최신 상태로 업데이트되는 것은 화면을 새로고침하거나 다른 화면으로 이동 후 돌아올 때 이루어진다.
  • 검색 결과를 List에서 Map으로 전환하여 성능 문제를 해결하였다. ViewModel과 Firestore에서 Map을 일관되게 사용하여 Key 기반 데이터 접근이 가능해졌고, UI 상태 업데이트 시 불필요한 리스트 순회를 제거하였다.
  • Compose UI에서 NORMAL 상태일 때만 버튼을 활성화하도록 처리하여 STATUS 변경 후 발생할 수 있는 중복 동작을 방지하였다.
  • 리스트 순회 방식에서 Map 기반의 Key 검색 방식으로 변경함으로써 검색 결과가 많아질 경우에도 빠르게 업데이트가 가능해졌으며, UI 반응 속도도 개선되었다.
  • 전체적으로 성능 개선, 네트워크 비용 감소, 사용자 경험 유지라는 목표를 모두 달성하였다. 특히, 도윤님의 코딩 테스트 경험에서 나온 조언 덕분에 문제를 효율적으로 해결할 수 있었다.

느낀 점

  • 이번 작업을 통해 실시간 데이터 반영의 어려움과 Firestore와 Compose를 함께 사용할 때 발생하는 문제를 효과적으로 해결할 수 있었다. 특히, 기존의 리스트 순회 방식을 Map 기반으로 전환하는 과정에서 데이터 구조 설계가 얼마나 중요한지 다시 한번 느낄 수 있었다.
  • 도윤님께서 제안해주신 Map 전환 방식은 간단하지만 강력한 해결책이었고, 실제로 성능과 코드 가독성 모두를 개선하는 데 크게 기여하였다. 평소 코딩 테스트를 통해 알고리즘 사고를 훈련한 경험이 실무 문제를 해결하는 데도 연결될 수 있다는 점을 알게 되어 인상 깊었다.
  • 또한, Firestore의 동작 방식에 대한 깊은 이해가 중요하다는 것도 배웠다. 상위 컬렉션과 하위 컬렉션 간 리스너의 동작 차이를 알게 되었고, 이를 고려하여 해결 방안을 설계한 과정은 앞으로 다른 프로젝트에서도 활용할 수 있을 것 같다.
  • Compose UI와 상태 관리의 중요성도 느낄 수 있었다. UI와 데이터 처리를 분리하고, 적절한 상태 관리 방식(StateFlow)을 활용하여 사용자 경험을 유지하는 방식을 고민하면서 UI와 비즈니스 로직의 역할을 명확히 나누는 것이 왜 중요한지 체감할 수 있었다.
  • 마지막으로, 동료와의 협업이 문제를 해결하는 데 있어 얼마나 큰 도움을 주는지도 깨달았다. 혼자만의 아이디어로는 한계가 있었겠지만, 도윤님의 조언과 팀원들과의 논의를 통해 최적의 해결책을 찾아낼 수 있었다. 앞으로도 문제를 단독으로 해결하려 하기보다는 팀원들과 적극적으로 소통하고 협업을 통해 더 나은 결과를 만들어가야겠다는 다짐을 하게 되었다.