Compose의 안정성

Compose는 유형을 안정적 또는 불안정으로 간주합니다. 변경 불가능하거나 Compose에서 값이 재구성 간에 변경되었는지 알 수 있으면 안정적인 유형입니다. Compose에서 값이 재구성 간에 변경되었는지 알 수 없으면 불안정한 유형입니다.

Compose는 컴포저블의 매개변수 안정성을 사용하여 입력을 비교하는 방법을 결정하고 리컴포지션 중에 컴포저블을 건너뛸 수 있는지 결정합니다 (Kotlin 2.0.20부터 기본적으로 사용 설정된 강력한 건너뛰기 모드 사용).

  • 안정적인 매개변수: Compose는 구조적 동등성 (Object.equals())을 사용하여 안정적인 매개변수를 비교합니다. 컴포저블의 안정적인 매개변수가 이전 값과 동일하면 Compose는 이를 건너뜁니다.
  • 불안정한 매개변수: 강력한 건너뛰기 모드가 사용 설정된 경우 (Kotlin 2.0.20 이상의 기본값) Compose는 인스턴스 동등성 (===)을 사용하여 불안정한 매개변수를 비교하고 동일한 객체 인스턴스가 전달되면 컴포저블을 건너뜁니다. Kotlin 2.0.20 이전 버전에서는 (또는 강력한 건너뛰기 모드가 사용 중지된 경우) 상위 요소가 리컴포즈될 때 불안정한 매개변수가 있는 컴포저블이 항상 리컴포즈됩니다.

앱이 불안정한 매개변수의 새 인스턴스를 자주 할당하거나 모델에 과도하게 주석을 달아 대규모 컬렉션에서 비용이 많이 드는 .equals() 검사를 강제하는 경우 불필요한 리컴포지션이나 비교 오버헤드가 발생할 수 있습니다.

이 문서에서는 Compose가 안정성을 결정하는 방법과 성능 및 전반적인 사용자 환경을 개선하기 위해 안정성을 최적화하는 방법을 자세히 설명합니다.

변경 불가능한 객체

다음 스니펫은 안정성과 리컴포지션의 일반적인 원칙을 보여줍니다.

Contact 클래스는 변경 불가능한 데이터 클래스입니다. 모든 매개변수가 val 키워드로 정의된 기본 유형이기 때문입니다. Contact 인스턴스를 만든 후에는 객체의 속성 값을 변경할 수 없습니다. 이 경우 새 객체가 생성됩니다.

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

ContactRow 컴포저블에는 Contact 유형의 매개변수가 있습니다.

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

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

사용자가 전환 버튼을 클릭하고 selected 상태가 변경될 때 어떤 일이 발생하는지 생각해 보세요.

  1. Compose는 ContactRow 내부의 코드를 리컴포즈해야 하는지 평가합니다.
  2. ContactDetails의 유일한 인수가 Contact 유형임을 확인합니다.
  3. Contact는 변경 불가능한 데이터 클래스이므로 Compose는 ContactDetails의 인수가 변경되지 않았다고 확신합니다.
  4. 따라서 Compose는 ContactDetails을 건너뛰고 재구성하지 않습니다.
  5. 반면 ToggleButton의 인수가 변경되었으므로 Compose가 해당 구성요소를 재구성합니다.

변경 가능한 객체

앞의 예에서는 변경 불가능한 객체를 사용하지만 변경 가능한 객체를 만들 수도 있습니다. 다음 스니펫을 고려해 보세요.

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

이제 Contact의 각 매개변수가 var이므로 클래스를 더 이상 변경할 수 없습니다. 속성이 변경되더라도 Compose는 이를 알지 못합니다. 이는 Compose가 Compose 상태 객체의 변경사항만 추적하기 때문입니다.

Compose에서는 이러한 클래스를 불안정한 것으로 간주합니다. 강력한 스킵 모드(Kotlin 2.0.20 이상)를 사용하면 Compose가 인스턴스 동등성 (===)을 사용하여 Contact를 비교합니다. contact.name을 제자리에서 변경하면 리컴포지션이 트리거되지 않지만 값이 동일한 새로 할당된 Contact 인스턴스를 전달하면 ContactDetails이 리컴포지션됩니다 (강력한 스킵 모드가 없으면 selected가 변경될 때마다 ContactDetails이 리컴포지션됨).

Compose의 구현

리컴포지션 중에 건너뛸 함수를 Compose가 정확히 어떻게 결정하는지 고려하는 것이 중요하지는 않지만 도움이 될 수 있습니다.

Compose 컴파일러가 코드에서 실행되면 여러 태그 중 하나로 각 함수와 유형을 표시합니다. 이러한 태그는 리컴포지션 중에 Compose가 함수나 유형을 처리하는 방식을 반영합니다.

함수

Compose는 함수를 skippable 또는 restartable로 표시할 수 있습니다. 함수를 다음 중 하나 또는 둘 다로 표시할 수 있습니다.

  • 건너뛸 수 있음: 컴파일러가 컴포저블을 건너뛸 수 있는 것으로 표시하면 모든 인수가 이전 값과 동일한 경우 리컴포지션 중에 Compose가 이를 건너뛸 수 있습니다. (Kotlin 2.0.20 이상의 강력한 건너뛰기 모드를 사용하면 다시 시작 가능한 모든 컴포저블을 자동으로 건너뛸 수 있습니다.)
  • 다시 시작 가능: 다시 시작 가능한 컴포저블은 리컴포지션이 시작될 수 있는 '범위' 역할을 합니다. 즉, 함수는 상태가 변경된 후 Compose가 리컴포지션을 위해 코드 재실행을 시작할 수 있는 진입점이 될 수 있습니다.

유형

Compose는 유형을 변경할 수 없거나 안정적인 것으로 표시합니다. 각 유형은 다음 중 하나입니다.

  • 변경 불가: 속성 값이 변경될 수 없고 모든 메서드가 참조 투명한 경우 Compose는 유형을 변경 불가로 표시합니다.
    • 모든 기본 유형은 변경 불가능으로 표시됩니다. 여기에는 String, Int, Float가 포함됩니다.
  • Stable: 생성 후 속성이 변경될 수 있는 유형을 나타냅니다. 런타임 중에 이러한 속성이 변경되면 Compose는 이러한 변경사항을 인식합니다.

안정성 디버그

앱이 매개변수가 변경되지 않은 컴포저블을 리컴포즈하는 경우 먼저 불안정한 유형의 새 인스턴스가 모든 리컴포지션에서 할당되는지 (또는 강력한 건너뛰기 모드가 사용 중지된 경우 컴포저블에 불안정한 유형의 var 속성이나 val 속성이 있는 매개변수가 있는지) 확인합니다.

Compose의 안정성과 관련된 복잡한 문제를 진단하는 방법에 관한 자세한 내용은 안정성 디버그 가이드를 참고하세요.

안정성 문제 해결

Compose 구현을 안정화하는 방법에 관한 자세한 내용은 안정성 문제 수정 가이드를 참고하세요.

요약

전반적으로 다음 사항에 유의해야 합니다.

  • 매개변수: Compose는 리컴포지션 중에 구조적 동등성(.equals())을 사용하여 비교할지 인스턴스 동등성 (===)을 사용하여 비교할지 결정하기 위해 컴포저블의 각 매개변수의 안정성을 결정합니다.
  • 즉각적인 수정: 컴포저블이 건너뛰지 않고 성능 문제가 발생하는 경우 각 패스에서 불안정한 매개변수의 새 인스턴스가 다시 생성되는지 또는 State 대신 var 속성이 사용되는지 확인합니다.
  • 컴파일러 보고서: 컴파일러 보고서를 사용하여 클래스에 관해 추론되는 안정성을 확인할 수 있습니다.
  • 컬렉션: Compose는 기본 구현이 변경될 수 있으므로 표준 컬렉션 인터페이스 (List, Set, Map)를 불안정한 것으로 간주합니다. 강력한 건너뛰기 모드 (Kotlin 2.0.20 이상에서 기본적으로 사용 설정됨)를 사용하면 불안정한 컬렉션은 컬렉션 참조가 변경되지 않을 때 빠른 O(1) 인스턴스 동등성 (===)을 사용하여 재구성을 건너뜁니다. 구조적 동등성 (O(N) .equals())이 특별히 필요한 경우가 아니라면 큰 컬렉션을 Kotlinx 변경 불가능한 컬렉션으로 변환하거나 컬렉션 보유 클래스에 @Immutable 또는 @Stable로 주석을 달지 마세요. 각 리컴포지션에서 모든 요소를 비교하는 것이 리컴포지션 자체보다 비용이 더 많이 들 수 있기 때문입니다.
  • 기타 모듈: Compose는 Compose 컴파일러가 실행되지 않는 모듈의 클래스가 불안정하다고 간주합니다 (강력한 건너뛰기 모드에서 ===를 사용하여 비교). 데이터 소스가 동일한 값을 사용하여 Compose가 아닌 모듈에서 작은 플랫 모델을 자주 다시 인스턴스화하는 경우 안정성 구성 파일을 구성하거나 .equals()이 저렴한 @Stable 또는 @Immutable를 사용할 수 있습니다.

추가 자료