반응형
Notice
Recent Posts
Recent Comments
Link
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

컴포저블 수명 주기 본문

카테고리 없음

컴포저블 수명 주기

어리둥절범고래 2025. 2. 9. 02:23
반응형

Jetpack Compose의 컴포저블(@Composable) 함수는 기존의 Android 생명주기(onCreate(), onResume() 등)와는 다르게 동작합니다. XML 기반 UI 시스템에서는 Activity나 Fragment의 생명주기를 따르지만, Compose에서는 컴포저블 함수 자체가 UI를 동적으로 관리합니다.

 

컴포지션 내 컴포저블의 수명 주기. 컴포저블은 컴포지션을 시작하고 0회 이상 재구성되고 컴포지션을 종료합니다.

 

컴포지션은 컴포저블 함수들이 트리 구조로 UI를 구성하는 과정입니다.

Jetpack Compose는 초기 컴포지션에서 컴포저블을 실행하고 UI 구성을 추적합니다.
이후 앱 상태가 변경되면 리컴포지션(Recomposition)을 통해 필요한 컴포저블만 다시 실행하고 UI를 업데이트합니다.
컴포지션은 초기 컴포지션에서 생성되며, 리컴포지션을 통해서만 변경될 수 있습니다.

 

리컴포지션은 일반적으로 State<T> 객체가 변경되면 트리거됩니다. Compose는 이러한 객체를 추적하고 컴포지션에서 특정 State<T>를 읽는 모든 컴포저블 및 호출하는 컴포저블 중 건너뛸 수 없는 모든 컴포저블을 실행합니다.

 

 

 

Jetpack Compose에서는 컴포저블 함수가 호출될 때마다 새로운 인스턴스가 생성되어 컴포지션(Composition)에 배치됩니다.
즉, 같은 컴포저블 함수라도 여러 번 호출되면 각 호출마다 개별적인 UI 요소가 생성되며, 각각 독립적인 생명주기를 가집니다.

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

색상이 다른 요소는 요소가 별도의 인스턴스

 

 

 

컴포지션 내 컴포저블의 분석

 

컴포지션 내 컴포저블의 인스턴스는 호출 사이트로 식별됩니다. Compose 컴파일러는 각 호출 사이트를 고유한 것으로 간주합니다. 여러 호출 사이트에서 컴포저블을 호출하면 컴포지션에 컴포저블의 여러 인스턴스가 생성됩니다.

여기서 호출 사이트는 컴포저블이 호출되는 소스 코드 위치입니다. 호출 사이트는 컴포지션 내 위치와 UI 트리에 영향을 미칩니다.

 

리컴포지션 시, Compose는 새롭게 호출된 컴포저블과 기존 컴포저블을 비교하여 입력이 변경되지 않은 경우 재구성(Recomposition)하지 않습니다.

@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 의 표현. 색상이 동일하면 재구성되지 않았음을 의미

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

 

또한, 부수 효과(Side Effect)를 다룰 때는 컴포저블의 ID를 유지하여 불필요한 재시작을 방지하는 것이 중요합니다.

 

여기서 ID는 컴포저블의 부수 효과(Side Effect)가 유지될지, 새로 실행될지를 결정하는 키 값 정도로 이해하면 되겠습니다.

@Composable
fun TimerScreen() {
    var time by remember { mutableStateOf(0) }

    LaunchedEffect(Unit) { 
        while (true) {
            delay(1000)
            time++
        }
    }

    Text("Time: $time")
}

 

LaunchedEffect(Unit)은 컴포저블이 처음 실행될 때만 실행됩니다.
하지만 만약 Unit이 아닌 변경되는 값(time 등)을 키로 사용하면, 리컴포지션마다 타이머가 새로 시작될 수 있습니다.

@Composable
fun TimerScreen(userId: String) {
    var time by remember { mutableStateOf(0) }

    LaunchedEffect(userId) {  // userId가 변경되지 않는 한, 기존 효과 유지
        while (true) {
            delay(1000)
            time++
        }
    }

    Text("User: $userId, Time: $time")
}

처음 userId = "123"이면 LaunchedEffect("123")이 실행됩니다.
리컴포지션이 발생해도 userId가 그대로면 기존 타이머 유지되며, userId가 "456"으로 변경되면, 기존 타이머는 종료되고 새로운 타이머 시작합니다.

 

 

스마트 리컴포지션

 

동일한 위치(호출 사이트)에서 여러 번 호출될 경우, Compose는 개별 인스턴스를 구분할 정보가 부족하게 됩니다.
이때, 실행 순서를 기준으로 각 인스턴스를 구분합니다.
하지만 실행 순서만으로 구분하면 UI 변화에 따라 예기치 않은 동작이 발생할 수도 있습니다.

@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)
        }
    }
}

위의 예에서 Compose는 호출 사이트 외에 실행 순서를 사용하여 컴포지션에서 인스턴스를 구분합니다. 새 movie가 목록의 하단에 추가된 경우 Compose는 인스턴스의 목록 내 위치가 변경되지 않았고 따라서 인스턴스의 movie 입력이 동일하므로 컴포지션에 이미 있는 인스턴스를 재사용할 수 있습니다.

MovieOverview 의 색상이 동일하면 컴포저블이 재구성되지 않았음을 의미

하지만 항목을 추가하거나 항목을 삭제하거나 재정렬하여 movies 목록이 변경되면 목록에서 입력 매개변수의 위치가 변경된 모든 MovieOverview 호출에서 리컴포지션이 발생합니다.

MovieOverview의 색상이 다르면 컴포저블이 재구성되었음을 의미

이상적으로, MovieOverview 인스턴스의 ID는 해당 movie의 ID와 연결되어야 합니다. 영화 목록이 재정렬될 때, 기존 인스턴스를 유지하면서 순서만 변경하는 것이 더 효율적입니다. 이를 위해 Compose에서는 key를 사용해 런타임에 특정 트리 요소를 식별할 수 있습니다.

컴포저블 호출을 key 블록으로 감싸고 값을 전달하면, 컴포지션에서 인스턴스를 구분하는 기준이 됩니다. 단, key 값은 전체적으로 고유할 필요 없이 같은 호출 사이트 내에서만 고유하면 됩니다.

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

 

고유 키가 있으므로 Compose가 변경되지 않은  MovieOverview  인스턴스를 인식하고 재사용

@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

일부 컴포저블에는 key 컴포저블 지원 기능이 내장되어 있습니다.

 

 

 

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

 

리컴포지션 중 입력이 변경되지 않으면 일부 컴포저블 함수는 실행을 건너뛸 수 있습니다. 하지만 다음과 같은 경우 건너뛰지 않습니다.

  • 반환 타입이 Unit이 아님
  • @NonRestartableComposable 또는 @NonSkippableComposable로 주석 처리됨
  • 필수 매개변수가 안정적이지 않은 유형

 

안정적인(Stable) 유형은 다음 조건을 만족해야 합니다.

  • equals 결과가 항상 일정함
  • 공개 속성이 변경되면 컴포지션에 알림이 전송됨
  • 모든 공개 속성도 안정적인 유형임

 

Compose는 아래 유형을 자동으로 안정적인 것으로 간주합니다.

  • 원시 타입 (Boolean, Int, Float 등)
  • String
  • 함수 타입(람다)

 

이들은 변경할 수 없는(Immutable) 타입이므로 컴포지션 변경을 알릴 필요가 없습니다.

 

Compose에서 MutableState는 안정적이지만 변경 가능한 중요한 유형입니다. MutableState의 .value 속성이 변경되면 Compose에 알림이 전송되므로 상태 객체는 안정적인 것으로 간주됩니다.

컴포저블에 전달된 매개변수가 모두 안정적이면, 값 비교는 UI 트리 내 위치를 기준으로 진행되며, 이전 호출 이후 값이 변경되지 않으면 리컴포지션을 건너뜁니다.

모든 입력이 안정적이고 변경되지 않으면 Compose는 리컴포지션을 건너뛰며, 비교는 equals 메서드를 사용하여 진행됩니다.

인터페이스와 같이 구현을 변경할 수 있는 공개 속성이 있는 유형은 일반적으로 안정적이지 않습니다.
안정적인 유형으로 간주하려면 @Stable 주석을 사용해야 합니다.

// 타입을 안정적(Stable)로 표시하여 리컴포지션을 건너뛰고 스마트한 재구성을 유도합니다.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

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

 

 

 

 

 

https://developer.android.com/develop/ui/compose/lifecycle?hl=ko#add-info-smart-recomposition

반응형