토이 프로젝트에서 Jetpack Compose를 다루기 시작하면서 Compose에 대해 공부하는 시간이 늘어나고 있다.
Compose로 개발을 하면서 느낀 건 바로 State 관리의 중요성이다. Compose는 기존의 안드로이드 View(xml)과 같은 명령형 UI가 아닌 선언형 UI다. 이미 선언된 UI에 표시되는 데이터를 변경하기 위해서는 State를 변경하여 Recomposition(재구현)을 진행해야 한다.
즉, Compose를 사용하면서 State를 다루지 않겠다는 건 화면 UI에 초기 데이터 이후 어떤 데이터 변경도 표시하지 않겠다는 이야기다.
이렇게 State 관리의 중요성을 알다보니 생각보다 꽤 머리가 복잡해졌다. 여러 이유가 있지만 그 중 하나는 가장 보편적으로 쓰이는 MVVM 패턴이 Compose에 과연 잘 맞나? 하는 의문이 들기 시작한 것이다.
내가 가장 크게 느낀 부분은 바로 생명주기의 차이이다.
Composable 구성요소들은 State가 변경되는 Recomposition이 일어나면서 재구성이 일어나면서 새로 생성된다. 하지만 ViewModel은 종속된 Activity나 Fragment가 완전히 종료될때까지 동일한 인스턴스를 호출한다.
그래서 만약 ViewModel에서 특정 Composable의 생명주기에 의존하는 값이나 메서드를 가지고 있을 경우 의도치 않은 다른 결과를 불러올 수도 있다.
실제로 안드로이드 공식 문서에서도 이 점에 대해서 주의할 것을 명시하고 있다.
class GreetingActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Column {
GreetingScreen("user1")
GreetingScreen("user2")
}
}
}
}
}
@Composable
fun GreetingScreen(
userId: String,
viewModel: GreetingViewModel = viewModel(
factory = GreetingViewModelFactory(userId)
)
) {
val messageUser by viewModel.message.observeAsState("")
Text(messageUser)
}
class GreetingViewModel(private val userId: String) : ViewModel() {
private val _message = MutableLiveData("Hi $userId")
val message: LiveData<String> = _message
}
class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return GreetingViewModel(userId) as T
}
}
위처럼 GreetingScreen이 서로 다른 userId 값을 값고 2번 호출되어도 GreetingViewModel은 GreetingActivity가 완전히 종료되기 전까지 동일한 인스턴스를 반환하기 때문에 2번의 GreetingScreen은 모두 "user1"에 대한 인사말을 출력하게 된다.
찾아보니 MVI 패턴을 사용하면 State(상태) 관리를 좀 더 쉽게 다룰 수 있다는 이야기를 들었고 Compose로 UI를 구성한 앱에서 효과적인 디자인 패턴이라는 아닐까하는 생각이 들었다.
MVI 패턴은 Model, View, Intent 크게 3가지 구성요소로 이루어져 있다.
이 패턴은 사용자의 의도(Intent)를 명확하게 표현하여 앱의 상태를 관리한다.
Model: UI에 반영될 상태를 의미한다. 데이터를 의미하는 MVP, MVVM에서의 정의와는 다르다.
View: UI 그 자체. View, Activity, Fragment, Compose 등이 될 수 있다.
Intent: MVI에서는 사용자 액션 및 시스템 이벤트에 따른 결과라고 해석할 수 있다. (안드로이드에서 흔히 다루는 그 Intent와는 다르다.)
쉽게 설명하자면, User(사용자)가 View를 클릭하거나 하는 등의 행동(Action)을 취하면 이 행동이 Intent(의도)가 되어서 Model에 전달된다. 이 Intent가 Model, 즉 상태를 업데이트한다. 그리고 이 변경된 상태가 다시 View에 반영된다.
MVI의 핵심은 단방향 흐름(Uni-directional Flow) 구조이다. 그래서 다음 그림과 같이 표현하기도 한다.
Intent는 사용자의 액션을 Model로 전달하고, Model은 이를 처리하여 새로운 상태를 생성한 후, 이 상태는 View를 통해 사용자에게 표시되기 때문이다.
이 과정에서 모든 상태 변화는 예측 가능하며, 앱의 상태는 항상 일관된 상태를 유지한다.
또한, MVI 패턴은 상태의 변화를 중앙에서 관리함으로써 디버깅과 테스트가 용이해진다.
예를 들어, 사용자의 클릭과 같은 이벤트가 Intent로 변환되어 Model에 전달되고, Model은 이를 처리하여 새로운 View 상태를 생성한다.
MVI 패턴은 MVP, MVVM과 같은 MVx 패턴에 가장 최근에 추가된 패턴이다.
MVVM과 공통점이 많지만 상태 관리 측면에서 좀 더 구조화된 방법을 갖고 있어 Compose와 좀 더 맞는 패턴이지 않을까 생각된다. 좀 더 공부해봐야겠다!