본문으로 바로가기
반응형

Compose로 샘플 앱을 봤을때도 그렇고, 직접 화면을 제작해 보니 기존 xml로 만든 화면보다는 무언지 모를 이질감이 있습니다. 여기서 말하는 이질감은 화면의 미세한 버벅임을 말합니다. 물론 제가 아직 compose를 사용하는 걸음마 단계로 화면을 구현하는데 급급하여, 성능 부분은 미쳐 신경을 쓰지 못하는게 이유이기는 합니다.

게다가 다년간 xml로 만들어온 경험과 개선된 방법을 익히고 적용하며 개발자들한테 compose의 방식은 생소하고 어렵게만 다가옵니다. 기본적인 컨셉의 이해부터 화면의 배치나 구성하기도 어려운데 그걸 이용하여 다양한 효과를 표현하고 성능까지 잡아야 한다면 Compose 적용 자체가 부담스러울수 밖에 없습니다.

Compose에 대한 정보는 google 공식 사이트의 문서밖에 없는 상황에서 (뭔가 튜토리얼 같은 기본서가 아직 안나온 상황에서..) Compose 내부를 설명하는 좋은 영상이 있어 그중에 성능향상을 위한 방법 부분을 정리하려 합니다.

시작하기에 앞서 이 글은 Google Summit 2021의 Deep dive into Jetpack Compose layouts에서 성능 관련된 내용만 발췌하여 사용합니다. 전체 영상은 아래 링크를 확인하시길 바랍니다.[1]

https://www.youtube.com/watch?v=zMKMwh9gZuI&list=PLWz5rJ2EKKc9-BJh8Os6W6iQIp1gtOh2T&index=2

Compose의 내부 동작

Compose는 아래 세단계를 거쳐 화면에 표시됩니다.

Composition 단계에서는 composable function을 compose tree로 만드는 작업을 합니다. 위 그림에서 왼쪽의 functions들은 오른쪽의 compose tree로 표현됩니다. 물론 내부의 state가 바뀌면 compose tree의 구성도 변경됩니다.

Layout 단계는 두가지로 나눠 집니다. Compose의 크기를 측정하여 사용할 공간을 확정한 후에 (Measure), 화면에 알맞게 배치합니다.(Place)

실제로는 위 그림처럼 세단계를 거쳐 Layout 단계를 완성합니다.

자식들의 크기를 측정하고, 이에 따른 자신의 크기를 결정한 후에 자식들의 화면(x,y)에 배치 합니다. Drawing은 이렇게 배치된 compose를 렌더링하여 화면에 그리는 작업입니다.

  1. Measure children: 자기가 가진 자식 compose의 크기를 측정
  2. Decide own size: 자식 compose의 크기가 확정되면 자신의 크기를 확정
  3. Place children: 확정된 공간에 자식들을 좌표(x,y)로 배치

여기서 Compose의 성능상 이점이 나오게 되는데, 이는 단일 패스로 한번만 측정을 한다는점 입니다.

  1. Row를 measure합니다. Row의 measure를 알기 위해서는 자식들의 measure 값을 알아야 합니다. 
  2. 따라서 첫번째 자식인 Image를 measure 합니다. 
  3. Image는 추가적으로 자식 compose를 갖지 않으므로 필요한 size를 바로 계산할 수 있습니다.
  4. 그다음 child compose인 Column을 measure 합니다. column은 자식을 갖는 composable function이므로 size를 확정하기 위해서는 하위 compose의 measure가 필요합니다.
  5. Column의 첫번째 자식 compose인 Text를 measure 합니다.
  6. Text는 하위에 자식을 갖지 않으므로(못하므로) 바로 size를 계산할 수 있습니다.
  7. 두번째 Text composable을 measure 합니다.
  8. 이 역시 자식을 갖지 못하는 leaf composable로 바로 size를 계산할 수 있습니다.
  9. Column의 자식들은 size가 확정되었으니 Column 역시 자신이 필요한 크기를 계산하여 size를 확정할 수 있습니다. 
  10. 최상단의 Row 역시 자식이었던 Image와 Column의 size가 확정되었으므로 본인의 size를 계산하여 확정할 수 있습니다.

이런게 모든 단계의 measure가 끊나면 다시한번 ui tree를 돌면서 화면에 배치 (place) 하게 됩니다.

위 단계가 이해 되었다면 아래 포스팅해 두었던 custom layout 관련글을 이해하기에 훨신 수월해 집니다.[2][3]  해당 내용을 이미 다 파악하고 있다는 가정하에 실제 코드에서는 어떻게 처리하는지 보도록 하겠습니다.

Layout sample

여기서는 아래와 같이 vertical grid를 custom layout으로 만들어 봅니다. 입력값으로 몇개의 column을 사용할지 받도록 함수를 구성합니다.

해당 composable function은 columns의 개수를 param으로 전달 받도록 합니다.

Layout을 구성할때 각각의 item의 width는 부모에서 전달받은 제약조건인 constraint의 maxWidth를 배치할 개수만큼 나눠서 갖도록 합니다.

그리고 child composable을 measure할때 사용할 constraint의 width를 재정의하여 해당 size를 사용하도록 강제해 줍니다

이렇게 만든 제약조건으로 자식 composable (measurables) 을 measure하여 placeables로 반환받습니다. 모든 크기가 정해졌으니 본인의 width, height를 계산할수 있고 자식들을 알맞은 x,y좌표에 배치 시킬수 있습니다.

이번에는 하단 navi bar를 만드는 예제 입니다. 버튼이 선택되는경우에만 아이콘과 이름을 animation으로 보여주고 나머지 메뉴들은 icon만 보여지도록 합니다. 

navi bar에 위치할 개별 아이템을 그리는 BottomNavItem() composable을 하나 만듭니다. icon과 text composable을 각각 전달 받으며, 0~1f 까지의 값에 따라 text의 노출 여부를 결정하는 animationProgress 값도 전달 받습니다. 0이면 text가 표시되지 않으며, 1이면 text가 표시 됩니다.

먼저 각각에 icon, text에 layoutId를 부여하여 measure시점에 구분할수 있도록 만듭니다. 그냥 순서대로 해도 되지만 명확하게 id를 부여하는쪽이 가독성면이나, 버그 발생 감소 면에서도 유리 합니다.

measure단계에서 각각의 id로 measure를 수행 합니다. 따라서 icon과 text가 필요한 크기가 확정 됩니다.

다만 animation의 값에 따라서 text와 icon의 x 좌표가 달라지며 0때는 text는 아예 보여지지 않아야 합니다. 따라서 animationProgress의 값에 따라 textWidth를 결정하고, 결정된 textWidth로 icon과 text의 x좌표를 계산합니다.

계산된 x,y 좌표로 icon과 text를 배치하여 animation 효과가 적용된 custom layout을 만들 수 있습니다.

When to go custom?

custom layout을 만들어 사용하는건 보통 아래의 이유 때문입니다.

  1. 기존에 제공하는(Row, Column, Box..) Layout으로 화면을 그릴기 어려울때.
  2. measurement/placement의 정확한 조정이 필요할때
  3. Layout anmimation이 필요할때
  4. 성능 (성능을 높일 수 있도록 만들기 위해)

Modifier의 동작 순서

modifier는 padding을 조절하는것 부터 화면의 배치, click, drag, talkback등 다양한 기능을 composable에 제공합니다. modifier는 chaining을 통해서 여러 설정들을 적용 가능하며, chaining 순서에 따라 동작이 달라 집니다. 이런 동작들이 내부적으로는 어떻게 달라지는 아래의 Box 예제를 통해서 알아보겠습니다.

위와 같이 Box를 생성하면 파란색으로 화면의 중간에 위치하게 됩니다.

만약 간단하게 위와 같이 두개의 설정만 넣었다면 Box의 화면상위치는 기본위치인 좌상단이 됩니다. 여기에 fillMaxSize()와 WrapContentSize()에 의해서 아래의 단계로 인하여 가운데에 위치하게 됩니다.

Box를 갖는 부모의 최대 공간이 가로 200 x세로 3000 이라고 할때,

1. fillMaxsize() 단계에서 가로, 세로를 사용할수 있는 공간이 200x300으로 measure 됩니다.

2. wrapContentSize()는 이 공간에서 중간에 box를 위치시키는 역할을 합니다. 이느 wrapContentSize(align = Alignment.Center) 가 기본값이기 때문입니다. 여기서도 여전히 가용 가능한 공간은 200x300으로 measure 됩니다.

3. size(50.dp)는 박스의 가로,세로의 크기를 50x50으로 한정합니다. 따라서 box의 실제 measure된 최종 값은 50x50이 됩니다.

measure가 완료되었기 대문에 이제 배치(place)를 진행합니다.

이번에 위 순서와 반대로,

1. box는 50x50의 크기로 place 되어야 합니다.

2. size()역시 place의 크기는 50x50입니다.

3. wrapContentSize에서 배치할 공간은 200x300으로 결정되며, 기본 정렬값(align)에 따라 가운데 배치 됩니다.

Performance

위에서 measureplace에 대한 부분을 여러 예제와 함께 계속 언급했던 이유는 사실 이 performance 항목을 언급하기 위함 이었습니다. 변경된 사항으로 화면을 갱신하기 위해서는 recomposition을 해야 하며 composition -> layout -> Drawing 단계를 다시 거치게 됩니다. 이때, 화면에 그리는 내용이 바뀌는게 아니라 단순히 어디에, 어떻게 위치시킬지만 바뀌는 거라면 위 세단계 과정 전체를 거치는게 아니라 일부 과정을 생략시켜 속도를 향상시킬 수 있습니다.

 

과자에 대한 설명을 하는 화면으로 Details를 위로 스크롤 하면 상단 이미는 축소되어 우상단으로 이동하도록 되어 있도록 구성된 화면입니다.

실제로 드래그시 위와같이 동적으로 컵케익 이미지가 우상단으로 이동하고, "Cupcake"이라는 제목도 위로 이동 됩니다. 이를 일반적인 방법으로 구현하면 아래와 같이 코드를 작성할 수 있습니다.

hoisting된 scroll 상태를 Body에 넘겨 줍니다.

Body는 넘겨받은 scroll 상태를 적용하여 Column을 scrollable 하도록 만듭니다.

또한 이 값은 내부의 Title을 나타내는 composable function의 param으로 전달되어 제목의 위치(offset)을 이동 시킵니다. 즉 스크롤에 따라서 제목의 위치가 변경됩니다.

이러한 코드로 구현된다면,Title function은 scroll이 됨에 따라서 지속적으로 recomposition이 발생합니다.  또한 Modifie의 offset 변경을 위해 다시 measure -> place 작업을 진행합니다.

하지만 아래와 같이 scroll 값을 제공하는 lambda function으로 param을 변경하면 offset을 다른 방식으로 구성할 수 있습니다.

scroll의 값 변경을 읽을 수 있는 lambda를 수행하는 코드를 param으로 전달받으면, scroll의 변경은 다른 시간에 계산 할 수 있습니다. 즉  offset안에 lambda를 통해 스크롤 값을 얻어 계산하도록 코드를 작성하면 Modifier를 재생성할 필요가 없습니다. 따라서 measure 단계 없이 place 단계만 수행됩니다.

즉 Title의 내용이 바뀌지 않았으니 place -> drawing 작업만 수행되면서 수행 단계를 효율적으로 만들 수 있습니다.

추가적으로 위에서 언급했던 예제중에 하단 navigation 코드를 다시보겠습니다.

이 구현을 위하여 각각의 item들이 animation progress 값을 전달 받았습니다. (0~1) 동일하게 animation의 상태가 변경될때마다 BottomNavItem() functions은 recomposing되어야 합니다.

하지만 위와 같은 예제도 위치만 달라질뿐 실제로 보이는 내용이 바뀌는 부분은 아닙니다. 따라서 아래와 같이 lambda로 animationProgress를 전달하는 함수를 넘겨주도록 수정 합니다.

따라서 위와 같이 수정함에 따라 composition 단계를 건너뛸수 있습니다.

이 아이디어를 적용하는 key point는 recomposition이 발생해야하는 목적에 있습니다. 무엇을 보여줄지에 대한 recomposition이 아니라 어디에?, 어떻게? 보여줄지에 대한 변경이라면 위와같은 변경으로 불필요하게 발생하는 recomposition을 방지할 수 있습니다.

References

[1] https://www.youtube.com/watch?v=zMKMwh9gZuI&list=PLWz5rJ2EKKc9-BJh8Os6W6iQIp1gtOh2T&index=2

[2] 2021.09.30 - [개발이야기/Android] - [Compose] 7.Custom Layout

[3] 2021.10.01 - [개발이야기/Android] - [Compose] 8. Complex custom layout - staggered layout

반응형