이 예제에서는 MVVM 패턴과 MVI 패턴이 복잡한 상태를 어떻게 관리하는지 보여줍니다. 이 예제의 시나리오는 여러 개의 필터(가격, 카테고리, 정렬 등)를 적용할 수 있는 상품 목록 화면을 다룹니다. 필터가 동시에 적용될 수 있으며, 새로운 데이터를 서버에서 가져와 목록을 갱신해야 합니다.
MVVM 패턴에서는 ViewModel이 모든 상태를 관리하며, 필터가 변경될 때마다 상태가 업데이트됩니다. 각 필터는 별도로 관리되며, 필터가 많아질수록 로직이 복잡해질 수 있습니다.
class ProductListViewModel(
private val repository: ProductRepository
) : ViewModel() {
private val _products = MutableLiveData<List<Product>>()
val products: LiveData<List<Product>> get() = _products
private var priceFilter: PriceFilter = PriceFilter.NONE
private var categoryFilter: CategoryFilter = CategoryFilter.ALL
private var sortingOrder: SortingOrder = SortingOrder.NONE
fun applyPriceFilter(newPriceFilter: PriceFilter) {
priceFilter = newPriceFilter
fetchProducts()
}
fun applyCategoryFilter(newCategoryFilter: CategoryFilter) {
categoryFilter = newCategoryFilter
fetchProducts()
}
fun applySorting(sorting: SortingOrder) {
sortingOrder = sorting
fetchProducts()
}
private fun fetchProducts() {
viewModelScope.launch {
val result = repository.getProducts(priceFilter, categoryFilter, sortingOrder)
_products.value = result
}
}
}
뷰에서는 LiveData를 관찰하고 UI를 업데이트합니다.
class ProductListFragment : Fragment() {
private lateinit var viewModel: ProductListViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(ProductListViewModel::class.java)
viewModel.products.observe(viewLifecycleOwner, Observer { products ->
// RecyclerView를 새로운 상품 목록으로 업데이트
})
// 필터 적용
priceFilterButton.setOnClickListener {
viewModel.applyPriceFilter(PriceFilter.LOW_TO_HIGH)
}
categoryFilterButton.setOnClickListener {
viewModel.applyCategoryFilter(CategoryFilter.ELECTRONICS)
}
sortingButton.setOnClickListener {
viewModel.applySorting(SortingOrder.BY_NAME)
}
}
}
이 MVVM 구현에서 상태가 복잡해질수록 관리가 어려워집니다. 필터가 변경될 때마다 새로운 메서드가 필요해지고, 동시에 여러 필터가 변경되면 로직이 복잡해져 코드 중복이 발생할 수 있습니다.
MVI (Model-View-Intent)에서는 상태를 단일한 불변 State 객체로 모델링합니다. 모든 사용자 액션이나 이벤트는 Intent로 표현되며, 모든 상태 전환은 Reducer를 통해 이루어집니다. 이 패턴은 상태 관리를 중앙 집중화하여 복잡한 상태를 더 쉽게 처리할 수 있도록 합니다.
단일 상태는 모든 가능한 UI 상태를 나타냅니다.
data class ProductListState(
val products: List<Product> = emptyList(),
val priceFilter: PriceFilter = PriceFilter.NONE,
val categoryFilter: CategoryFilter = CategoryFilter.ALL,
val sortingOrder: SortingOrder = SortingOrder.NONE,
val isLoading: Boolean = false,
val errorMessage: String? = null
)
Intent는 사용자의 액션을 나타냅니다. 일반적으로 Intent는 sealed class로 표현됩니다.
sealed class ProductListIntent {
data class ApplyPriceFilter(val priceFilter: PriceFilter) : ProductListIntent()
data class ApplyCategoryFilter(val categoryFilter: CategoryFilter) : ProductListIntent()
data class ApplySorting(val sortingOrder: SortingOrder) : ProductListIntent()
object LoadProducts : ProductListIntent()
}
Reducer는 현재 상태와 Intent를 받아 새로운 상태를 반환합니다.
class ProductListReducer {
fun reduce(intent: ProductListIntent, currentState: ProductListState): ProductListState {
return when (intent) {
is ProductListIntent.ApplyPriceFilter -> {
currentState.copy(priceFilter = intent.priceFilter, isLoading = true)
}
is ProductListIntent.ApplyCategoryFilter -> {
currentState.copy(categoryFilter = intent.categoryFilter, isLoading = true)
}
is ProductListIntent.ApplySorting -> {
currentState.copy(sortingOrder = intent.sortingOrder, isLoading = true)
}
is ProductListIntent.LoadProducts -> {
currentState.copy(isLoading = true)
}
}
}
}
이제 ViewModel은 상태를 보유하며, Reducer를 통해 Intent를 처리합니다.
class ProductListViewModel(
private val repository: ProductRepository
) : ViewModel() {
private val _state = MutableLiveData<ProductListState>()
val state: LiveData<ProductListState> get() = _state
private val reducer = ProductListReducer()
init {
_state.value = ProductListState()
}
fun dispatch(intent: ProductListIntent) {
val currentState = _state.value ?: ProductListState()
_state.value = reducer.reduce(intent, currentState)
if (intent is ProductListIntent.LoadProducts) {
loadProducts(currentState.priceFilter, currentState.categoryFilter, currentState.sortingOrder)
}
}
private fun loadProducts(priceFilter: PriceFilter, categoryFilter: CategoryFilter, sortingOrder: SortingOrder) {
viewModelScope.launch {
try {
val products = repository.getProducts(priceFilter, categoryFilter, sortingOrder)
_state.value = _state.value?.copy(products = products, isLoading = false)
} catch (e: Exception) {
_state.value = _state.value?.copy(isLoading = false, errorMessage = e.message)
}
}
}
}
뷰에서는 Intent를 ViewModel에 보내고 상태를 관찰합니다.
class ProductListFragment : Fragment() {
private lateinit var viewModel: ProductListViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(ProductListViewModel::class.java)
viewModel.state.observe(viewLifecycleOwner, Observer { state ->
// RecyclerView를 새로운 상품 목록으로 업데이트
if (state.isLoading) {
// 로딩 인디케이터 표시
} else if (state.errorMessage != null) {
// 오류 메시지 표시
} else {
// 상품 목록 업데이트
}
})
// Intent 전송
priceFilterButton.setOnClickListener {
viewModel.dispatch(ProductListIntent.ApplyPriceFilter(PriceFilter.LOW_TO_HIGH))
}
categoryFilterButton.setOnClickListener {
viewModel.dispatch(ProductListIntent.ApplyCategoryFilter(CategoryFilter.ELECTRONICS))
}
sortingButton.setOnClickListener {
viewModel.dispatch(ProductListIntent.ApplySorting(SortingOrder.BY_NAME))
}
}
}
State 객체로 중앙 집중화하여 복잡한 UI 상호작용을 더 쉽게 관리하고 디버깅할 수 있습니다.이 예제에서 MVVM으로 복잡한 상태를 관리하는 것이 얼마나 어려울 수 있는지 확인한 후, MVI로 리팩토링하여 상태 관리를 중앙 집중화하고 더 예측 가능하고 유지보수하기 쉬운 흐름을 만들었습니다.