Recomposition

98oys·2024년 1월 29일
0

Android Jetpack Compose

목록 보기
5/5

1. Recompostion 이란?

  • Compose에서 UI는 함수로 표현되며, 데이터가 변경될 때마다 Recompostion이 됩니다.
  • Recompostion은 Compose에서 UI가 변경될 때 발생하는 프로세스 입니다.
  • 변경사항이 감지되면 Compose는 해당 부분을 다시 그리고 업데이트합니다.

→ 리컴포지션은 Composable 함수의 상태가 변경될 때 Compsable 함수를 다시 호출하여, UI를 업데이트 하는 것

2. Recomposition & Performance

전체 UI 트리를 재구성하는 것은 컴퓨팅 성능과 배터리 수명을 사용하므로 계산 비용이 많이 들 수 있습니다. Compose는 SmartRecpomposition을 통해 이 문제를 해결합니다 .

공식 문서에 따르면 UI트리를 재구성하는 것은 비용이 많이 들 수 있다고 나와 있습니다. 아래의 요인에 따라 빈번한 리컴포지션 발생 시, 성능저하 이슈가 발생할 수도 있습니다.

  1. UI의 복잡성
    • 복잡한 UI에서 많은 컴포저블이 동시에 리컴포지션이 빈번하게 일어나면, 레이아웃 계산과 그리기 작업에 더 많은 자원이 소비될 수 있습니다.
    • 애니메이션 사용
      • 로띠 애니메이션, 고용량 그래픽 이미지, 지속적인 루프 연산 등등
  2. 데이터 모델의 규모
    • 대량의 데이터가 자주 변경되면, 리컴포지션 소요 시간이 늘어날 수 있습니다.
  3. 기기 성능
    • 성능이 낮은 기기에서는 빈번한 리컴포지션은 더 큰 영향을 미칠 수 있습니다.

경험적으로 일반적인 UI에서 리컴포지션이 몇 번 더 발생하더라도, 퍼포먼스가 떨어진다는 것은 체감하지 못했습니다. 하지만 그럼에도 최적화하는 방법에 대해서 생각해 볼 필요는 있을 것 같습니다. 그러면 불필요한 리컴포지션을 최소화하기 위해 우리가 할 수 있는 것은 무엇일까요? 컴포저블 파라미터의 안정성을 확인하는 것입니다. 불안정한 상태가 많이 포함되어 있으면, 성능 및 기타 문제가 발생할 수 있습니다.

3. Stability

컴포즈는 컴포저블 파라미터의 안정성을 사용하여, 리컴포지션을 스킵할 수 있을 지 결정합니다.

컴포즈는 타입을 안정적인 것 또는 불안정적인 것으로 간주합니다.

  • Immutable하거나 컴포즈가 리컴포지션 시에 값이 변경되었는 지 알 수 있는 경우 안정적 타입입니다.
  • 컴포즈가 리컴포지션 간 값이 변경되었는지 여부를 알 수 없는 경우는 불안정한 타입입니다.

Type


Stable

  • 모든 원시 값 타입, 문자열, 함수 타입 파라미터는 Stable합니다.
  • 인스턴스는 모든 프로퍼티가 Stable 해야 안정적인 것으로 간주합니다.
data class User(
	val id: String
	val name: String
)
  • equals 결과가 동일한 두 인스턴스의 경우 안정적인 것으로 간주합니다.
User("1", "XXX") == User("1", "XXX")
  • @Stable 어노테이션을 사용하면, 컴포즈 컴파일러가 안정적인 값으로 인식하게 할 수 있습니다.
  • 클래스에 어노테이션을 달면 컴파일러가 클래스에 관해 추론하는 내용이 재정의됩니다. Kotlin의 !! 연산자와 유사합니다.
  • 컴포저블의 모든 파라미터가 Stable하고, 변경사항이 없다면, 리컴포지션을 스킵할 수 있습니다.

Unstable

  • 컴포저블에 불안정한 매개변수가 있는 경우 Compose는 구성요소의 상위 요소를 재구성할 때 항상 컴포저블을 재구성합니다.
  • Collection
    • Compose는 List, SetMap와 같은 컬렉션 클래스를 항상 불안정한 것으로 간주합니다. 이는 해당 파일이 불변성이라고 보장할 수 없기 때문입니다. (MutableList)
    • 대신 https://github.com/Kotlin/kotlinx.collections.immutable을 사용하거나, 클래스에 @Immutable 또는 @Stable로 어노테이션을 달아서 안정적인 타입으로 만들 수 있습니다.
    • 따라서 컬렉션 클래스를 사용할 때는 @Immutable 또는 @Stable 어노테이션을 사용해야합니다.
    data class Snack(
      val id: Long,
      val name: String,
      val imageUrl: String,
      val price: Long,
      val tagline: String = "",
      val tags: Set<String> = emptySet()
    )
    
    unstable class Snack {
       stable val id: Long
       stable val name: String
       stable val imageUrl: String
       stable val price: Long
       stable val tagline: String
       unstable val tags: Set<String>
       <runtime stability> = Unstable
    }
   @Immutable
   data class Snack(
     val id: Long,
     val name: String,
     val imageUrl: String,
     val price: Long,
     val tagline: String = "",
     val tags: Set<String> = emptySet()
   ) //snackList는 여전히 unstable
   	
   
   @Composable
   private fun HighlightedSnacks(
       snacks: ImmutableList<Snack>,
   )
   
   @Immutable
   data class SnackCollection(
      val snacks: List<Snack>
   )
   
   	
   @Composable
   private fun HighlightedSnacks(
       index: Int,
   -   snacks: List<Snack>, (unstable)
   +   snacks: ImmutableList<Snack>, (stable)
       onSnackClick: (Long) -> Unit,
       modifier: Modifier = Modifier
   )
   
   	or
   
   @Composable
   private fun HighlightedSnacks(
       index: Int,
       snacks: SnackCollection,
       onSnackClick: (Long) -> Unit,
       modifier: Modifier = Modifier
   )
   
   https://developer.android.com/jetpack/compose/performance/stability/fix?hl=ko#immutable-collections
  • Mutable Object
    data class Contact(
    	 var name: String,
    	 var number: String
    )
  • 클래스 내에 변수가 포함되어 있으면, 컴포즈는 이를 불안정하다고 간주합니다.
  • 불안정한 클래스는 속성이 변경되어도 컴포즈가 인식하지 못합니다. 이는 Compose가 Compose 상태 객체의 변경사항만 추적하기 때문입니다.
    • 컴포저블에 Unstable 객체가 포함되어 있다면, 항상 리컴포지션이 발생합니다.
  • Painter
    • Painter 는 불안정한 타입입니다. Painter를 파라미터로 갖고 있는 컴포저블은 상위 컴포저블이 이 리컴포지션 될 때, 항상 같이 리컴포지션 됩니다.
    • 따라서 Painter를 매개변수로 전달하는 대신 URL 또는 드로어블 리소스 ID를 매개변수로서 컴포저블에 전달하는 것이 좋습니다.
    @Composable
    fun UnstableImage(painter: Painter) {
    	//항상 리컴포지션이 발생함.
    }
    
    @Composable
    fun MyImage(url: String) {
    
    }
    
    파라미터로 url 또는 res 값을 받는 것을 권장
    https://developer.android.com/jetpack/compose/graphics/images/optimization?hl=ko#pass-url

4. 요약

  • 컴포저블의 매개변수 타입이 불안정하거나 변경될 때 리컴포지션된다.
  • 상태가 변경되지 않았는데, 리컴포지션이 발생한다면?
    • 불안정한 타입의 상태를 체크하고, 안정적인 타입으로 만들자
  • Unstable 타입을 가지고 있다면, 무조건 리컴포지션된다.
    • 추적이 불가능해서 상태가 바뀌었는지 안바뀌었는지 모르기 때문
  • Stable 타입만 가지고 있다면, 상태 변경 시에만 리컴포지션됩니다.
    • 추적이 가능한 상태이므로
  • immutable 은 상태가 변하지 않으므로 recomposition에서 제외된다.
  • Skippable 컴포저블을 만들기 위해서 파라미터의 Stability를 체크하자
    • 컴포저블을 최대한 작고 구체적으로 만드는 것이 좋다.

Stability in Compose  |  Jetpack Compose  |  Android Developers


Q&A

  • Unstable 컴포저블은?
    • 컴포즈는 Unstable한 컴포저블을 항상 리컴포지션한다.
    • 상위 컴포저블에서 리컴포지션이 발생했을 경우, 항상 리컴포지션 된다.
  • 실제로 Stable하지만 Unstable 타입으로 간주되는 상황에서는?
    • @Stable 을 붙이자.
    • 어떤 상황에서 붙이면 좋은가?
      • 리스트를 포함하고 있는 경우
  • Immutable 어노테이션을 붙여야 하는 상황은?
  • Collection 타입은 불안정한타입이다. 그렇다면 어떻게 안정적으로 만드는가?
  • Immutable과 Stable의 차이

→ 모든 파라미터가 안정적이라면 상태가 변할때만 UI가 갱신된다. 이상태를 skippable이라고한다. 우리는 최대한 skippable한 UI를 구성해서 렌더링 효율을 높이자

  • using은 @Immutable 값이 절대 변하지 않을 것이라는 약속입니다.
  • using은 @Stable 값을 관찰할 수 있다는 약속이며 변경된 경우 리스너에게 알림을 보냅니다.

불변 객체는 '인스턴스가 생성된 후에 공개적으로 액세스 가능한 모든 속성과 필드가 변경되지 않음'을 의미합니다. 이 특성은 Compose가 두 인스턴스 간의 '변경 사항'을 매우 쉽게 감지할 수 있음을 의미합니다.

반면에 안정적인 객체가 반드시 불변인 것은 아닙니다. 안정적인 클래스는 변경 가능한 데이터를 보유할 수 있지만 모든 변경 가능한 데이터는 변경 시 Compose에 알려야 필요에 따라 재구성이 발생할 수 있습니다.

Compose는 모든 함수 매개변수가 안정적이거나 변경할 수 없으며 건너뛸 수 있는 함수의 핵심임을 감지하면 런타임 시 다양한 최적화를 활성화할 수 있습니다. Compose는 클래스가 불변인지 안정적인지 자동으로 추론하려고 시도하지만 때로는 올바르게 추론하지 못하는 경우도 있습니다. 그런 일이 발생하면 클래스에서 [@Immutable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Stable)및 주석을 사용할 수 있습니다.[@Stable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Stable)

불변성과 안정성#

다시 시작 가능 및 건너뛸 수 있음은 함수의 Compose 속성인 반면 불변성과 안정성은 객체 인스턴스, 특히 구성 가능한 함수에 전달되는 객체의 속성입니다.

불변 객체는 '인스턴스가 생성된 후에 공개적으로 액세스 가능한 모든 속성과 필드가 변경되지 않음'을 의미합니다. 이 특성은 Compose가 두 인스턴스 간의 '변경 사항'을 매우 쉽게 감지할 수 있음을 의미합니다.

반면에 안정적인 객체가 반드시 불변인 것은 아닙니다. 안정적인 클래스는 변경 가능한 데이터를 보유할 수 있지만 모든 변경 가능한 데이터는 변경 시 Compose에 알려야 필요에 따라 재구성이 발생할 수 있습니다.

@Stable
class SomeViewState {
  var someFlag by mutableStateOf(false)
}

Compose는 모든 함수 매개변수가 안정적이거나 변경할 수 없으며 건너뛸 수 있는 함수의 핵심임을 감지하면 런타임 시 다양한 최적화를 활성화할 수 있습니다. Compose는 클래스가 불변인지 안정적인지 자동으로 추론하려고 시도하지만 때로는 올바르게 추론하지 못하는 경우도 있습니다. 그런 일이 발생하면 클래스에서 [@Immutable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Stable)및 주석을 사용할 수 있습니다.[@Stable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Stable)


Recomposition의 장점과 단점

단점:

성능 문제: 구성 가능한 함수에서 불필요한 재구성이 수행되면 애플리케이션 성능에 부정적인 영향을 미칠 수 있습니다. 따라서 개발자는 재구성을 신중하게 사용하고 불필요한 재계산을 피해야 합니다.

메모리 소비: 구성 가능한 함수가 재구성되면 Jetpack Compose는 메모리를 활용합니다. 이로 인해 애플리케이션의 메모리 소비가 증가하여 성능에 부정적인 영향을 미칠 수 있습니다.

업데이트 문제: 재구성을 사용하면 구성 가능한 함수 내의 항목이 변경될 때 전체 함수를 다시 계산할 수 있습니다. 하나의 항목을 변경하면 전체 인터페이스에 영향을 미칠 수 있으므로 이는 특히 대규모 애플리케이션에서 문제가 될 수 있습니다. 따라서 Jetpack Compose 개발자는 컴포저블 기능을 최대한 작고 구체적으로 만드는 것이 좋습니다.

제한된 실행 취소 지원: 재구성은 구성 가능한 기능의 적절한 실행 취소를 항상 보장하지 않습니다. 따라서 개발자는 필요한 경우 실행 취소 작업을 수동으로 처리해야 할 수도 있습니다.

장점:

효율성: 재구성은 수정된 구성 요소만 다시 계산하므로 전체 인터페이스를 다시 계산할 필요가 없습니다. 이를 통해 애플리케이션의 성능이 향상되고 효율성이 향상됩니다.

단순성: Jetpack Compose는 Android 인터페이스를 디자인하기 위해 XML과 같은 기존 도구 대신 Kotlin으로 직접 코드 작성을 사용합니다. 이를 통해 개발자는 인터페이스를 더 빠르고 쉽게 만들 수 있습니다.

모듈성: 구성 가능한 기능은 재사용 가능한 작은 부분으로 나눌 수 있습니다. 이를 통해 개발자는 인터페이스를 더욱 모듈화하여 더 깔끔하고 읽기 쉬운 코드를 만들 수 있습니다.

쉬운 테스트 가능성 및 유지 관리: 재구성은 코드의 복잡성을 줄여줍니다. 개발자는 필요한 경우에만 특정 요소의 재계산을 지정하면 됩니다. 이렇게 하면 코드가 더 모듈화되고 유지 관리가 더 쉬워집니다.

애니메이션: 재구성은 애니메이션에도 사용할 수 있습니다. 애니메이션의 자동 재구성은 보다 원활한 사용자 경험을 제공합니다.

정의

  • Recomposition은 컴포저블 함수의 상태가 변경되었을 때, 화면을 재구성하는 것을 의미합니다.
  • 컴포즈 런타임은 변경된 상태를 사용하여 컴포저블 함수를 다시 호출합니다.(Restartable)
  • 필요한 경우에 한해 컴포저블 함수에 의해 방출된 위젯이 새로운 데이터로 다시 그려집니다. (smart recomposition)
  • 재구성은 입력이 변경될 때 구성 가능한 함수를 다시 호출하는 프로세스. 이는 함수의 인풋이 변경될 때 발생합니다.
  • 비용이 많이 드는 작업을 수행해야 하는 경우, 뷰모델 내 백그라운드 코루틴에서 수행하고, 연산의 결과를 컴포저블 함수에 매개변수로 전달하는 방식이 좋습니다.
    • 앱 로직은 업데이트를 트리거하기 위해 콜백과 함께 현재 값을 전달합니다.

Recomposition은 세 단계로 나뉩니다.

  1. Composition : 화면에 표시할 컴포지션 트리를 그립니다. 어떤 컴포저블 함수가 조건에 따라 숨겨질 수도 있고, 보여져야 하는지, 또한 어떤 컴포넌트 간에 어떤 트리 구조로 부모 자식관계를 갖는지 결정합니다. 즉, 무엇을 보여줄 지 결정합니다.
  2. Layout : 선택된 컴포넌트들이 화면에 어떻게 배치될지를 결정합니다. 각 컴포넌트의 x,y좌표 width/height가 결정됩니다. 즉 어디에 보여줄지 결정합니다 where to show
  3. 마지막으로 정해진 컴포넌트를 정해진 위치에 그립니다. Draw to the Screen

위 세 단계를 거쳐 컴포저블 함수는 리컴포지션 됩니다.

  • 컴포저블은 어떤 순서로든 실행될 수 있다.
  • 구성가능한 함수는 병렬로 실행될 수 있다.
    • 순차적으로 호출되지 않습니다.
  • 리컴포지션은 가능한 많이 스킵합니다.
    • 상태 변경이 없는 컴포저블인 경우, 리컴포지션을 스킵합니다.
  • 재구성은 낙관적이며 취소될 수 있습니다.
    • optimistic: 낙관적인 → 사이드이펙트가 없을 것으로 예상하는
  • 컴포저블 함수는 애니메이션의 프레임만큼 자주 실행될 수 있습니다.

→ 모든 경우에 가장 좋은 방법은 구성 가능한 함수를 빠르고, 멱등성이 있고, 부작용이 없도록 유지하는 것입니다.

쉽게 말하면 리컴포지션이랑 컴포저블의 상태가 바뀔 때 화면을 재구성하는데,

우리가 실제로 개발하면서 신경쓰이는 부분은 리컴포지션이 여러번 될 경우, 성능 저하 이슈가 발생할까? 어떤 악영향이 있을까? 리컴포지션 최적화가 성능향상에 도움이되는가? 이런 고민들을 하게 된다.

전체 UI 트리를 재구성하는 것은 컴퓨팅 성능과 배터리 수명을 사용하므로 계산 비용이 많이 들 수 있습니다. Compose는 지능적인 재구성을 통해 이 문제를 해결합니다 .

  • 그래도 최악의 경우를 가정해보자.

그렇다면 리컴포지션을 최소화하기 위한 방법은 무엇이 있을까?

  1. key 지정
  2. stable & immutable 어노테이션 활용
  3. 안정적인 것 불안정적인것? 불변인것?

https://tourspace.tistory.com/536

profile
Android Developer, Department of Information and Communication Engineering, Inha University

0개의 댓글