Android

[Jetpack Compose] Composable 수명 주기

도우 2024. 9. 9. 18:16
728x90

컴포저블(Composable)의 수명주기는 Jetpack Compose에서 UI의 상태 변화와 관련된 중요한 개념이다. 컴포저블은 UI의 구성 요소로서 상태가 변경될 때 재구성(Recomposition) 과정을 통해 UI를 갱신하게 되며, 이를 통해 효율적으로 상태 변화를 처리한다.

1. 컴포지션(Composition) 이란?

컴포지션은 컴포저블을 실행하여 앱의 UI를 설명하는 과정이다. 앱의 상태에 따라 적절한 UI가 구성되며, 이를 컴포지션 트리(Composable Tree)라고 한다. 즉, UI를 구성하는 각 컴포저블이 트리 구조로 표현된다.

초기 컴포지션 : 앱이 처음 시작될 때, 컴포저블이 호출되어 UI가 초기화 된다.

리컴포지션 : 앱의 상태(State 객체 등)가 변경될 때 Jetpack Compose가 상태 변화에 반응하여 필요한 부분만 다시 컴포저블을 호출하여 UI를 갱신하는 과정이다.

 

2. 컴포저블 수명 주기 단계

컴포저블의 수명 주기는 아래 3단계로 나눌 수 있다.

(1) 컴포지션 시작 : 컴포저블이 처음 호출되어 UI를 그리는 과정이다.

(2) 0회 이상의 재구성(Recomposition) : 컴포저블이 상태 변화에 따라 재호출되어 UI를 갱신하는 과정

(3) 컴포지션 종료 : 컴포저블이 더 이상 필요하지 않으면 컴포지션 트리에서 제거되어 컴포지션이 종료된다.

컴포저블의 수명 주기에서 가장 중요한 점은, 재구성(Recomposition)은 컴포지션을 수정하는 유일한 방법이라는 것이다. UI를 수정할 수 있는 방법은 상태 변경을 통해 재구성을 트리거하는 것이다.

 

3. 리컴포지션의 작동 방식

리컴포지션은 State<T> 객체의 변경에 의해 트리거된다. Compose는 컴포지션 트리에서 상태를 읽는 컴포저블을 추적하고, 상태가 변경되면 해당 컴포저블을 다시 실행하여 UI를 갱신한다.

상태가 변경되지 않은 컴포저블은 리컴포지션에서 건너뛸 수 있으며, 이를 통해 불필요한 UI 갱신을 방지하고 성능을 최적화한다.

 

4. 컴포저블 인스턴스 및 호출 사이트

각 컴포저블 호출은 호출 사이트(Call Site)에 의해 고유하게 식별된다. 동일한 컴포저블을 여러 번 호출하는 경우, 호출된 위치에 따라 각 인스턴스가 구분된다. 이는 컴포지션에서 중복 호출을 방지하고, 상태가 변경되지 않은 컴포저블을 재사용할 수 있게 돕는다.

호출 사이트란 컴포저블이 호출되는 소스 코드 위치이다. 호출 사이트는 컴포지션 내 위치와 UI 트리에 영향을 미친다. 리컴포지션 시 컴포저블이 이전 컴포지션 시 호출한 것과 다른 컴포저블을 호출하는 경우 Compose는 호출되거나 호출되지 않은 컴포저블을 식별하여 두 컴포지션 모두에서 호출된 컴포저블의 경우 입력이 변경되지 않은 경우 재구성하지 않는다.

 

@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() { /* ... */ }

위 코드를 살펴보자. LoginError()가 발생해서 LoginScreen이 재구성되더라도 LoginInput은 재사용된다. 리컴포지션이 발생할 때 Compose는 입력값이 변경되지 않은 컴포저블은 건너뛸 수 있는 스마트 리컴포지션 방식을 사용하기 때문이다. 위 코드에서 showError 플래그가 변경되어 LoginError()가 조건부로 추가되더라도, LoginInput()은 상태나 입력값이 변하지 않았기 때문에 다시 호출되지 않고, 기존 인스턴스를 재사용한다. 

동일한 호출 사이트에서 컴포저블을 여러 번 호출하는 경우에는 Compose가 각 컴포저블 호출을 고유하게 식별할 수 있는 정보가 없으므로 인스턴스를 구분하기 위해 호출 사이트 외에 실행 순서를 적용하기도 한다. 이 동작만 필요한 경우도 있지만 경우에 따라 원치 않는 동작이 발생할 수도 있다. 

특히나 리스트나 반복되는 데이터가 있을 때, 데이터의 순서가 변경되면 원치 않는 리컴포지션이 발생할 수 있다. 이를 방지하기 위해서는 Key를 사용하여 각 컴포저블을 고유하게 식별하는 방법이 필요하다.

 

 

5. 부수 효과(Side Effects)와 키(Key) 사용

@Composable
fun MovieListScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // MovieOverview는 실행 순서에 의존하여 각 컴포저블이 구분됩니다.
            MovieOverview(movie)
        }
    }
}

이 코드는 영화 목록을 나열하는 간단한 예시이다. 위 코드에서는 실행 순서에 의존하면서 각 영화의 컴포저블을 구분하도록 구현되어 있다. movies 리스트가 변경되어 영화가 추가되거나 순서가 바뀌면, 이전에 있던 컴포저블 인스턴스의 순서가 변하면서 모든 영화에 대해 리컴포지션이 발생할 수 있다. 이는 부수 효과(Side Effect)를 사용하는 경우 예상치 못한 재시작이 발생할 수 있다.

※ 부수 효과는 다음 글 참고

https://ehdnsdlek.tistory.com/48

 

 

 

@Composable
fun MovieListScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // movie.id를 key로 사용하여 각 MovieOverview 컴포저블을 고유하게 식별
            key(movie.id) {
                MovieOverview(movie)
            }
        }
    }
}

위와 같이 key를 사용하면, 리스트에서 영화가 추가되거나 순서가 바뀌더라도 Compose는 아이디 key값을 통해서 각 컴포저블을 고유하게 식별하여, 변경되지 않은 컴포저블을 재사용할 수 있다. 이를 통해 불필요한 리컴포지션을 방지하고 성능을 최적화할 수 있다.

 

6. 리컴포지션 최적화

리컴포지션은 성능을 최적화하는 방식으로 설계되어 있다. 모든 입력이 동일하고 변경되지 않은 경우 Compose는 해당 컴포저블의 리컴포지션을 건너뛴다. 이를 통해 불필요한 UI 갱신을 방지하고 효율적인 UI 갱신이 가능하다

컴포저블이 리컴포지션 중에 실행을 건너뛸 수 있으려면 해당 컴포저블의 모든 입력이 안정적(Stable)이어야 한다. 이때 안정적인 유형(Stable type)은 다음 조건을 충족해야 한다.

(1) 두 인스턴스의 equals 결과가 항상 동일해야 한다. 즉, 동일한 두 인스턴스에 대한 equals 비교가 항상 같은 결과를 반환해야 한다.

(2) 해당 유형의 공개 속성이 변경될 경우 Compose는 이를 인지하고 알릴 수 있어야 한다.

(3) 해당 유형의 모든 공개 속성도 안정적인 유형이어야 한다.

 

안정적인 유형 (Stable Types)

Compose는 일부 일반적인 유형을 안정적(stable)로 간주한다. 이러한 유형은 변경되지 않으므로, 상태가 변하지 않았다고 안전하게 판단하고 리컴포지션을 건너뛸 수 있다. 다음 유형들은 Compose에서 안정적으로 처리된다.

- 모든 원시값(Primitive Value Types) : Boolean, Int, Long, Float, Char 등

- 문자열 (String)

- 모든 함수 타입 (Function Types) : 람다(lambda)를 포함한 함수 타입

- Compose의 MutableState : 이 객체는 상태가 변경되면 Compose가 이를 인지하도록 설계되어 있으며, 내부 상태가 변경될 때 자동으로 Compose에 알린다.

이러한 유형들은 변경 불가능한(Immutable) 특성을 가지므로, 상태가 변하지 않았다고 간주할 수 있다. 따라서, 이들은 리컴포지션 시 불필요하게 다시 실행되지 않으며, 성능을 최적화할 수 있다.

 

건너뛸 수 없는 경우

다음과 같은 경우, 컴포저블은 리컴포지션 시 건너뛰기 대상에서 제외된다.

- 컴포저블 함수가 Unit이 아닌 반환 타입을 가질 때

- 함수에 @NonRestartableComposable 또는 @NonSkippableComposable 주석이 있을 때

- 필수 매개변수가 안정적이지 않은(non-stable) 유형일 때

Compose는 기본적으로 인터페이스나 변경 가능한 속성을 가진 유형을 안정적이지 않다라고 간주한다. 그러나 @Stable 주석을 사용하여 안정적인 타입으로 표시할 수 있으며, 이를 통해 Compose는 더 스마트하게 리컴포지션을 건너뛸 수 있다.

 

 

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

 

컴포저블 수명 주기  |  Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 컴포저블 수명 주기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 페이지에서는 컴포저블의 수명

developer.android.com

 

 

728x90