이번 시간에는 State Hoisting 에 대해 알아보려고 한다.
다만, 그 전에 필수적으로 알아야하는 두 가지 단어를 먼저 알아보자.
내부에 상태를 가지고 있는 Composable. 바깥에서는 내부 상황을 알 수 없고, 변경도 불가능하여 테스트나 재사용이 어렵다.
내부에 상태를 가지지 않는, 무상태 Composable. 상위 Composable 로부터 상태를 받아오고 이벤트 처리 또한 Lambda 를 통해 상위로 넘긴다.
말 그대로 상태를 공통 부모까지로 끌어올리는 기법으로, Stateful Composable 을 stateless Composable 로 바꿔주어 Test Code 작성 및 코드 재사용성에 도움을 주는 기법
한 가지 상황을 들어보자.
다음과 같이 ParentA, ParentB 가 Child() 를 필요로 한다고 쳐보자.
@Composable
fun ParentA(){
Child()
}
@Composable
fun ParentB(){
Child()
}
@Composable
fun Child(){
var count by remember { mutableStateOf(0) }
Text(count.toString())
Button({ count++ }){}
}
이 코드는 ParentA 와 ParentB 가 모두 Child 를 필요로 한다.
어느 순간, ParentB 에서 count 를 50으로 시작해야한다고 생각해보자.
그러면 Child() 내부의 count 를 변경해줘야하는데, 이러면 ParentA() 에서 문제가 다시 발생하게 된다.
현재는 이러한 코드가 2개 뿐이지만, 수십 수백개라면 상황이 꽤나 난처해질 것이다.
또한 count 가 100일때의 테스트 같은 것도 진행하기 위해서는 원본인 Child 를 수정해줘야하는데, 이 경우도 Side Effect 가 터진다.
이제 State Hoisting 을 사용해보자.
Child 내부에 존재하던 count 를 두 부모로 끌어올려주고, Click Event 를 처리해줄 것이다.
@Composable
fun ParentA(){
var count by remember { mutableIntStateOf(0) }
Child(count){ count++ }
}
@Composable
fun ParentB(){
var count by remember { mutableIntStateOf(50) }
Child(count){ count++ }
}
@Composable
fun Child(count: Int, onClick: () -> Unit){
Text(count.toString())
Button(onClick = onClick){}
}
이처럼 구현하면 추후 Child 를 필요로 하는 새로운 Composable 이 발생해도 인자를 넘겨주고 이벤트를 처리할 수 있기 때문에 재사용성이 증가하고, 테스트도 가능해진다 !
무조건 써야하는 것은 아니다.
외부에서 내부의 상황을 알 필요가 없거나, 재사용성이 극히 제한적인 경우라면 오히려 State Hoisting 을 사용함으로 인해 코드가 복잡해지기 때문이다.
StateHoisting 을 사용하게 되면 공통부모가 상태를 가지게 되는 만큼, 해당 리컴포지션 범위가 넓어질 수 있겠으나 실질적으로 이것에 의한 메모리 낭비는 거의 발생하지 않는다.
다른 Composable 들의 상태가 변경되지 않으면 Compose Runtime 은 이를 Skipping 해버리기 때문이다.