본문 바로가기

Compose

[Compose] Composable 수명 주기

Compose 공식 문서를 참고해 번역한 내용입니다.

 

 

Composable의 수명주기를 알기 위해서는 Composition에 대해 알아야 한다

 

Composition은 UI를 구성하는 기본적인 개념이며 Composable UI Tree 구조

 

 

Composition은 초기 Composition을 통해서만 생성되고 Recomposition을 통해서만 업데이트 될 수 있다

 

 

Initial Composition (초기 컴포지션)

  • 처음 Composable 함수가 호출될 때 발생
  • UI 트리를 처음으로 생성
  • 하위 Composable 실행

Recomposition

  • 상태(State<T>)가 변경될 때 트리거되고 상태를 추적해 State<T>를 읽는 모든 Composable 중 skip할 수 없는 모든 Composable 실행
  • 효율적으로 변경된 부분만 업데이트
  • Smart Recomposition을 통해 필요한 부분만 재구성

 

Composition 내 Composable Lifecycle은 아래와 같다

 

Composable Lifecycle

 

 

1. Enter the Composition

  • Composable이 UI 트리에 진입하는 시점
  • 초기화 작업 수행

 

2. Recompose 0 or more times

  • 실제 UI가 그려지고 업데이트
  • 상태 변경에 따라 0번 이상의 재구성이 발생할 수 있다
  • Smart recomposition으로 필요한 부분만 효율적으로 업데이트

 

3. Leave the composition

  • Composable이 UI 트리에서 제거되는 시점
  • 리소스 정리 작업 수행
  • DisposableEffect 등으로 cleanup 처리

 

Composition 내 UI 트리

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

 

Composition 마다 자체 Lifecycle을 가지고 있다

 

 

 

Composition 내 Composable 분석

Composition 내부에서 Composable 인스턴스는 Call Site(호출 사이트)로 식별된다

Call site는 Composable이 호출되는 소스 코드의 위치

 

 

Compose 컴파일러는 각 call site를 고유한 것으로 간주한

여러 call site에서 Composable을 호출하면 Composition에 여러 Composable 인스턴스가 생성된다

 

또한 Recomposition시 Compose는 호출된 / 호출되지 않은 Composable을 구별하고

모두 호출된 Composable의 경우 input이 변경되지 않았다면 재구성하지 않는다

 

예시) LoginScreen

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput()
}

@Composable
fun LoginInput(modifier: Modifier = Modifier) { ... }

@Composable
fun LoginError(modifier: Modifier = Modifier) { ... }

 

LoginInput은 재구성되지 않는다

(같은 call site, 변경되지 않은 input)

Composition

 

 

Smart Recomposition을 위한 정보

Composable 함수를 여러 번 호출하면 Composition에 여러 번 추가

만약 동일한 call site에서 함수를 여러 번 호출하면 컴파일러가 함수들을 식별할 수 있는 정보가 없어, 구분하기 위해 실행 순서를 사용

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            MovieOverview(movie)
        }
    }
}

 

실행 순서를 사용해 하단에 추가

MovieScreen

 

하지만 List 맨 위나 중간에 데이터를 추가한다면 모든 곳에서 Recomposition 발생

이를 방지하기 위해 Compose에서 Runtime에 Tree에 특정 부분(key)을 식별하는 데 사용할 값을 지정할 수 있다

 

key를 사용한 Composable 함수

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) {
                MovieOverView(movie)
            }
        }
    }
}

 

 

key Composable

@Composable
inline fun <T> key(
    @Suppress("UNUSED_PARAMETER")
    vararg keys: Any?,
    block: @Composable () -> T
) = block()

 

key값은 전체적으로 고유하지 않아도 되며 call site에서의 Composable 호출 사이에서만 고유하면 된다

 

 

 

Input이 변경되지 않았을 경우 Skip

Recomposition 중 조건을 충족하는 Composable의 경우 완전히 Skip 할 수 있다

 

Skip할 수 없는 경우

  • 함수의 Return Type이 Unit이 아닌 경우
  • 함수에 @NonRestartableComposable , @NonSkippableComposable 주석 처리된 경우
  • 필수 매개변수가 non-stable 타입일 경우

Type이 Stable로 간주되기 위한 조건

  • 인스턴스의 equals가 같아야 한다
  • type의 public property가 변경되면 Composition이 알 수 있어야 한다
  • 모든 public property 타입은 stable하다
// Unstable 타입
class UnstableUser {
    var name = "Park"    // 외부 접근 가능한 public property
    var age = 31         // 외부 접근 가능한 public property
    
    fun updateAge() {
        age += 1  // 변경을 Compose가 감지할 수 없음
    }
}

// Stable 타입 - State 사용
class StableUser {
    var name by mutableStateOf("Park") // State로 관리되는 public property
    var age by mutableStateOf(31)      // State로 관리되는 public property
  
    fun updateAge() {
        age += 1  // Compose가 변경을 감지하고 recomposition
    }
}

// Stable 타입 - immutable data class 사용
data class StableUser2(
    val name: String,  // immutable public property
    val age: Int       // immutable public property
)

// 사용 예시
// unstable한 UnstableUser
@Composable
fun User(user: UnstableUser) {  
    Text(user.name)  // name 변경 감지 X
    Text(user.age.toString())
}

// stable한 StableUser
@Composable
fun UserProfile(user: StableUser) {
    Text(user.name)  // name이 변경되면 recomposition
    Text(user.age.toString())
}

// stable한 StableUser2
@Composable
fun UserProfile(user: StableUser2) {
    Text(user.name)  
    Text(user.age.toString())
}

 

 

@Stable 어노테이션을 사용하지 않아도 Compiler가 안정적인 것으로 간주하는 Type들이 있다

(이 Type들은 모두 immutable하기 때문에)

  • 모든 Primitive types - Boolean, Int, Long, Float, Char, etc
  • Strings
  • 모든 함수 타입 (lambdas)

 

Compose의 MutableState는 mutable하지만 Compose에서 stable로 간주한다

MutableState는 value가 변경될 때 자동으로 Compose에 알림이 전송되어 Stable 조건을 만족한다

 

 

@Stable 어노테이션을 통해 unstable한 상태를 컴파일러가 stable 하도록 만들 수 있다

인터페이스가 안정적이라고 Compose 컴파일러에게 알리고 value나 exception이 변경될 때 Compose가 이를 감지

smart recomposition과 skip 가능

 

// 일반적으로 interface는 unstable
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

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

'Compose' 카테고리의 다른 글

[Compose] Compose Phases  (0) 2025.02.09
[Compose] Compose 이해하기  (0) 2025.02.08