읽기 전
- 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
- 개인적으로 실습하면서 배운 점을 정리한 글입니다,
Jetpack Compose의 State 코드랩과 Jetpack Compose State 공식문서의 내용을 정리합니다. Stateful과 Stateless 컴포저블의 차이를 확인하고 상태를 상단으로 끌어올리는 Hoisting
에 대해 알아봅니다.
관련 자료
Android | Jetpack Compose Remember State에서 이어지는 포스트입니다.
Hoisting(호이스팅)이란?
주로 제시하는 설명은 "변수의 선언과 초기화를 분리한 후 선언 부분만 코드의 최상단으로 옮기는 행위"라고 한다. 왜 이러한 과정을 수행하는지 알아보자.
상태 호이스팅
remember
를 사용하여 객체를 저장하는 컴포저블은 내부 상태를 포함하고 있어 Stateful
하다고 볼 수 있다. 이는 호출하는 곳에서 상태를 제어할 필요가 없고 상태를 직접 관리하지 않아도 내부적으로 상태를 사용할 수 있는 상황에서 유용하다. 그러나, 내부 상태를 갖는 컴포저블은 재사용 가능성이 적고 테스트하기가 더 어렵다. 그래서 상태를 바깥 호출하는 위치에서 선언함으로써 호출된 컴포저블 내부에서는 주입된 상태를 다루는 방식 즉, Stateless
한 컴포저블을 사용할 수 있다.
Compose에서의 상태 호이스팅은 컴포저블을 Stateful
에서 Stateless
로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴을 의미한다. 일반적으로 상태 관련된 변수를 다음 두 개의 매개변수로 바꿈으로써 이루어진다.
value: T
- 컴포저블이 다룰 상태 값onValueChange: (T) -> Unit
- 상태의 값을 변경하도록 요청하는 이벤트이며 T는 컴포저블에 제안할 새로운 값이다.
이렇듯 상태가 내려가고(UI에 표시할 상태가 컴포저블로 내려감) 이벤트가 올라가는(하위 컴포저블에서 발생된 이벤트가 상위 컴포저블로 올라감) 패턴을 단방향 데이터 흐름(UDF)라고 하고 상태 호이스팅은 해당 아키텍쳐를 Compose에서 구현하는 방법이라고 설명한다.
상태를 호이스팅함으로써 갖는 속성으론 다음이 있다.
- 단일 소스 저장소 : 상태를 복제하는 대신 옮겼기에 소스 저장소가 하나만 존재하여 버그 방지에 도움
- 공유 가능 : 호이스팅한 상태를 여러 컴포저블과 공유할 수 있음
- 캡슐화됨 :
Stateful
한 컴포저블만 상태를 수정할 수 있음 - 가로채기 가능 :
Stateless
컴포저블의 호출자는 상태를 변경하기 전 이벤트를 무시할 것인지 반영할 것인지 결정가능 - 분리됨 :
Stateless
컴포저블에서 다루는 상태를 어디에나 저장할 수 있게 된다. (ex. ViewModel)
Stateful
과 Stateless
비교
컴포저블 함수에서 모든 상태를 외부로 추출하여 만들어진 컴포저블 함수는 Stateless
하다고 볼 수 있다.
Stateless
컴포저블 : 상태를 소유하지 않는 컴포저블로 새로운 상태를 갖거나 정의하거나 수정하지 않음Statefull
컴포저블 : 시간이 지남에 따라 변하는 상태를 갖는 컴포저블
모든 세부 컴포저블이 Stateless
할 수는 없지만 가급적 적게 상태를 갖게끔 설계하여 상태를 호이스팅함이 바람직하다고 한다.
기존 WaterCounter
컴포저블을 리팩토링하여 StatefulCounter
와 StatelessCounter
로 구분해보자.
StatelessCounter
컴포저블은 count
를 표시하고 count
를 늘릴 때의 함수를 호출한다. 앞서 작성하였듯이 value
와 onValueChange
를 정의해야 하므로 상태 변수인 count
와 그 값을 증가시키는 onIncrement
람다 함수를 매개변수로 받도록 선언한다.
WaterCounter.kt - StatelessCounter
@Composable fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) { Column(modifier = modifier.padding(16.dp)) { if (count > 0) { Text("You've had $count glasses.") } Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) { Text("Add one") } } }
StatefulCounter
컴포저블은 상태를 소유하므로 count
의 상태를 갖고 StatelessCounter
컴포저블을 호출할 때 해당 상태를 매개변수로 입력하며 수정합니다.
WaterCounter.kt - StatefulCounter
@Composable fun StatefulCounter(modifier: Modifier = Modifier) { var count by rememberSaveable { mutableStateOf(0) } StatelessCounter(count, { count++ }, modifier) }
이렇게 기존 WaterCounter
를 두 개의 컴포저블인 StatefulCounter
와 StatelessCounter
로 분리함으로써 상태를 성공적으로 호이스팅하였다. 이제 WaterCounter
를 호출하던 코드를 수정하면 되겠다.
WellnessScreen.kt
@Composable fun WellnessScreen(modifier: Modifier = Modifier) { StatefulCounter(modifier) }
호이스팅 시 주의할 점
- 상태는 적어도 해당 상태를 사용하는 모든 커포저블의 가장 낮은 공통 상위 Element로 끌어올려야 한다. (읽기)
- 상태는 최소한 변경될 수 있는 가장 높은 수준으로 끌어올려야 한다. (쓰기)
- 두 상태가 동일한 이벤트에 대한 응답으로 변경되는 경우 두 상태는 동일한 수준으로 끌어올려야 한다.
위 규칙에 따라 상태를 적절한 수준으로 끌어올리지 않으면 단방향 데이터 흐름을 구현하지 못할 수 있다.
현재 상태를 호이스팅하느라 두 개의 컴포저블로 분리하였는데, 이 경우 몇가지 이점이 있다.
Stateless
컴포저블 재사용
만약 마신 물과 함께 주스의 잔 개수도 함께 계산하려면 WaterCounter
와 유사한 JuiceCounter
를 생성하지 않고도 각각 waterCount
와 JuiceCount
변수를 기억하고 StatlessCounter
컴포저블 함수를 사용하여 두 개의 독립 상태를 출력할 수 있다.
WaterCounter.kt - StatefulCounter
@Composable fun StatefulCounter() { var waterCount by remember { mutableStateOf(0) } var juiceCount by remember { mutableStateOf(0) } StatelessCounter(waterCount, { waterCount++ }) StatelessCounter(juiceCount, { juiceCount++ }) }
앱을 실행하면 컴포지션 구조는 아래 그림과 같이 waterCount
를 표시하는 StatelessCounter
와 juiceCount
를 표시하는 StatelessCounter
두 개의 컴포저블이 있다.
만약 juiceCount
가 수정되면 StatefulCounter
가 재구성되며 리컴포지션 중에 Compose가 juiceCount
만 변경됨을 인지하고 해당 상태를 사용하는 컴포저블 함수에 대해서만 리컴포지션을 트리거한다.
사용자가 juiceCount
를 늘리면 StatefulCounter
가 재구성되고 juiceCount
를 읽는 StatelessCounter
도 재구성된다. 그러나, waterCount
를 읽는 StatelessCounter
는 재구성되지 않는다.
Stateful
컴포저블 함수는 여러 컴포저블에 동일한 상태를 제공할 수 있음
@Composable fun StatefulCounter() { var count by remember { mutableStateOf(0) } StatelessCounter(count, { count++ }) AnotherStatelessMethod(count, { count *= 2 }) }
임시로 두 컴포저블이 하나의 상태롤 바라보게끔 작성하였다. 이 경우 count
변수가 StatelessCounter
나 AnotherStatelessMethod
함수에 의해 변경되면 두 컴포저블이 하나의 공통 상태를 관찰하고 있기 때문에 모든 항목이 재구성된다.
호이스팅된 상태는 공유할 수 있으므로 리컴포지션을 방지하고 재사용성을 높이기 위해 컴포저블에 필요한 상태만 전달함이 좋다. 리컴포지션이 자주 발생하면 앱의 성능에 안좋은 영향이 발생할 수 있기 때문에 필요한 경우에만 매개변수로 상태를 입력해야 한다.
참고자료
'Android' 카테고리의 다른 글
Android | Jetpack Compose Observable MutableList (0) | 2022.12.19 |
---|---|
Android | Jetpack Compose State 복원, 목록 사용 (0) | 2022.12.13 |
Android | Jetpack Compose Remember State (0) | 2022.12.07 |
Android | Jetpack Compose State & Event (0) | 2022.12.05 |
Android | Activity의 개념과 생명주기(Life Cycle) (0) | 2022.09.22 |