본문 바로가기
Development/Android

[Android/안드로이드] Jetpack Compose - Stability와 Recomposition 그리고 최적화

by du.it.ddu 2024. 6. 27.
반응형

Jetpack Compose가 정식으로 출시된 이후, 많은 개발자들이 관심을 갖고 사용하고 있습니다.
또한 Jetpack Compose 역시도 많은 업데이트가 있었고, 앞으로도 그럴 것이라 예상이 됩니다.

다양한 사용 사례가 나타나면서 선언형 UI 프로그래밍 방식은 익숙해지거나, 익숙해지기 쉬운 환경들이 만들어졌다고 생각합니다.
우리는 이제 "효율적으로" Compose를 사용하는 방법을 알아가고 익힐 필요가 있습니다.

Jetpack Compose를 어떻게 효율적으로 사용할 수 있을까요?


UI의 입장에서 "효율적이다" 를 생각해보면 의외로 답은 간단할 수 있습니다.
우리가 XML 방식으로 UI를 구현할 때, 어떻게 효율적인 코드를 작성하는지 생각해봅시다.
여러가지가 떠오를 수 있는데, 저는 "불필요한 UI 업데이트가 발생하지 않도록 한다" 가 중요한 포인트가 아닐까 합니다

UI는 메인 쓰레드에서 동작하기 때문에 잦은 업데이트는 곧 메인 쓰레드에서 할 일이 많아짐을 의미하고, UI의 성능에 부정적인 영향을 미칠 가능성이 높죠.

그래서 업데이트는 최소한으로, 필요하더라도 작업은 최소한으로, 작업들은 ViewModel과 같은 영역에서 메인 쓰레드가 아닌 곳에서 실행하고 최종 결과가 메인 쓰레드에서 반영될 것과 같은 생각을 하게 됩니다.

이것을 Jetpack Compose에도 마찬가지로 적용할 수 있을 겁니다.
그렇다면 Jetpack Compose에서 불필요한 UI 업데이트가 발생하지 않는 방법은 무엇이 있을까요?

제목에서 유추할 수 있듯이, 오늘의 주제인 Stability가 바로 그것입니다.


Stability에 대해 알아보기 전에, Jetpack Compose가 UI를 그려내는 방식을 이해할 필요가 있습니다.
그래야 Jetpack Compose의 입장에서 불필요한 UI 업데이트 과정이 어떤 것이 있는지, Stability가 어떻게 최적화할 수 있는지 이해할 수 있을 겁니다.

https://developer.android.com/develop/ui/compose/phases?hl=ko

 

Jetpack Compose 단계  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Jetpack Compose 단계 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 대부분의 다른 UI 도구 키트와 마찬가

developer.android.com

위 문서를 자세히 읽어보는 것이 가장 좋습니다만, 번거롭다면 제 나름대로 요약한 내용을 참고해주세요.

Jetpack Compose는 아래와 같은 순서로 UI를 만들어냅니다.

Composable 함수에 Data(혹은 상태)가 변경되면, Composition을 하게되고, UI의 크기를 결정하고 배치하게 됩니다.
그리고 Drawing을 거쳐 비로소 UI가 보여지게 되는 것이죠.

기존의 XML 뷰가 그려내는 과정에 크게 다르지 않습니다만, 우리는 "Composition" 이라는 것에 주목할 필요가 있습니다.

Composable 함수는 Data(혹은 상태)를 기억하고 있고, 변경되었을 때 UI를 갱신하기 위해 Composition을 실행하게 됩니다.
그리고 이 과정을 Recomposition 이라고 하며, UI를 업데이트하는 트리거가 되는 셈이죠.

눈치를 채신분도 계시겠지만, Jetpack Compose에서 UI 최적화, 혹은 효율적이라는 것은 결국 Recomposition이 최대한 적게 발생하도록 하는 것을 의미합니다.


자 그렇다면, Stailibty와 Recompostion은 어떤 관계가 있을까요?

우리는 Composable 함수에 전달하는 Data(혹은 상태)가 변경되어야만 Recompostion이 발생한다는 것을 알았습니다.
그럼 "변경되었다" 는 어떤 기준으로 결정되는 걸까요? 객체의 equlas가 true면 되는 것일까요?
어쩌면 맞는 말이지만, 어쩌면 잘못된 생각이 될 수 있습니다.

https://developer.android.com/develop/ui/compose/performance/stability?hl=ko

 

Compose의 안정성  |  Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose의 안정성 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose는 유형을 안정적이거나 불안정

developer.android.com

Compose는 타입을 "안정적이다", 와 "불안정하다" 두 가지로 구분합니다.
그리고 이 두 가지로 Recomposition을 결정합니다.

Composable에 변경되지 않은 매개변수가 있다면 Recomposition을 건너뜁니다.
그러나 Composable에 불안정한 매개변수가 있다면, 변경되지 않았더라도 Recomposition가 발생하게 됩니다.

따라서 우리는 불안정한 매개변수가 많이 포함되어 있다면 Recomposition이 의도치 않게 많이 발생하게 되고, UI 성능에 부정적인 영향이 있을 것이라 예상할 수 있습니다.

결국 우리의 목표는 불안정한 매개변수를 최대한 없애는 것으로 볼 수 있습니다.
그렇다면 어떤 것이 안정적인 매개변수이고 어떤 것이 불안정한 매개변수 일까요?


하나의 예시를 보겠습니다.

data class Contact(
    val name: String,
    val number: String
)


data class Contact(
    var name: String,
    var number: String
)

@Composable
fun ContactRow(contact: Contact, modifier: Modifier = Modifier) {
   var selected by remember { mutableStateOf(false) }

   Row(modifier) {
      ContactDetails(contact)
      ToggleButton(selected, onToggled = { selected = !selected })
   }
}

 

위 코드에서 Contact의 두 가지 버전이 있습니다.
하나는 매개변수들이 "val" 로 불변이며, 하나는 "val"로 가변입니다. 어떤 것이 안정적인 매개변수 일까요?

어렴풋이 매개변수들이 불변인 것이 안정적이라는 생각이 들 것이며, 정답입니다.

그 이유는, Compose는 객체에 "var"로 선언된 매개변수들의 속성을 인식하지 못하고,
Compose의 상태 객체의 변경만 추적하기 때문입니다.

즉, 불변 변수들로 선언했다면 상태를 변경하기 위해 객체 자체를 변경해야 하지만, 가변 변수로 선언했다면 객체를 변경하지 않고 매개변수들을 변경할 수 있기 때문에 Compose는 이를 인식하지 못하게 됩니다.

따라서 Compose는 "불안정하다" 로 판단해서 실제 값이 변경되지 않아도 Recomposition의 대상으로 인식하게 됩니다.


이제 "안정적인" 매개변수와 "불안정적인" 매개변수가 어떤 차이인지 이해했을 것입니다.
그렇다면 항상 "val"을 사용해서 불변 매개변수들로만 객체를 구성하면 "항상" 안정적일까요?

안타깝게도 그렇지만은 않습니다.

예를들어 우리는 List, Map, Set과 같은 컬렉션을 val로 선언한다면, 내용물을 변경할 수 없기 때문에 불변하다라고 생각하게 될 것입니다

그러나 컬렉션들은 interface이기 때문에 가변한 형태의 구현체가 주입될 수 있으므로 "불안정하다" 로 인식됩니다.
이것을 해소하기 위해 Compose 컴파일러에는 Kotlinx 변경 불가능한 컬렉션 이 포함되어 있습니다.

 

GitHub - Kotlin/kotlinx.collections.immutable: Immutable persistent collections for Kotlin

Immutable persistent collections for Kotlin. Contribute to Kotlin/kotlinx.collections.immutable development by creating an account on GitHub.

github.com

 

컬렉션이 아니더라도, 기본 자료형을 제외한다면 어떤 것이든 "불안정한" 매개변수로 여겨질 수 있습니다.
이럴 때 우리는 다음과 같은 방법을 사용할 수 있습니다.

첫번째, Stable 또는 Imutable 어노테이션을 사용할 수 있습니다.

@Immutable // or @Stable
data class Snack(
…
)

이렇게 하면 Compose 컴파일러가 해당 객체의 구현이 어떻게 되었든 안정적으로 표시합니다.
이 방법은 편리하고 효과적일 수 있지만, 잘못된 객체에 사용한다면 의도치 않게 Recompostion을 건너뛰게 만들 수 있습니다.
따라서 주의해야 합니다.

또는 안정성 구성 파일 을 사용해서 안정적이라고 간주할 클래스의 구성 파일을 컴파일 시간에 제공해서, 클래스를 안정적인 것으로 간주할 수 있습니다.
자세한 내용은 링크의 가이드를 참고하는 것을 권장합니다.


지금까지 Jetpack Compose의 Stability와 Recomposition을 최소화해서 최적화 하는 방법에 대해 알아보았습니다.
별 생각 없이 Compose를 사용해서 UI를 구성하고 있었다면, 이런 부분을 고려해서 Compose를 위한 좋은 UI 설계가 무엇인지 고민해보면 좋을 것 같습니다.

만약 현재 프로젝트의 Composable 함수들이 얼마나 안정적인지(효율적인지) 알고 싶다면,
안정성 문제 진단 의 가이드를 참고해서 Compose 컴파일러 보고서를 참고해보시면 좋을 것 같습니다.

자, 그럼 우리의 Composable 함수들에게 안정감을 줘봅시다.

반응형