Android/Compose

Composables Lifecycle (컴포저블 수명 주기)

네모메모 2024. 5. 6. 06:45
반응형

 

 

 

 

 

 

 

 

 

 

 

 

 


용어정리

  • Composable
    - UI 구성 요소를 나타내는 함수 또는 어노테이션
    • Composable 어노테이션 (@Composable)
      - Composable 함수를 표시하는 어노테이션 
    • Composable 함수
      - Compose에서 UI 구성을 정의하고, UI 요소의 계층 구조를 형성하며, 화면에 표시되는 뷰를 생성하는 함수 
      - UI를 선언적으로 정의하는 Compose에서 사용되는 기본 단위
      - UI 요소를 생성하고, UI 상태를 나타내며, 사용자 입력 및 상호 작용에 응답하는 등의 작업을 수행한다.
  • Composition
    - UI 구성 요소들이 결합되어 화면에 표시되는 것
    - Compose에서는 여러 'Composable 함수'가 조합되어 화면에 나타나는 UI 구조를 형성합니다. Composition은 'Composable 함수'들의 트리 계층 구조를 통해 정의된다.
    - JetBack Compose 가 컴포저블을 실행 할때 빌드한 UI에 관한 설정

    • Initial Composition(=초기 컴포지션) 
      - UI가 처음으로 (컴포저블을 실행하여) 화면에 그려질 때 발생하는 Composition
      - 앱이 시작되거나 새로운 화면이 표시될 때, Compose는 초기 Composition을 수행하여 화면을 그린다.

    • Recomposition
      -
      데이터가 변경될 때 Composition을 업데이트 하기 위해 Composable을 다시 실행 하는 것
        =상태 변경사항에 따라 변경될 수 있는 Composable만을 다시 실행한 다음 변경사항을 반영하도록 컴포지션을 업데이트함

      - 화면에 표시되는 데이터나 상태가 변경되면, Compose는 해당 변경 사항을 감지하고 UI를 다시 그리는 과정을 수행하며, 이 과정을 Recomposition이라고 한다.
      - 사용자 입력, 데이터 업데이트, 상태 변화 등의 이벤트에 의해 발생함
      - Recomposition은 일반적으로 State<T> 객체가 변경되면 트리거됩니다.
      Compose는 이러한 객체를 추적하고 Composition에서 특정 State<T>를 읽는 모든 컴포저블 및 호출하는 컴포저블 중 건너뛸 수 없는 모든 컴포저블을 실행합니다.

 

 

 


Composition

  • 'UI를 기술하는 Composable'의 트리 구조이다.
  • 앱의 UI를 설명하고 컴포저블을 실행하여 생성된다.
  • 동작
    1. Initial Composition 시 처음으로 컴포저블을 실행할 때, Composition에서 UI를 기술하기 위해 호출하는 Composable을 추적한다.
    2. 그런 다음 앱 상태가 변경되면 Jetpack Compose는 Recomposition을 예약합니다.
  • Initial Composition을 통해서만 생성되고, Recomposition을 통해서만 업데이트될 수 있다.
    Composition을 수정하는 유일한 방법은 Recomposition하는 것이다.


 

 

Composable 의 수명 주기

- Composable 은 컴포지션을 시작하고 0회 이상 재구성되고 컴포지션을 종료한다.

- (위 Composition의 동작에 따라) 컴포저블의 수명 주기는 아래 3가지 상태로 정의됩니다.

  1. Composition 시작
  2. 0회 이상 Recomposition
  3. Composition 종료 이벤트 

           그림 1. 컴포지션 내 컴포저블의 수명 주기. 

 

 - 컴포저블의 수명 주기는 뷰, 활동, 프래그먼트의 수명 주기보다 간단하다,
    따라서 컴포저블이 수명 주기가 더 복잡한 외부 리소스를 관리하거나 이와 상호작용해야 하는 경우 효과를 사용해야 합니다.

        ㄴ> 효과 : Compose에서 컴포저블이 다루는 외부 리소스와의 상호작용을 처리하는 방법을 의미

 

 

 


 

Composable이 여러 번 호출되면?

=> Composition에 여러 인스턴스가 배치되며, Composition의 각 호출에는 자체 수명 주기가 있다.

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

 

 

 

그림 2. Composition 내 MyComposable의 표현. (색상이 다른 요소는 요소가 별도의 인스턴스임)

 

 

 

 

 


Composition 내 Composale 의 분석

  • Composition 내 Composale의 인스턴스는 call site(=호출 사이트)로 식별됨
    Compose 컴파일러는 각 call site를 고유한 것으로 간주합니다.
    따라서 여러 call site에서 Composale을 호출하면 컴포지션에 Composale의 여러 인스턴스가 생성됩니다.
    • Call Site(=호출 사이트) 란?
      - 컴포저블이 호출되는 소스 코드 위치
      - 컴포지션 내 위치와 UI 트리에 영향을 미친다.
  • Recomposition 시 컴포저블이 이전 컴포지션 시 호출한 것과 다른 컴포저블을 호출하는 경우,
    Compose는 호출되거나 호출되지 않은 컴포저블을 식별하며 두 컴포지션 모두에서 호출된 컴포저블의 경우 입력이 변경되지 않은 경우 재구성하지 않는다.

 

 

Side effects를 컴포저블과 연결하기 위해서는,

Recomposition마다 다시 시작하는 대신 완료할 수 있도록 ID를 유지하는 것이 중요하다!!

ex) 

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }

@Composable
fun LoginError() { /* ... */ }
 

위의 코드에서 LoginScreen은 LoginError 컴포저블을 조건부로 호출하며 항상 LoginInput 컴포저블을 호출한다.
각 호출에는 고유한 호출 사이트 및 컴파일러가 호출을 고유하게 식별하는 데 사용하는 소스 위치가 있다.

그림 3. 상태가 변경되고 리컴포지션이 발생할 때 컴포지션 내 LoginScreen의 표현. (색상이 동일하면 재구성되지 않았음을 의미)

LoginInput이 첫 번째로 호출되었다가 두 번째로 호출되었지만 LoginInput 인스턴스는 여러 리컴포지션에 걸쳐 유지된다. 또한 LoginInput에는 리컴포지션 간에 변경된 매개변수가 없으므로 Compose가 LoginInput 호출을 건너뜁니다.


 


Composition 내 Composale 의 분석 中

1. Smart Recomposition에 도움이 되는 정보 추가하기

  • (위 내용 반복) Composition 내 Composale의 인스턴스는 call site(=호출 사이트)로 식별된다.

 

Case1) 서로 다른 call site에서 Composale을 호출하면,

- 컴포지션에 Composale의 여러 인스턴스가 생성됨 => (문제없음)


Case2) 동일한 call site에서 Composale을 여러 번 호출하면,

- 각 컴포저블 호출을 고유하게 식별할 수 있는 정보가 없으므로 Composale 인스턴스를 구분하기 위해 call site 외에 실행 순서가 사용됨
=> (경우에 따라 문제있음) 

 

Case2-1) Composale 인스턴스를 구분에 실행순서를 사용해도 문제없는 경우

  • 새 movie가 목록의 하단에 추가된 경우,
    => Compose는 인스턴스의 목록 내 위치가 변경되지 않았고 따라서 인스턴스의 movie 입력이 동일하므로 컴포지션에 이미 있는 인스턴스를 재사용할 수 있다.
@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}
 

 

그림 4. 목록의 하단에 새 요소가 추가된 경우 되면 컴포지션 내 MoviesScreen의 표현. 컴포지션의 MovieOverview 컴포저블은 재사용할 수 있습니다.
(
MovieOverview의 색상이 동일하면 컴포저블이 재구성되지 않았음을 의미)

 

 


 

 

Case2-3) Composale 인스턴스를 구분에 실행순서를 사용해도 문제있는 경우

  • 목록의 상단 또는 가운데에 항목을 추가 혹은 삭제하거나 재정렬하여 movies 목록이 변경되는 경우이면서
    && MovieOverview가 부수 효과를 사용하여 영화 이미지를 가져오는 경우일 때,

    => 목록에서 입력 매개변수의 위치가 변경된 모든 MovieOverview 호출에서 리컴포지션이 발생한다.
          이는 예를 들어 MovieOverview가 부수 효과를 사용하여 영화 이미지를 가져오는 경우 매우 중요한데, 효과가 적용되는 동안 리컴포지션이 발생하면 효과가 취소되고 다시 시작된다.
@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}
 

그림 5. 목록에 새 요소가 추가될 때 컴포지션 내 MoviesScreen의 표현 MovieOverview 컴포저블은 재사용할 수 없으며 모든 부수 효과가 다시 시작됩니다.
(
MovieOverview의 색상이 다르면 컴포저블이 재구성되었음을 의미)

 

 

Case2-3-a) 'Case2-3'의 문제 해결하기

(위 동작과는 다르게) 보통 이상적으로는 MovieOverview 인스턴스의 ID는 인스턴스에 전달된 movie의 ID에 연결된 것으로 간주된다.
영화 목록을 재정렬하는 경우 다른 영화 인스턴스로 각 MovieOverview 컴포저블을 재구성하는 대신 기존에 존재하던 컴포지션 트리 내 인스턴스를 재사용하여 재정렬하는 것이 이상적으로 보인다. 

 

👇 기존에 존재하던 컴포지션을 구별하려면? 

 

key 컴포저블

  • key 컴포저블을 사용하면 Compose가 컴포지션에서 컴포저블 인스턴스를 식별할 수 있다.
    = Compose에서 런타임에 트리의 특정 부분(key 컴포저블)을 식별하는 데 사용할 값을 지정
  • 이 기능은 여러 컴포저블이 동일한 호출 사이트에서 호출되고 부수 효과 또는 내부 상태가 포함되어 있을 때 중요
  • 일부 컴포저블에는 key 컴포저블 지원 기능이 내장되어 있다.Ex) LazyColumn의 경우 items DSL에 맞춤 key를 지정할 수 있다.
    @Composable
    fun MoviesScreenLazy(movies: List<Movie>) {
        LazyColumn {
            items(movies, key = { movie -> movie.id }) { movie ->
                MovieOverview(movie)
            }
        }
    }
     
  • key 값은 전체적으로 고유하지 않아도 되며 호출 사이트에서의 컴포저블 호출 간에만 고유하면 된다.
    ㄴ> 따라서 이 예제에선 각 movie에는 movies 사이에 고유한 key가 있어야 합니다.
  • 앱의 다른 위치에 있는 다른 컴포저블과 이 key를 공유하는 것은 가능하다.
  • 적용방식 : 주요 컴포저블 호출로 코드 블록을 래핑하고 하나 이상의 값을 전달하면 이러한 값이 컴포지션에서 인스턴스를 식별하는 데 함께 사용됩니다. 
@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}
 

위와 같이 key를 설정하면 목록의 요소가 변경되더라도 Compose는 개별 MovieOverview 호출을 인식하고 재사용할 수 있다.

그림 6. 목록에 새 요소가 추가될 때, 컴포지션 내 MoviesScreen의 표현 MovieOverview 컴포저블에는 고유 키가 있으므로 Compose가 변경되지 않은 MovieOverview 인스턴스를 인식하고 재사용되며, 인스턴스의 부수 효과는 계속 실행된다.

 
 

 

 

Composition 내 Composale 의 분석 

2. 입력이 변경되지 않은 경우 건너뛰기

  • Recomposition 중에 '입력이 가능한 일부 Composable 함수'는 입력이 이전 컴포지션에서 변경되지 않은 경우 실행을 완전히 건너뛸 수 있다.
  • [ Recomposition과정에서 건너뛸 수 없는 Composable함수 ]
    (이를 제외한 경우 건너뛸 수 있다🏃‍♂️)
    • Unit이 아닌 리턴타입이 있는 함수
    • @NonRestartableComposable 또는 @NonSkippableComposable로 주석 처리된 함수
    • 필수 매개변수가 불안정한 유형인 함수
      ㄴ> 실험용 컴파일러 모드인 강력한 건너뛰기가 있는데 이 모드는 이 요구사항을 완화한다고 한다.
  •  

안정적인 유형
  • @Stable 주석
    Compose가 타입이 안정적이라고 추론할  없지만 안정적인 것으로 간주하도록 하려면, @Stable 주석으로 표시한다.

  • Compose는 증명할 수 있는 경우에만 타입을 안정적인 것으로 간주한다.
    ex) 인터페이스는 일반적으로 안정적이지 않은 것으로 간주되며, 구현을 변경할 수 없으며 변경할 수 있는 공개 속성이 있는 타입도 안정적이지 않다.

   [안정적인 유형의 조건]

  • 두 인스턴스의 equals 결과가 동일한 두 인스턴스의 경우 항상 동일하다.
  • 유형의 공개 속성이 변경되면 컴포지션에 알림이 전송된다.
  • 모든 공개 속성 타입은 안정적이다.
   [ (기본적으로 Compose 컴파일러가 안정적인 것으로 취급하는) 위 조건에 포함되는 중요한 일반 타입들 ]
  • 모든 원시타입 : Boolean, Int, Long, Float, Char 
  • 문자열
  • 모든 함수 타입 (람다)
      => 이 타입은 모두 '변경할 수 없는 타입'이다.
            (
변경할 수 없는 타입은 절대 변경되지 않으므로 컴포지션에 변경사항을 알리지 않아도 되며 따라서 위 조건을 훨씬 더 쉽게 준수할 수 있다.)

'변경불가능한 타입'과 '안정적인 타입'의 관계

   - 대부분 변경 불가능한 모든 타입은 안전하게 안정적인 타입으로 간주할 수 있다.
   - 하지만, 안정적이지만 변경가능한 한 가지 중요한 타입은 Compose의 "MutableState 타입"이다.

 

 

Compose의 "MutableState 타입"

  • 안정적이지만 변경가능한중요한 타입 (대부분 안정적인 타입은 변경불가능한 타입들)
  • State의 `.value 속성`이 변경되면 Compose에 알림이 전송되므로 상태 객체는 전체적으로 안정적인 것으로 간주된다.

 

컴포저블에 매개변수로 전달된 모든 타입이 안정적인 경우,

UI 트리 내 컴포저블 위치를 기반으로 매개변수 값이 동일한지 비교하고, 이전 호출 이후 모든 값이 변경되지 않은 경우 리컴포지션을 건너뛴다.

위 비교에서는 equals 메서드를 사용한다.

 

 

@Stable 어노테이션

  • Compose가 타입이 안정적이라고 추론할 수 없지만 안정적인 것으로 간주하도록 명시하는 어노테이션
  • @Stable 주석으로 표시한다.
  • 유형의 안정성을 추론할 수 없다면 Compose가 스마트 재구성을 선호하도록 유형에 @Stable을 명시한다.
  • ex) 
// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}
 

ㄴ> 위의 코드 스니펫에서는 UiState가 인터페이스이므로 Compose가 일반적으로 이 유형을 안정적이지 않은 것으로 간주할 수 있지만 @Stable 주석을 추가하였으므로 Compose가 이 유형이 안정적임을 알게 되고 스마트 리컴포지션을 선호하게 된다.
즉 이 인터페이스가 매개변수 유형으로 사용되는 경우 Compose가 모든 구현을 안정적인 것으로 간주한다.

 

 

 

 

 

 

 

 

 

 

출처


반응형