컴포즈는 Composition을 통해서 @Composable function들이 node로 변환이 되고, 트리에 등록되는 과정을 통해서 UI를 그려나가게 됩니다.
이 과정을 위해서 Compose에서는 GapBuffer라는 자료구조를 사용합니다.
GapBuffer
Gap buffer는 현재 편집중인 텍스트를 효율적으로 편집하고 저장하는데 주로 사용되는 자료구조입니다.
Compose에서는 Gap Buffer를 사용한 구조를 Slot Table이라고 부르고 있습니다.
배열과 비슷하지만 여러 변경 사항을 처리하기 위해서 cursor 와 gap이 존재합니다.
Composable 계층을 실행함에 따라 이 데이터 구조를 호출할 수 있고, 항목을 입력할 수 있습니다.
커서가 있는 곳이 현재 계층에서 실행되고 있는 지점입니다.
계층을 실행하는 것이 끝났다고 가정했을 때, 위와 같이 아이템이 삽입되었다고 합시다.
이후에 어느 시점에서 recompose하게 되었다면 어떻게 될까요?
커서를 다시 맨위로 리셋하고, 다시 실행을 시작합니다.
이때 커서 지점에 해당하는 데이터를 볼 수 있는데, 아무것도 하지 않고 넘어갈 수도 있고 값을 바꾸거나 UI구조를 바꿀 수도 있습니다.
새로운 UI 구조를 위해서 삽입을 원한다면, Gap을 현재 위치로 이동시킵니다. 이후 이제 그 지점에 새로운 아이템들을 삽입할 수 있습니다.
Gap Buffer의 중요한 특징은 Gap을 움직이는 것을 제외하고, 데이터를 얻기,움직이기, 삽입, 삭제 등을 상수시간에 가능하다는 점입니다.
Compose에서 이 데이터 구조를 선택하는 이유는 평균적으로 UI 구조가 많이 바뀌지 않을 것이라는 것이 배팅을 하고 있기 때문에 사용한다고 합니다.
-> UI 구조가 많이 바뀌지 않는다면 Gap을 움직일 이유가 없으니까, 나머지 데이터의 처리는 상수시간만 걸리게 되니 엄청 효율적으로 트리를 구현할 수 있을 것입니다.
즉 동적 UI가 있는 경우가 값 측면에서는 변경이 되지만 구조는 거의 자주 변경되지 않습니다.
다음으로 컴포즈가 이런 GapBuffer에 등록되는 과정을 보겠습니다.
//1
@Composable
fun Counter(){
val count = +state{ 0 }
Button(
text="Count: ${count.value}"
onPress={count.value += 1}
)
}
-> decompose
//2
fun Counter($composer : Composer, $key: Int){
$composer.start($key)
val count = +state{$composer, 123){0}
Button($composer, 456,
text="Count: ${count.value}"
onPress={count.value += 1}
)
$composer.end()
}
Compose Compiler는 @Composable 함수를 2번과 같이 변환하고, composer 매개 변수를 추가합니다.
Composer는 모든 하위 트리에 전달되고, Composable이 트리를 구체화하고 업데이트된 상태로 유지하는데 필요한 모든 정보를 제공하는 역할을 합니다.
Counter 컴포저블을 실행하면 start를 호출하게 됩니다. 그럼 start가 slot table에 그룹 객체를 삽입합니다.
여기서 key가 있는데 이 호출 지점이 나타내는 소스 위치에서의 해시를 나타내는 값입니다.
Button이나 각 매개변수들도 저장하게 됩니다. 작업이 끝나면 composer.end를 호출합니다.
보시면 이 데이터 구조가 전체 컴포지션으로부터 모든 객체를 보유하고 있다는 것을 알 수 있습니다.
그렇다면 복잡한 컴포저블을 구현하게 된다면, slot에 너무나 많은 그룹객체를 삽입하는 것이 아닌가?라고 생각될 수 있습니다.
하지만 실제로는 우리는 조건부로 Composable을 삽입할 수 있습니다.
@Composable fun App() {
val result = getData()
if (result == null) {
Loading(...)
} else {
Header(result)
Body(result)
}
}
fun App($composer: Composer) {
val result = getData()
if (result == null) {
$composer.start(123)
Loading(...)
$composer.end()
} else {
$composer.start(456)
Header(result)
Body(result)
$composer.end()
}
}
getData()로 인해서 result가 null이 아닐 경우, 컴파일러는 슬롯테이블에 있는 그룹이 일치하지 않는 것을 확인하고 이것은 UI구조가 변경됐음을 의미합니다.
컴파일러는 Gap을 현재 커서 위치로 이동하고, Gap을 확장하여 기존의 그룹을 지우게 됩니다.
이렇게 UI를 실행 시키면서 UI 내부에 임의의 제어 흐름을 갖고서 Slot table을 관리하며 캐시와 비슷한 데이터 구조를 호출하도록 만들어주는 개념을 Positional Memoization 이라고 부릅니다.
Positional Memoization(위치 메모이제이션)
위치 메모이제이션을 이해하기전에 함수 메모이제이션에 대해 알아야합니다.
함수 메모이제이션은 입력을 기반으로 결과를 캐시하는 함수의 기능으로, 동일한 입력에 대해 함수가 호출될 때마다 다시 계산할 필요가 없습니다.
위치 메모이제이션은 이 아이디어를 기반으로 하지만 중요한 차이점이 있습니다.
컴포저블 함수는 컴포저블 트리에서의 위치에 대한 지속적인 정보를 가지고 있습니다.
런타임은 부모 내에서 위치에 따른 고유한 ID를 제공하여 동일한 Composable 함수에 대한 호출을 구별합니다.
이 ID는 재구성 동안에도 유지되므로 컴포저블이 이전에 호출되었는지 또는 변경되었는지 여부를 확인할 수 있습니다.
fun Counter($composer : Composer, $key: Int){
$composer.start($key)
val count = +state{$composer, 123){0}
Button($composer, 456,
text="Count: ${count.value}"
onPress={count.value += 1}
)
$composer.end()
}
다시 코드를 보면 remember를 통해서 slot table에 state를 등록해줍니다.
이 컴포저블이 두번째로 실행될 때 remember는 전달되는 새 값을 보고 이전 값과 비교합니다.
둘다 변경되지 않은 경우 0 초기화작업을 건너뛰고 이전 결과가 반환됩니다.
즉 Positional Memoization은 위치 ID를 통한 Slot table의 접근과 새로운 값과 이전값의 비교를 통해서 변경되지 않았다면 기존의 값을 반환해주는 동작과정을 말합니다.
마무리
이번 글에서는 컴포즈에서 사용하는 Gap Buffer를 주제로 작성을 해보았습니다.
처음에는 remember 내부 코드를 보면서 이에 대해 글을 작성하려고 하였으나, 값을 업데이트나 내부적으로 SlotTable을 사용하고 있기 때문에 이부분에 대해서 어느정도 알아야 remember에 대해서 자세히 작성할 수 있을 것이라 생각했습니다.
다음에는 조금 더 자세한 내용으로 찾아올께용.ㅠㅠ
출처
https://tourspace.tistory.com/430
https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd
https://www.youtube.com/watch?v=Q9MtlmmN4Q0&t=1639s
https://leanpub.com/composeinternals/
https://sungbin.land/jetpack-composes-data-storage-system-slot-table-change-list-82e92d274c32
https://jisungbin.medium.com/gap-buffer-%EA%B0%84%EB%8B%A8%ED%9E%88-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-e1ed40649af9
'Compose' 카테고리의 다른 글
Compose와 함께 Motion Layout을 사용하여 애니메이션 구현하기 (0) | 2023.05.24 |
---|---|
rememberUpdatedState (0) | 2023.02.15 |
SwipeToDismiss Jetpack Compose로 구현하기 (0) | 2023.02.02 |
Jetpack Compose의 최적화 (0) | 2022.10.13 |
Blur in Jetpack Compose (0) | 2022.07.20 |