단방향 데이터 흐름을 만들기 위해 컴포즈를 ViewModel과 함께 어떻게 사용해야 하는지 알아보았다. 지금부터 컴포즈가 내부적으로 state와 어떻게 상호작용하는지 알아보자.
지난 섹션에서는 컴포저블을 다시 호출하여 컴포즈가 화면을 업데이트 하는것을 보았다. 이러한 처리를 recomposition(재구성)이라 한다. TodoScreen을 다시 호출하여 동적인 목록을 보여줄 수 있었다.
이 섹션과 다음 섹션에서는 컴포저블을 stateful하게 만들는 방법에 대해서 살펴본다.
stateful 컴포저블은 자기 자신이 시간에 따라 변경될 수 있는 state 일부를 가지고 있는 컴포저블을 말한다.
이 섹션에서 컴포저블 함수에 어떤식으로 state를 기억할 수 있도록 하는지 살펴본다.
Dishelveled Design
이 섹션에서는, 팀의 새로운 디자이너가 나에게 위와 같은 이미지가 최신 디자인 트렌드라며, 단정하지 못한 디자인(disheveled design)을 줬다.
단정하지 못한 디자인의 핵심 원칙은 좋은 디자인을 취하고, 겉으로 보이는 무작위 변경사항을 추가하여 “흥미롭게” 만드는 것을 의미한다.
이 디자인에서 각 아이콘은 0.3에서 0.9사이의 랜덤한 투명도를 갖는다.
Tip : Disheveled design은 실제 디자인 트렌드가 아니다.
컴포저블에 무작위로 추가하기
시작하기 위해, TodoScreen.kt를 열고 TodoRow 컴포저블을 찾는다. 이 컴포저블은 todo 목록의 단일 행(row)을 표현한다.
iconAlpha는 현재 버그가 있는데 나중에 수정될 예정이다. 매번 목록이 변경될 때마다 tint 색상을 변경하게 된다.
새로운 val iconAlpha를 randomTint() 값과 함께 정의한다. 이는 디자이너가 요구한 0.3 에서 0.9 사이의 float 형식이다. 그런 뒤, 아이콘의 tint를 설정하자.
//TodoScreen.kt
@Composable
fun TodoRow(todo: TodoItem, onItemClicked: (TodoItem) -> Unit, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clickable { onItemClicked(todo) }
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(todo.task)
val iconAlpha = randomTint()
Icon(
imageVector = todo.icon.imageVector,
tint = LocalContentColor.current.copy(alpha = iconAlpha),
contentDescription = stringResource(id = todo.icon.contentDescription)
)
}
}
미리보기를 다시 확인하면, 아이콘 투명도가 무작위로 설정되는 것을 확인할 수 있다.
LocalContentColor.current란 무엇인가?
LocalContentColor는 아이콘 및 서체와 같은 콘텐츠에 알맞는 색상을 제공한다. 배경을 그리는 Surface와 같은 컴포저블에 의해 변경된다.
재구성 살펴보기
앱을 다시 시작해서 새로운 disheveled design을 테스트 해보면, 투명도 색상이 매번 바뀌는 것을 알아차릴 수 있다. 디자이너가 무작위로 계속 바뀌는걸 보고, “이건 좀 너무하지 않나”라고 한다.
내부에서 무슨일이 일어나고 있는것일까? 화면상 목록의 각 행이 변경될 때마다, 재구성(recomposition)처리가 randomTint를 호출하고 있다.
재구성(Recomposition)은 컴포저블을 다시 호출하여 새로운 입력값을 받고 컴포즈 트리를 업데이트 하는 처리를 말한다. 이 경우 TodoScreen이 새로운 목록과 함께 다시 호출될 때, LazyColumn은 화면상에 모든 하위요소를 재구성한다. 이는 TodoRow를 다시 호출하고, 새롭게 무작위로 투명한 값을 생성하게 된다.
Recompositon은 데이터가 변경될 때 트리를 업데이트하기 위해 동일한 컴포저블을 다시 실행하는 프로세스다.
컴포즈는 트리를 생성한다. 안드로이드 View 시스템에서 UI 트리를 구성하는 것과 비슷해 보이지만 조금 다르다. UI 위젯 트리 대신, 컴포즈는 컴포저블 트리를 생성한다. TodoScreen을 가시화 하면 다음과 같다.
컴포즈가 구성(composition)을 처음할 때, 컴포즈는 모든 컴포저블을 호출하여 트리를 만든다. 그런 다음, 재구성할 때는 새롭게 호출되는 컴포저블과 함께 트리를 업데이트 한다.
TodoRow를 재구성 될 때마다 아이콘들이 업데이트 되는 이유는 TodoRow는 숨겨진 부작용(side-effect)을 가지고 있기 때문이다. 부작용이란 컴포저블 함수 실행 바깥에 어떤 변경사항이 드러난다는 점이다.
Random.nextFloat()을 호출하는 것은 의사-난수 생성기(pseudo-random number generator)를 사용하여 무작위 변수를 업데이트한다. 이것이 Random이 매번 요청하는 난수를 반환하는 방법이다.
부작용은 컴포저블 함수 외부에 나타나는 어떤 변경사항을 말한다.
컴포저블을 재구성하는 것은 부작용이 없어야만 한다.
예를 들어, ViewModel내의 state를 업데이트 하는 것, Random.next()를 호출하는 것, 또는 데이터베이스를 쓰는 것들을 전부 부작용이라고 할 수 있다.
컴포저블 함수에 메모리 도입하기
우리는 TodoRow가 재구성될 때마다 아이콘 투명도가 변경되는 것을 원치 않는다. 그렇게 하려면, 우리가 지난 구성에서 사용한 색상 정보를 기억해야한다. 컴포즈는 컴포지션 트리에서 값들을 저장할 수 있도록 하기 때문에, 우리는 TodoRow를 업데이트하고 iconAlpha값을 컴포지션 트리에 저장 할 수 있다.
remember는 컴포저블 함수에 메모리를 제공한다.
remember에 의해 계산된 값은 컴포지션 트리 내에 저장되고, remember에 대한 키들이 변경되면 다시 재계산 된다.
private val 프로퍼티가 하는 방식과 동일하게, remember는 함수에 단일 객체를 위해 주어진 저장소라고 생각하면 된다.
TodoRow를 수정하고, randomTint를 remember로 다음과 같이 감싼다.
// TodoScreen.kt
val iconAlpha: Float = remember(todo.id) { randomTint() }
Icon(
imageVector = todo.icon.imageVector,
tint = LocalContentColor.current.copy(alpha = iconAlpha),
contentDescription = stringResource(id = todo.icon.contentDescription)
)
TodoRow 에 대한 새로운 컴포즈 트리를 보면, iconAlpha가 컴포즈 트리에 추가 된 것을 확인할 수 있다.
앱을 다시 시작하면, 투명도 색상이 목록이 변경될 때마다 더 이상 변경되지 않는 것을 확인할 수 있다. 대신에, 재구성이 발생할 때 이전에 저장된 값이 remember에 의해 반환된다.
remember 호출하는 쪽을 자세히 보면, todo.id를 키(key)로 전달하는 것을 볼 수 있다.
remember(todo.id) { randomTint() }
rememeber 호출은 크게 두가지 부분으로 볼 수 있다.
- 키 인자(key arguments) – remember가 사용하는 ‘키’는 괄호 안에 전달되는 부분으로, 여기서 우리는 todo.id를 키로써 전달하고 있다.
- 계산(calculation) – 저장 될 새로운 값을 계산하는 람다는 후행 람다로 전달된다. 여기서 우리는 무작위 값을 randomTint()와 함께 계산한다.
처음 이것을 구성할 때, remember는 항상 randomTint를 호출하고, 다음 재구성을 위해 이 결과를 기억한다. 전달 된 todo.id 또한 추적하게 된다. 그런 뒤, 재구성할 때 randomTint 호출하는 것을 건너뛰고, TodoRow에 새로운 todo.id가 전달되지 않는 한 이미 저장하고 있는 값을 반환한다.
구성(컴포지션)내에 저장된(기억된) 값들은 컴포지션 트리로부터 컴포저블이 제거되자 마자 함께 제거된다(잊혀진다)
트리에서 컴포저블을 호출할 때 그것들은 재-초기화를 한다. LazyColumn내에서 가장 상단의 아이템들을 지우는 것으로 이 문제를 재현시켜볼 수 있다.
멱등원(idempotent)은 컴포저블 함수에다가 같은 입력(매개변수)을 넣으면, 그에 대해서 항상 같은 결과(UI)를 보여주는 것을 의미 한다.. 그리고 컴포즈는 재구성시에도 부작용이 발생하지 않아야 한다.
컴포저블 함수는 반드시 재구성을 지원하는 멱등원이여야 한다.
컴포저블의 재구성은 반드시 멱등원이여야 한다. remember로 randomTint를 감싸면, todo 항목이 변경되지 않는 한 재구성 시 random 호출을 건너뛴다. 그러한 결과로, TodoRow는 부작용이 없으며, 동일한 입력으로 재구성할 때마다 같은 결과를 나타내고, 이는 멱등원이다.
저장된 값을 제어할 수 있게 만들기
지금 앱을 실행하면, 각 아이콘이 랜덤한 투명도 색상을 갖는것을 볼 수 있다. 이제 디자이너가 dishelved 디자인 원칙을 따른 이 결과물과 함께 기뻐하며, 앱에 디자인 적용을 승인한다.
하지만 점검하기 전 사소한 코드 변경이 여기에 하나 있다. 지금 당장은 TodoRow를 호출하는 쪽이 아이콘에 대한 투명도 색상을 지정할 방법이 없다. 이게 필요한 많은 이유가 있는데, 예를 들어 제품 담당 부사장이 이 화면을 보고 앱을 출시하기 직전에 흐트러진 부분을 제거하기 위해 핫픽스를 요구할 수 있다.
호출자가 이 값을 제어할 수 있도록 하려면, 새 iconAlpha 매개변수의 기본 인자로 remember 호출을 이동하기만 하면 된다.
@Composable
fun TodoRow(
todo: TodoItem,
onItemClicked: (TodoItem) -> Unit,
modifier: Modifier = Modifier,
iconAlpha: Float = remember(todo.id) { randomTint() }
) {
Row(
modifier = modifier
.clickable { onItemClicked(todo) }
.padding(horizontal = 16.dp)
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(todo.task)
Icon(
imageVector = todo.icon.imageVector,
tint = LocalContentColor.current.copy(alpha = iconAlpha),
contentDescription = stringResource(id = todo.icon.contentDescription)
)
}
}
이제 호출자는 기본값으로 TodoRow가 randomTint를 계산하는 동작을 갖지만, 원하는 투명도를 지정할 수 있고 alphaTint를 제어할 수 있기 때문에 이 컴포저블을 재사용할 수 있다. 다른 화면에서 디자이너는 모든 아이콘을 0.7 투명도로 표시할 수도 있다.
컴포저블에 메모리를 추가할 때, 항상 자신에게 묻도록 하자. “어떤 호출자(caller)가 이를 제어하고 싶을까?”
만약 대답이 yes면, 매개변수를 추가하고,
만약 대답이 no면, 지역 변수로 이를 유지하자.
또한 remember 사용할 때 아주 미묘한 버그가 있다. “Add random todo”를 반복적으로 클릭한 다음 스크롤하여 화면에서 몇 개 스크롤할 수 있도록, todo 행을 충분히 추가해 보자. 스크롤 하고 다시 원래의 위치로 스크롤할 때마다 투명도가 변경되는 것을 알 수 있다.
remember는 컴포지션에 값을 저장하고, remember를 호출 한 컴포저블이 제거되면 해당 값을 잊는다.
이런 부분은 LazyColumn과 같이 하위요소들을 추가하고 제거하는 컴포저블 내부에 중요한 것을 저장하면 안된다는 것을 의미한다.
예를 들어, 짧은 애니메이션을 위한 애니메이션 상태는 LazyColumn의 하위요소에서 remember랄 사용하여 저장하기에 안전하다. 하지만 Todo 작업의 완료 상태를 remember를 사용하여 LazyColumn내에 저장했다면, 스크롤시에 완료 상태가 날아가 버린다.
다음 섹션에서는 이러한 버그들을 고칠 수 있는 도구를 제공하는 state 및 state hoisting에 대해서 살펴보자.
0개의 댓글