코딩하는 일용직 노동자

안드로이드 MVI 패턴의 핵심, Reducer 완벽 해부: 역할, 기능, 사용법 본문

안드로이드

안드로이드 MVI 패턴의 핵심, Reducer 완벽 해부: 역할, 기능, 사용법

bacass 2025. 4. 20. 16:03

안녕하세요! 오늘은 안드로이드 앱 개발 디자인 패턴 중 하나인 MVI (Model-View-Intent) 패턴의 핵심 구성 요소, Reducer에 대해 자세히 알아보겠습니다. MVI 패턴은 예측 가능하고 테스트하기 쉬운 UI 개발을 목표로 하며, Reducer는 이 아키텍처에서 상태(State)를 변경하는 순수 함수라는 중요한 역할을 담당합니다.

MVI 패턴이란?

MVI 패턴은 단방향 데이터 흐름을 강조하여 UI 개발의 복잡성을 줄이고 유지보수성을 높이는 아키텍처입니다. 주요 구성 요소는 다음과 같습니다.

  • Model (상태 - State): 화면에 표시되는 데이터와 UI의 상태를 나타냅니다. 불변성을 유지하는 것이 중요합니다.
  • View (UI): 사용자에게 데이터를 보여주고, 사용자 액션(Intent)을 ViewModel로 전달합니다.
  • Intent (의도 - Event): 사용자의 액션을 나타내는 이벤트입니다. ViewModel로 전달되어 상태 변화를 유발합니다.

그리고 오늘 우리가 집중할 핵심!

  • Reducer (상태 변경자): 현재 상태와 발생한 이벤트를 받아 새로운 상태를 생성하는 순수 함수입니다.

Reducer, 너는 도대체 무슨 역할을 하니?

Reducer는 MVI 아키텍처의 심장이자 두뇌라고 할 수 있습니다. 주요 역할은 다음과 같습니다.

  1. 상태 업데이트의 중심: 애플리케이션 내의 모든 상태 변화 로직이 Reducer를 통해 이루어집니다. 이는 상태 변화를 한 곳에서 관리하고 추적할 수 있도록 도와줍니다. 마치 교통 통제 센터처럼, 모든 상태 변경 요청을 Reducer가 관리하는 것이죠.
  2. 단방향 데이터 흐름의 핵심 연결고리: View에서 발생한 사용자의 의도(Intent)는 Event 형태로 ViewModel을 거쳐 Reducer로 전달됩니다. Reducer는 이 Event를 기반으로 새로운 상태를 만들고, 이 새로운 상태는 다시 View로 흘러가 화면을 업데이트합니다. 이 흐름은 예측 가능성을 높여줍니다.
  3. 순수 함수의 힘: Reducer는 순수 함수입니다. 이는 동일한 입력(이전 상태와 이벤트)이 주어지면 항상 동일한 출력(새로운 상태)을 반환한다는 의미입니다. 부작용(Side Effect)이 없어 예측 가능하고 테스트가 매우 용이합니다. 마치 수학 공식처럼, 정해진 입력에는 항상 정해진 결과가 나오는 것이죠.
  4. 불변성 유지의 보루: Reducer는 기존 상태를 절대로 직접 변경하지 않습니다. 대신, 이전 상태를 복사하여 필요한 부분만 변경한 새로운 상태 객체를 생성하여 반환합니다. 이는 상태 변화를 추적하고, UI를 효율적으로 업데이트하는 데 필수적인 원칙입니다. 마치 원본 문서를 보존하고 수정된 새 복사본을 만드는 것과 같습니다.

Reducer, 어떻게 사용해야 할까? (코드 예시와 함께)

이해를 돕기 위해 간단한 카운터 앱의 MVI 패턴 코드를 통해 Reducer의 사용법을 살펴보겠습니다.

1. UiState 정의 (화면 상태):


interface UiState

data class MainState(
    val data: Int = 0
) : UiState {
    companion object {
        fun init() = MainState()
    }
}
    

2. UiEvent 정의 (사용자 액션):


interface UiEvent

sealed class MainEvent : UiEvent {
    object Increase : MainEvent()
    object Decrease : MainEvent()
}
    

3. Reducer 추상 클래스:


abstract class Reducer<S : UiState, E : UiEvent>(initialState: S) {
    private val _uiState = MutableStateFlow(initialState)
    val uiState get() = _uiState.asStateFlow()

    fun sendEvent(event: E) {
        reduce(_uiState.value, event)
    }

    fun setState(newState: S) {
        _uiState.value = newState
    }

    abstract fun reduce(oldState: S, event: E)
}
    

4. 구체적인 Reducer 구현 (MainReducer):


class MainReducer(state: MainState) : Reducer<MainState, MainEvent>(state) {
    override fun reduce(oldState: MainState, event: MainEvent) {
        when (event) {
            is MainEvent.Increase -> {
                setState(oldState.copy(data = oldState.data + 1))
            }
            is MainEvent.Decrease -> {
                setState(oldState.copy(data =  oldState.data - 1))
            }
        }
    }
}
    
  • MainReducerReducer를 상속받아 MainStateMainEvent를 처리합니다.
  • reduce 함수를 오버라이드하여 발생한 MainEvent에 따라 새로운 MainState를 생성합니다.
  • oldState.copy(...)를 사용하여 불변성을 유지하면서 원하는 데이터만 변경된 새로운 상태 객체를 만듭니다.
  • setState(newState) 함수를 통해 내부 _uiState를 업데이트합니다.

5. ViewModel에서 Reducer 활용:


class MainViewModel : ViewModel() {
    private val reducer = MainReducer(MainState.init())
    val uiState get() = reducer.uiState

    private fun sendEvent(event: MainEvent) {
        reducer.sendEvent(event)
    }

    fun increase(){
        sendEvent(MainEvent.Increase)
    }

    fun decrease(){
        sendEvent(MainEvent.Decrease)
    }
}
    
  • MainViewModelMainReducer의 인스턴스를 생성하여 상태 관리 로직을 위임합니다.
  • uiStateReducer가 관리하는 현재 상태를 StateFlow로 노출하여 View에서 관찰할 수 있도록 합니다.
  • sendEvent(event) 함수는 ViewModel에서 발생한 이벤트를 Reducer로 전달하여 상태 업데이트를 트리거합니다.
  • increase()decrease() 함수는 View의 사용자 액션에 따라 해당하는 MainEvent를 생성하여 sendEvent()로 전달합니다.

Reducer, 사용하면 뭐가 좋을까?

  • 예측 가능성 향상: 상태 변화 로직이 한 곳에 집중되어 있어 앱의 상태 변화를 예측하고 이해하기 쉬워집니다.
  • 테스트 용이성 증대: 순수 함수이기 때문에 독립적으로 쉽게 테스트할 수 있습니다.
  • 유지보수성 향상: 상태 관리 로직이 명확하게 분리되어 코드의 유지보수가 용이해집니다.
  • 협업 용이성 증대: 상태 변화의 흐름이 명확하여 여러 개발자가 함께 작업하기 수월합니다.
  • 디버깅 용이성 향상: 상태 변화의 과정을 추적하기 쉬워 디버깅이 용이합니다.

마무리하며

Reducer는 안드로이드 MVI 패턴에서 상태 관리를 위한 핵심적인 역할을 수행합니다. 순수 함수로서의 특징과 불변성 유지라는 원칙을 통해 더욱 안정적이고 예측 가능한 앱 개발을 가능하게 합니다. MVI 패턴을 도입하신다면, Reducer의 역할과 기능을 정확히 이해하고 활용하는 것이 중요합니다.

오늘 설명드린 내용이 MVI 패턴과 Reducer에 대한 이해를 높이는 데 도움이 되었기를 바랍니다. 궁금한 점이 있다면 언제든지 댓글로 질문해주세요! 😊