์ƒํƒœ ๊ด€๋ฆฌ์™€ UIState

๐Ÿ’ก์šฐ์•„ํ•œํ…Œํฌ์ฝ”์Šค ๊ณผ์ •์—์„œ ์ ๊ด€์šฉํ•œ ์ƒํƒœ ๊ด€๋ฆฌ์™€ UIState์— ๋Œ€ํ•˜์—ฌ ์ •๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

์ƒํƒœ ๊ด€๋ฆฌ

  • UIState๋Š” ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ํŒจํ„ด์„ ํ™œ์šฉํ•˜๋Š” ์ค‘์š”ํ•œ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค.

Android์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ UI ๋ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ค‘์š”ํ•œ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค. ํŠนํžˆ MVVM ํŒจํ„ด์ด๋‚˜ Jetpack Compose ๋“ฑ์—์„œ ์ƒํƒœ ๊ด€๋ฆฌ๋Š” ํ•„์ˆ˜์ ์ธ ์š”์†Œ์ด๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด UI๊ฐ€ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜์˜ํ•˜๊ณ  ์‚ฌ์šฉ์ž์˜ ์•ก์…˜์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

State ManageMent

  • ์•ˆ๋“œ๋กœ์ด๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋‹ค์–‘ํ•œ ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • Loading
    • Success
    • Error
    • Init
  • UI ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ํด๋ž˜์Šค๋‚˜ ์ƒํƒœ ๊ฐ์ฒด๋ฅผ ํ™œ์šฉํ•˜๋ฉด UI์™€ ViewModel์ด ์ƒํ˜ธ์ž‘์šฉํ•˜์—ฌ ์ƒํƒœ์— ๋”ฐ๋ฅธ UI๋ฅผ ๊ฐฑ์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

UI State ํŒจํ„ด

  • ๊ฐ ์ƒํƒœ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ •์˜ํ•˜๊ณ , ์ด๋ฅผ ํ†ตํ•ด UI๊ฐ€ ์–ด๋–ค ์ƒํƒœ์— ์žˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Sealed interface

  • Sealed interface์™€ Data Object๋งŒ์„ ํ™œ์šฉํ•ด์„œ UI State๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
sealed interface ClubListUiState {
    data object Init : ClubListUiState

    data object NotData : ClubListUiState

    data object NotAddress : ClubListUiState

    data object Error : ClubListUiState
}
  • sealed interface๋ฅผ ํ™œ์šฉํ•œ ์ƒ์†์ฑ„๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ , ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด์„œ ๊ฐ ์ƒํ™ฉ์— ๋งž๋Š” UI๋ฅผ ๋Œ€์‘ํ•ฉ๋‹ˆ๋‹ค.
viewModel.uiState.observe(viewLifecycleOwner) { state ->
    when (state) {
        ClubListUiState.Init -> applyViewVisibility(binding.includeClubList.rcvClubListClub)

        ClubListUiState.NotAddress -> applyViewVisibility(binding.includeClubAddress.linearLayoutClubNotAddress)

        ClubListUiState.NotData -> applyViewVisibility(binding.includeClubData.linearLayoutClubNotData)

        ClubListUiState.Error -> applyViewVisibility(binding.includeClubError.linearLayoutClubError)
    }
}
  • sealed Interface์™€ data object๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ƒํ™ฉ์— ๋งž๋Š” ๋ทฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ Sealed Class์™€ data Class๋ฅผ ํ•จ๊ป˜ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Sealed Class

  • sealed Class๋Š” data Class์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๊ฐ์ฒด๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Sealed Class๋กœ UI ์ƒํƒœ๋ฅผ ์ •์˜ํ•˜๋ฉด ์ƒํƒœ์— ๋งž๋Š” ๊ฐ’์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
sealed class UIState<out T> {
    object Loading : UIState<Nothing>()
    data class Success<out T>(val data: T) : UIState<T>()
    data class Error(val exception: Throwable) : UIState<Nothing>()
    object Empty : UIState<Nothing>()
}
  • ๊ฐ ์ƒํƒœ์— ๋งž๋Š” ๊ฐ’์„ ์ €์žฅํ•˜๊ณ , ์ƒํƒœ ๋ณ€ํ™” ์‹œ View๋กœ data๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ViewModel ํ™œ์šฉ

  • ViewModel์—์„œ LiveData๋‚˜ StateFlow๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ์„ฑํ•œ UIState๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ†ตํ•ด์„œ ์‚ฌ์šฉ์ž์˜ ์•ก์…˜์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๊ณ , UIState๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UIState<List<String>>>()
    val uiState: LiveData<UIState<List<String>>> get() = _uiState

    fun fetchData() {
        _uiState.value = UIState.Loading
        viewModelScope.launch {
            try {
                val data = repository.getData()  // ๋ฐ์ดํ„ฐ ๋กœ๋“œ
                if (data.isEmpty()) {
                    _uiState.value = UIState.Empty
                } else {
                    _uiState.value = UIState.Success(data)
                }
            } catch (e: Exception) {
                _uiState.value = UIState.Error(e)
            }
        }
    }
}

UI์—์„œ ์ƒํƒœ ๋ Œ๋”๋ง

  • UI ์—์„œ ViewModel์ด ๊ด€๋ฆฌํ•˜๋Š” UIState๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ์ƒํƒœ์— ๋”ฐ๋ผ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.observeAsState()

    when (uiState) {
        is UIState.Loading -> {
            // ๋กœ๋”ฉ ํ™”๋ฉด ํ‘œ์‹œ
            CircularProgressIndicator()
        }
        is UIState.Success -> {
            // ๋ฐ์ดํ„ฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๋“œ๋˜์—ˆ์„ ๋•Œ
            val data = (uiState as UIState.Success).data
            Text("Data: $data")
        }
        is UIState.Empty -> {
            // ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๋•Œ
            Text("No data available.")
        }
        is UIState.Error -> {
            // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ
            val error = (uiState as UIState.Error).exception
            Text("Error: ${error.message}")
        }
    }
}
  • ๋กœ๋”ฉ ์„ฑ๊ณต, ์‹คํŒจ, ์™„๋ฃŒ ๋“ฑ ๋‹ค์–‘ํ•œ ์ƒํƒœ์— ๋”ฐ๋ผ UI๊ฐ€ ๋ Œ๋”๋ง ๋ฉ๋‹ˆ๋‹ค.

์ƒํƒœ ๊ด€๋ฆฌ์˜ ์ค‘์š”์„ฑ

  • UIState๋Š” ์•ฑ์—์„œ ๋‹ค์–‘ํ•œ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , ๊ทธ ์ƒํƒœ์— ๋”ฐ๋ผ UI๋ฅผ ๋™์ ์œผ๋กœ ๋ณ€ํ™”์‹œํ‚ฌ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ์ค‘์š”ํ•œ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.
  • Android์—์„œ๋Š” ViewModel๊ณผ LiveData, StateFlow๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋Ÿฌํ•œ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  UI๊ฐ€ ์ด๋ฅผ ๊ตฌ๋…ํ•˜๋„๋ก ํ•˜์—ฌ ์ ์ ˆํ•œ ํ™”๋ฉด์„ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ผ๊ด€์„ฑ ์žˆ๋Š” UI ์ œ๊ณต

  • ์•ฑ์˜ ์ƒํƒœ ๋ณ€ํ™”์— ๋”ฐ๋ผ ์ผ๊ด€๋˜๊ฒŒ UI๋ฅผ ๊ฐฑ์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์œ ์ง€๋ณด์ˆ˜์„ฑ

  • ๊ฐ ์ƒํƒœ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ์ฝ”๋“œ๊ฐ€ ๋” ์ฝ๊ธฐ ์‰ฝ๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.

๋ฒ„๊ทธ ๋ฐฉ์ง€

  • ์ƒํƒœ์— ๋”ฐ๋ผ UI๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•จ์œผ๋กœ์จ ์˜ค๋ฅ˜, ๋นˆ List ๋“ฑ์˜ ์˜ˆ์™ธ์ ์ธ ๊ฒฝ์šฐ๋ฅผ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.