Skip to content

마이페이지_ViewModel을_통한_UI_상태_관리

yujin45 edited this page Nov 20, 2024 · 3 revisions

💥 마이페이지 ViewModel을 통한 UI 상태 관리

문제 발견 계기

Firestore에 사용자 정보를 저장하는 로직을 추가한 후, 다른 계정으로 로그인했을 때 Firestore에 새로운 사용자 정보는 성공적으로 기록되었지만, 화면에 표시된 사용자 이메일 및 프로필 사진이 갱신되지 않는 것을 확인하였다.

이를 통해, UI 갱신에 필요한 상태 관리가 올바르게 이루어지지 않음을 발견하였다. 문제의 근본 원인을 추적한 결과:

  • UI 상태가 ViewModel에서 관리되지 않았고, UI가 상태 변화에 반응하지 않았다.

기존 동작 방식

  1. 초기 UI 상태 설정 (setInitUiState):
    • UserManager.isSignIn()을 사용해 사용자가 로그인 상태인지 확인.
    • 로그인 상태라면 UiState.Success로 설정하여 사용자의 이메일과 프로필 이미지를 표시.
    • 로그인 상태가 아니라면 UiState.Idle 상태를 유지.
  2. 로그인 처리 (signInWithGoogle):
    • Google 로그인을 통해 인증이 완료되면, AuthResponse.Success를 수신.
    • 이후 _uiStateUiState.Success로 갱신하여 사용자 정보를 업데이트.
  3. UI 렌더링:
    • UiState.Success: 사용자의 프로필 이미지와 이메일을 화면에 표시.
    • UiState.Idle: 기본 "로그인 버튼" 화면을 표시.

현재 문제

로그인 성공 시 UiState.Success로 UI 상태가 설정되지만, 다시 UiState.Idle로 전환되는 로직이 없어 상태가 고정되는 문제가 발생하고 있었다.

코드 분석

signInWithGoogle 메서드

로그인 성공 시 UiStateSuccess로 설정하는 로직이다.

fun signInWithGoogle(context: Context) {
    authenticationManager.signInWithGoogle(context).onEach { response ->
        when (response) {
            is AuthResponse.Success -> {
                _uiState.emit(UiState.Success) // 상태를 Success로 변경
            }
        }
    }.launchIn(viewModelScope)
}

하지만, 아래와 같은 문제가 있었다:

  1. UiState가 Success 상태로 고정됨

    로그인 성공 후 UiStateSuccess로 변경된 이후, 다시 Idle 상태로 전환하는 로직이 없다.

    따라서, 로그인 이후 UI는 항상 Success 상태로 유지되며 UI의 업데이트가 이루어지지 않는다.

  2. UI 갱신 불가능

    UI는 UiState를 기반으로 재구성되는데, 상태가 고정되어 새로운 계정으로 로그인해도 갱신되지 않았다.


이전 상태 및 문제점

1. UI 상태 관리가 부족함

아래와 같이 UI가 UserManager.getUser()를 통해 Firebase의 사용자 정보를 직접 조회하고 있었다.

when (uiState.value) {
    is UiState.Success -> {
        val user = UserManager.getUser() // Firebase 직접 조회
        val email = user?.email
        val photoUri = user?.photoUrl
        UserProfileContent(uri = photoUri, email = email)
    }
}

문제점

  1. ViewModel이 아닌 UI에서 상태 관리

    UI Composable이 Firebase와 직접 통신하며 상태를 관리하여 Single Source of Truth를 위배하고 있었다.

  2. UI 갱신 문제

    UiState.Success로 상태가 고정되면서, 새로운 계정으로 로그인해도 화면이 갱신되지 않았다.


개선된 코드 및 해결 방법

문제를 해결하기 위해 다음과 같은 변경 사항을 적용하였다.

사용자 정보 상태를 ViewModel에서 관리

  • MutableStateFlow를 통해 사용자 정보(email, photoUrl, displayName)를 상태로 관리.
  • UI는 ViewModel의 상태를 구독하여 변경 사항에 반응하도록 수정.

변경된 ViewModel 코드

@HiltViewModel
class MyPageViewModel @Inject constructor(
    private val authenticationManager: AuthenticationManager
) : ViewModel() {
   ...
       private val _userEmail = MutableStateFlow<String?>(null)
    val userEmail: StateFlow<String?> = _userEmail

    private val _userPhotoUrl = MutableStateFlow<String?>(null)
    val userPhotoUrl: StateFlow<String?> = _userPhotoUrl

    private val _userDisplayName = MutableStateFlow<String?>(null)
    val userDisplayName: StateFlow<String?> = _userDisplayName
...

    fun signInWithGoogle(context: Context) {
        authenticationManager.signInWithGoogle(context).onEach { response ->
            when (response) {
                is AuthResponse.Success -> {
                   // 로그인 성공 시 사용자 정보 업데이트
                    val user = UserManager.getUser()
                    _userEmail.value = user?.email
                    _userPhotoUrl.value = user?.photoUrl.toString()
                    _userDisplayName.value = user?.displayName

                    _uiState.emit(UiState.Success)
                }
            }
        }.launchIn(viewModelScope)
    }

    private fun setInitUiState(): UiState {
        return if (UserManager.isSignIn()) {
            UiState.Success
        } else {
            UiState.Idle
        }
    }
}

변경된 UI 코드

UI는 ViewModel의 상태를 구독하여 사용자 정보를 표시하도록 수정되었다.

@Composable
fun MyPageScreen(
    navigateToHome: () -> Unit,
    myPageViewModel: MyPageViewModel = hiltViewModel(),
) {
...
   val userEmail = myPageViewModel.userEmail.collectAsStateWithLifecycle()
    val userPhotoUrl = myPageViewModel.userPhotoUrl.collectAsStateWithLifecycle()
    val userDisplayName = myPageViewModel.userDisplayName.collectAsStateWithLifecycle()
    ...

    Scaffold(topBar = {
    ...
     Column(
        ...
        ) {
            when (uiState.value) {
                is UiState.Success -> {
                    UserProfileContent(
                        uri = userPhotoUrl.value,
                        email = userEmail.value,
                        displayName = userDisplayName.value,
                    )
                }

                else -> {
                    UserProfileContent()
    ...
        }
    }
}

결론

1. 현재 상태

  • Firestore에 사용자 정보를 저장하는 기능을 테스트하면서 발생한 UI 갱신 문제를 해결하였다.
  • 사용자 상태 관리가 ViewModel에서 이루어지도록 개선되어 로그인된 계정에 따라 UI가 자동으로 갱신되도록 구현되었다.
  • 현재는 UiState를 사용하지 않고, userEmail, userPhotoUrl, userDisplayNameStateFlow로 구독하여 UI를 갱신하는 방식으로 구현되었다.
  • 이 방식은 ViewModel의 상태를 기반으로 UI가 반응하도록 하여, 친구 기능 테스트를 위한 요구를 충족한다.

2. 문제점

  • UiState를 활용하지 않고 StateFlow만 구독하는 현재 방식은, 로그아웃 후 재로그인 시 UI 상태 변경을 명확히 반영하기 어렵다.
  • UI 상태 변경 로직이 중복되거나 흩어질 가능성이 있으며, 상태 전환 관리가 명시적이지 않다.
  • StateFlow 기반의 UI 갱신 방식은 테스트 목적에는 적합하지만, 확장성과 유지보수성 측면에서 제약이 있다.

3. 추후 개선 방향

  • 로그아웃 후 재로그인 시 UiState를 활용하여 상태 전환을 명시적으로 관리하는 구조로 개선해야 한다.
  • 사용자 상태 관리 및 데이터 흐름을 *Single Source of Truth(SSOT)로 설계하여 상태와 데이터 관리의 일관성을 강화해야 한다.
  • 현재의 테스트 중심 구현 방식을 기반으로, 친구 추가 기능 개발이 완료된 후 구조를 재검토하고 논의하여 더 나은 상태 관리 체계로 재설계해야 한다.

최종 결론

현재 구현은 친구 추가 기능 개발을 위한 테스트 목적의 임시 구현이며, Firestore에 사용자 정보를 저장하고 UI를 갱신하는 문제를 해결하는 데 초점이 맞춰져 있다. 그러나 상태 관리 방식의 확장성과 유지보수성을 고려하여, 사용자 상태 관리 및 UI 갱신 로직의 구조를 개선하거나 재설계할 필요성이 있다. 프로젝트의 다음 단계에서는 이러한 논의를 통해 더 나은 구조로 발전시킬 계획이다.


*Single Source of Truth(SST)

  • 모든 데이터와 상태를 단일한 출처에서 관리하여 데이터의 일관성과 예측 가능성을 유지하는 원칙이다. Android에서 ViewModel과 StateFlow를 사용하여 상태를 관리하는 방식이 대표적인 사례다. SST를 준수하면 코드의 가독성, 유지보수성, 확장성이 크게 향상되지만, 초기 설계와 복잡한 상태 관리는 고려해야 할 사항이다.
Clone this wiki locally