개요
Android Jetpack Compose에서는 UI 업데이트의 기준으로 State
라는 개념을 사용합니다. 얼핏보면 React
와 Flutter
같은 SPA
방식의 프레임워크에서 사용하는 개념과 비슷해보이지만 분명히 안드로이드에서만의 State 개념이 존재합니다. 이에 대해 알아보고, Jetpack Compose를 사용할 때 state를 통해서 MVVM
아키텍쳐와 AAC
의 ViewModel
을 사용하여 단방향 데이터 플로우 패턴을 어떻게 적용하면 좋을지도 알아봅시다.
State로 UI 업데이트
Jetpack Compose에서는 state
를 변경함으로써 UI
를 업데이트합니다. State
의 선언 방법은 다음과 같이 3가지가 있습니다.
1 |
|
첫 번째 방법과 두 번째 방법의 차이는 LiveData
와 유사합니다. State
또한 LiveData
와 동일하게 항상 value
를 가지며, 첫 번째 방법은 State<T>
객체의 value
로써 접근해야 하지만 두 번째의 경우 값 그 자체로 생각할 수 있습니다. 대신 두 번째의 경우 state를 변경할 경우 객체 자체에 접근하므로 val
이 아닌 var
을 사용해야 변경이 가능합니다. 반면 세 번째의 경우 React와 같은 웹 프레임워크에서 자주 등장하는 방식입니다. State
와 그것을 초기화하는 함수를 따로 선언하는 방식입니다. 구글 안드로이드 Github 레포지토리를 참고해본 결과 두 번째 방법을 가장 선호하는 것 같습니다.
위에서 remember
키워드의 의미를 알아보기 전에, 이 state
를 이용해서 다음과 같은 UI를 작성한다고 가정해봅시다.
위와 같이 Show more 버튼을 눌러 리스트 아이템 영역을 확장하려고 합니다. 이때 Compose에서는 하나의 state
를 만들고 이에 따라 하단 padding
값을 다르게 해준 뒤, 버튼을 통해 state
를 변경하는 식으로 구현할 수 있습니다. 코드로 보면 다음과 같습니다.
1 |
|
이와 같은 동작이 가능한 이유는, 하나의 Composable
에서 state
값이 변경되면 해당 요소가 다시 그려지기 때문입니다. 이렇게 컴포저블이 재생성되는 과정을 recomposition
이라고 합니다. 그런데 요소가 재생성되면 위 코드에서 expanded
상태도 계속 false
값으로 초기화되지 않을까요? 이러한 state
의 초기화를 막아주는 요소가 바로 remember
키워드입니다. 정리하면 하나의 컴포넌트에서 state
변화가 일어나면 해당 컴포넌트는 리컴포지션을 거치고, 이때 remember
키워드를 통해 state
를 기억함으로써 성공적으로 UI를 업데이트합니다.
따라서 위와 같이 Jetpack Compose에서는 state
를 통해 직관적인 코드로 강력한 UI를 손쉽게 작성할 수 있습니다.
Stateless를 위한 state hoisting
Jetpack Compose의 컴포저블은 크게 stateful
과 stateless
로 나뉠 수 있습니다. Flutter
에서 사용하는 용어와 동일한 개념인데, 말그대로 stateful
은 state
를 갖으며 이를 직접 변경할 수 있는 컴포저블이고 stateless
는 그렇지 않은 컴포저블을 말합니다. 이때 stateless
컴포저블은 stateful
에 비해 다음과 같은 장점을 갖습니다.
- 테스트하기 쉽다
- 버그가 적다
- 재사용성이 높다
그 이유는 의존성 주입을 사용하는 이유와 비슷합니다. Stateless
컴포저블에서 상태를 선언하는 것이 아닌 주입하는 방식으로 구현할 수 있기 때문입니다. 따라서 개발자는 최대한 stateful
컴포저블을 줄이고 이들을 stateless
로 변경하는 것이 이상적입니다. 이러한 작업은 하위의 컴포저블에 선언된 state
들을 이들의 공통 조상인 상위 컴포저블로 옮기는 방식으로 수행할 수 있습니다. 이를 state hoisting이라고 합니다. 한글로 직역하면 상태 끌어올리기 정도가 됩니다.
예를 들어, 다음과 같이 버튼을 통해 특정 컴포넌트를 숨기고 표시하는 예제를 생각해봅시다.
1 |
|
위와 같이 MyCardView
나 MyButton
에 state
를 선언할 필요 없이, 적절한 상단 컴포저블에 state
를 선언한 뒤 상태의 값이나 상태를 변경하는 함수만 전달해주면 하위 컴포저블은 모두 stateless
로 유지한 채 동일한 UI를 구현할 수 있습니다.
이와 더불어 state hoisting
에는 다음과 같은 부가적인 장점이 있습니다.
- 상태를 한 곳에서만 관리함으로써 버그 방지에 도움이 됩니다.
- Hoisting한
state
를 여러 컴포저블과 공유할 수 있습니다.
추가적으로, State
를 어느 수준의 컴포저블까지 끌어올려야 할지 쉽게 파악할 수 있는 세 가지 규칙이 있습니다.
State
는 적어도 이를 사용하는 모든 컴포저블의 가장 낮은 공통 상위 요소로 끌어올려야 합니다(읽기).State
는 최소한 이를 변경할 수 있는 컴포저블 중 가장 상위의 컴포저블로 끌어올려야 합니다(쓰기).- 동일한 이벤트에 대한 응답으로 두
state
가 변경되는 경우 두state
를 같이 통일하여 끌어올려야 합니다.
생명주기 변경에도 state 유지하기
remember
키워드를 통해 리컴포지션 시에도 state
를 유지할 수 있다고 했습니다. 그런데 remember
키워드는 컴포지션이 유지되는 동안에만 작동하기 때문에, 컴포저블이 그려지는 밑바닥의 activity
나 fragment
의 수명주기가 변경되어 버리면 state
또한 초기화되어 버립니다. 따라서 기기를 회전하거나 다크 모드로 전환 시에는 state
가 초기화되어 화면이 바뀌어버릴 수 있습니다. 이를 해결하는 것이 바로 rememberSaveable
키워드입니다. 사용법은 remember
키워드와 동일합니다.
1 |
|
ViewModel에서 state 사용하기
이번에는 Jetpack Compose를 사용하며 state를 통해 어떻게 MVVM
아키텍쳐와 단방향 데이터 플로우 패턴을 적용할지 알아봅시다.
기존의 View
방식을 사용할 때에는 ViewModel
에 LiveData
와 같은 관찰 가능한 데이터 홀더 타입의 객체를 사용했습니다. 그런데 Compose에서는 state
변화 시에 해당 상태와 연결된 컴포저블들이 리컴포지션을 거치기 때문에 ViewModel
에서 state
와 이를 업데이트하는 함수를 직접 선언하고, 컴포저블에 이들을 내려주면 됩니다. 코드로 보면 다음과 같습니다.
1 |
|
여기서 눈여겨볼 것은 TodoScreen
에 바로 ViewModel
을 주입하지 않고 TodoActivityScreen
이라는 하나의 컴포저블을 더 거쳐서 각각의 요소를 따로 전달한 점입니다. 만약 실제 UI를 그리는 TodoScreen
에 직접 ViewModel
을 전달하면, Android Studio의 Preview
기능 사용이 어려워지고 테스트 가능성이 떨어집니다. 따라서 위와 같이 UI를 직접 그리는 컴포저블에는 ViewModel
을 직접 주입하지 않고 state holder를 담당하는 컴포저블을 한 단계 거치도록 하는 것이 좋습니다.
하지만 ViewModel
에서 state
가 아니라 꼭 LiveData
나 Flow
및 StateFlow
를 사용해야 하는 경우가 있습니다. 이때는 컴포저블에서 이들을 state
로 변환하여 사용하면 됩니다. 다음과 같은 메서드를 통해 변환하여 컴포저블에서 사용할 수 있습니다.
1 |
|
마치며
Android Jetpack Compose에서 State를 이용한 기본적인 UI 업데이트 방법과, 아키텍쳐 관점에서 이를 어떻게 사용해야 MVVM 및 단방향 데이터 플로우 패턴을 적용할 수 있는지 알아보았습니다. 직관적인 코드로 빠르게 강력한 UI를 작성할 수 있는 Compose 이지만, State에 대하여 충분히 이해하고 리컴포지션을 고려하여 작성하는 것과 그렇지 않은 코드에는 분명히 성능적인 차이가 있다는 것을 알 수 있었습니다. 또한 기존의 View
방식보다 단방향 데이터 플로우 패턴을 훨씬 깔끔하고 직관적으로 적용할 수 있다는 것도 Compose의 또다른 매력이자 장점인 것 같습니다.