ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android/Compose] 고성능 UI 컴포넌트 만들기
    안드로이드 2024. 10. 23. 17:03

    Compose에서는 Recomposition 단계를 생략하고도 UI를 변경할 수 있는 방법이 존재합니다.

     

    이 글을 통해 Compose의 그리기 단계를 소개하려 합니다.

     

    글의 구성은 퀴즈식으로 이루어져있고 퀴즈 이후에 곧바로 답을 작성해두었습니다.

    점진적으로 읽고 생각해보는 것을 권장합니다.


    ⛳ 읽기 전에

    ❌ Stable에 대해서 설명하지 않습니다.

    ❌ Skippable에 대해서 설명하지 않습니다.

    ❌ 모든 UI를 이렇게 만들 필요는 없습니다.

     

    ⭕ 신경써서 구현할 컴포넌트에 활용합니다.

    ⭕ 애니메이션, 혹은 성능이 중요한 특수한 경우에 활용할 수 있습니다.

     

     

    ♻️ Recomposition

    최근에는 대부분의 Compose 개발자가 알고 있는 Recomposition을 간략하게 짚고 넘어가려 합니다.

     

    그림에 나온 것 처럼 Recomposition은 0회 혹은 여러번 일어날 수 있는

    Compose의 핵심 매커니즘 중 하나입니다.

     

    Recomposition은 상태가 변경되었을 때 일어난다고 알려져 있는데, 정확히 어떨 때 일어나는 것일까요?

     

    Compose 공식 문서의 일부인데요 Compose팀에서 재구성은 가능한 한 많이 건너 뛰기 위해 최선을 다한다는

    사실을 인지하고 다음 단락으로 넘어가겠습니다.

     

     

    ❓ State 퀴즈

    Q. ClickableComponent를 클릭하면 ComponentContent는 몇 번의 Recomposition이 일어날까요?

     

    이해를 돕기위해 코드를 간략하게 설명하면, ClickableComponent를 클릭했을 때

    ViewModel의 state를 변경하고 이를 ComponentContent에 State를 전달합니다.

     

    이 때  ComponentContent에서 state는 참조하지 않습니다.

     

     

     

     

     

    정답은,

    이 코드 스니펫은 클릭을 하더라도 Recomposition은 일어나지 않습니다.

     

    Q. 그렇다면 위와 비슷한 다음 코드는 몇 번의 Recomposition이 일어날까요?

     

    위 코드에서 달라진 점은 상태를 읽는다는 점입니다.

     

     

     

     

     

     

    정답은,

    클릭 1번에 1회의 Recomposition이 일어납니다.

     

    왜 이런 차이가 일어나는 것일까요?

    이유는 상태가 변화할 때 Recomposition이 일어나는 것이 아니라

    상태를 읽을 때 Recomposition이 일어나기 때문입니다.

     

    이를 언제 활용하면 좋을까요?

    아래 이미지 처럼 뎁스가 많은 UI에서 recomposition이 빈번하게 일어나는 값에 대해

    State를 넘겨 Recomposition의 범위를 좁게 만드는 데 활용할 수 있습니다.

     

    물론 모든 UI가 Skippable 상태라면 문제가 없을 수 있기도 하고 다른 해결방안도 있을 수 있겠지만요

     

     

    ❓ State 퀴즈2

    Q. 다음 코드는 어떠한 문제점을 가지고 있는 코드일까요?

    (영상에 비해 코드가 일부 간략화 되어있습니다.)

     

     

     

     

     

     

     

    정답은,

    너무 많은 Recomposition이 발생하는 것입니다.

    직전 단락에서 설명했듯 Compose에서는 상태를 읽을 때 Recomposition이 일어나고

    이 코드 스니펫에서는 animate값이 변경될 때마다 상태를 계속해서 읽고 있습니다.

     

    영상 속 애니메이션을 수행하는 과정 중에 183번의 Recomposition이 발생했고

    만약, Skip이 불가능하면서 무거운 UI가 같은 트리 내에 존재한다면 심각한 성능 저하를 유발할 것입니다.

     

     

    🤷‍♂️ 궁금증

    "애니메이션을 만들기 위해서는 이렇게밖에 할 수 없지 않나요?"

     

    라고 생각할 수 있겠지만, 이렇게 하지 않을 수 있는 방법이 존재합니다.

     

     

    ✈️ Compose의 그리기 단계

     

    Composition은 기존에 알고 있던 단계라면, Layout은 배치와 영역을 결정하고

    Drawing은 그리는 작업을 담당합니다.

    각 단계는 이전의 단계에 영향을 주지 않지만, 이후의 단계가 존재한다면 수행합니다.

     

    가령, Drawing 단계에서 내가 담당하고 있던 Layout 범위를 넘겨서 그리더라도 배치에 영향을 주지 않습니다.

     

    Compose에서는 상태를 읽는 단계를 추적하여 해당 단계를 다시 실행하는 것으로 최적화를 진행합니다.

    Composition 단계를 건너 뛸 수도 있구요.

     

     

    ❓ Offset 퀴즈

    Q. 다음 1번과 2번, 두 함수의 차이점은 무엇일까요?

     

     

    단순히 offset을 dp로 받는지, px로 받는지의 차이일까요?

    내부 구현을 들여다봅시다.

     

    내부 구현을 보면, dp로 받는 경우에는 그대로 dp로 넘기고 IntOffset을 람다로 받았던 경우에는

    람다를 그대로 전달하는 것을 볼 수 있습니다.

     

    이 차이는 어떤 결과를 불러올까요?

    이러한 사각형이 움직이는 애니메이션을 구현한다고 합시다.

     

    왼쪽이 dp로 구현한 결과이고 오른쪽이 IntOffset 람다로 받은 결과입니다.

    IntOffset으로 받는 함수의 내부 구현을 보면 layout 블럭 내부에서 함수를 실행하여 값을 읽고 있습니다.

    아까 설명했듯 상태를 읽는곳을 추적하여 해당 단계를 재실행 하므로 이 코드는 layout 단계를 추적하여

    layout 단계를 재실행합니다.

     

    이러한 이유로 Composition 단계를 생략하여 Recomposition이 일어나지 않게 됩니다.

     

     

    ❓ Animate 퀴즈

    Q. 다음 코드에서 Box를 클릭했을 때 몇 번의 Recomposition이 일어날까요?

     

    정말 확실하게 이해하기 위해 추가 퀴즈를 작성했습니다.

    animateDpAsState를 통해 선언했지만 state 변수는 참조되지 않습니다.

     

     

     

     

     

     

     

     

     

    정답은,

    클릭 시 1회 Recomposition이 일어납니다.

    왜냐하면 animateDpAsState를 선언할 때 targetValue에서 value라는 Boolean 상태를 읽고 있기 때문입니다.

     

    UI는 변경되지 않았지만 상태를 읽었기 때문에 Recomposition이 일어났습니다.

    이제는 헷갈리지 않을 것입니다.

     

     

    🤹‍♂️ Switch 예제로 최적화 해보기

    이제는 코드를 보면 바로 문제점을 찾을 수 있을 것입니다.

     

    현재 Composition 단계에서 draggableState를 두 차례나 읽고 있습니다.

    이렇게 구현하면 스위치가 드래그되거나 움직일 때 많은 양의 Recomposition이 발생합니다.

     

    설명을 위해 이 아래부터는 외부의 Box는 Track이고, 내부의 Box는 Thumb라고 작성하겠습니다.

     

    Thumb를 먼저 확인해보겠습니다.

    아까 offset 예제에서 봤던 것 처럼 offset을 람다표현식으로 바꾸는 것만으로도

    더이상 상태를 읽더라도 Recomposition이 일어나지 않게 됐습니다.

     

    조금 더 고쳐본다면, 아예 Canvas를 활용해서 Offset을 표현한다면 Layout 단계도 생략하고

    Drawing 단계만 실행되어 더욱 더 최적화 될 것입니다.

     

    다음은 Track입니다.

     

    Track도 마찬가지로 Canvas를 활용한다면 더욱 더 빠른 성능으로 개선할 수 있을 것입니다.

     

     

    🙅‍♂️🙅‍♂️🙅‍♂️ 사실 직전의 예제는 더욱 정확한 이해를 위한 함정입니다.

     

    Canvas에서 그리기는 것만이 중요한 게 아니라 state를 Drawing 단계에서 읽어야만

    Composition 단계를 건너뛸 수 있습니다.

     

    직전의 예제는 Canvas를 활용했지만 상태를 Composition 단계에서 읽었으므로 최적화되지 않았습니다.

     

    만약 이 코드의 'state.requireOffset().dp / SwitchWidth'를 외부에서 계산해서 받고 싶다면 어떻게 하면 될까요?

    Offset이 선언 되어 있던 것 처럼 매개변수를 람다로 넘겨 받으면 됩니다.

     

     

    💡 정리

    위 글을 통해서 다음과 같은 내용을 알게 되었습니다

    1. Compose는 상태를 읽는 곳을 추적합니다.

    2. Compose는 Composition, Layout, Drawing 이라는 단계가 존재합니다.

    3. 상태는 최대한 늦게, 나중 단계에서 읽는다면 일반적으로 최적화됩니다.

    반응형

    댓글

Designed by Tistory.