컴포저블의 수명 주기는 크게 Composition , Recomposition , Exit 의 세 단계로 나눌 수 있습니다. 이 단계들은 컴포저블이 어떻게 생성되고, 업데이트되며, 종료되는지를 설명합니다.
이러한 수명 주기를 이해하면, 앱의 성능을 최적화하고 불필요한 리소스 소모를 줄일 수 있습니다.

컴포저블이 그려지는 단계는 다음과 같은 과정을 포함합니다:

각 단계들은 기본적으로 모든 frame 에서 작동합니다.
하지만, Compose 는 UI 를 업데이트할 때 필요한 최소한의 작업만 실행해서 더 좋은 퍼포먼스를 유지합니다.
State 를 추적하고 있어 State 의 변경 반응해 필요한 UI 업데이트만 진행합니다.이때 State 가 생성되고 저장되는 위치는 관계가 없으며, State 를 읽는 시점과 위치에 따라서만 달라집니다.
무슨 뜻이냐 하면,
@Composable
fun ParentComposable() {
var padding by remember { mutableStateOf(8.dp) }
Text(text = "Parent")
ChildComposable(padding)
}
@Composable
fun ChildComposable(padding: Dp) {
Text(
modifier = Modifier.padding(padding),
text = "Child"
)
}
위 코드에서 State 자체는 ParentComposable 에 선언되어 있습니다. 하지만 실질적으로 사용되고 읽는 위치는 ChildComposable 입니다.
이 때, padding 이라는 State 가 변경되면 Recomposition 이 이뤄지는 컴포저블은 ParentComposable 이 아닌 ChildComposable 입니다.
Composition
Compose Runtime 은 @Composable 을 실행하고 UI 를 나타내는 트리 구조를 생성합니다. 이 트리 구조를 Composition 이라고도 합니다.
아래 사진처럼 각 @Composable 은 UI 트리에서 위치와 상위, 하위 컴포저블에 따라서 각 노드에 매핑됩니다.

Composition 결과에 따라서 Compose 는 Layout 단계와 Drawing 결과를 실행합니다. 만약 컴포저블 안의 내용, 크기, 레이아웃이 변경되지 않으면 Layout 단계와 Drawing 단계를 스킵합니다.
Layout
Compose 는 Composition 단계에서 생성된 UI 트리를 Layout 단계의 입력으로 사용합니다. 각 layout node 는 아래의 3단계를 거쳐서 UI 요소의 크기(Measurement)와 위치(Placement)를 결정합니다.
이 단계가 끝나면 각 layout node 에는 다음의 정보들을 포함합니다.
Layout 컴포저블에 전달된 측정 정보와, LayoutModifier 인터페이스의 MeasureScope.measure 함수를 사용해서 크기를 측정합니다.@Suppress("NOTHING_TO_INLINE")
@Composable
@UiComposable
inline fun Layout(
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
)
@JvmDefaultWithCompatibility
interface LayoutModifier : Modifier.Element {
fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult
}layout 함수의 블록과, Modifier.offset { } 람다 블록의 값을 통해 위치 정보를 설정합니다.fun layout(
width: Int,
height: Int,
alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult
var offsetX by remember { mutableStateOf(8.dp) }
Text(
text = "Hello",
modifier = Modifier.offset {
// The `offsetX` state is read in the placement step
// of the layout phase when the offset is calculated.
// Changes in `offsetX` restart the layout.
IntOffset(offsetX.roundToPx(), 0)
}
) Drawing
Compose 는 이 단계에서 UI 트리를 Top → Bottom 으로 각 노드를 방문합니다.
Layout 단계에서 결정된 노드에 있는 정보(크기, 위치)를 바탕으로 Screen(Canvas) 에 모든 layout node 에 대한 UI 요소를 그리는 작업을 진행합니다.
Canvas() , Modifier.drawBehind , Modifier.drawWithContent 같은 함수에서 State 값이 변경되었을 경우에는, 오직 Drawing 단계만 재실행합니다.
var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = modifier) {
// The `color` state is read in the drawing phase
// when the canvas is rendered.
// Changes in `color` restart the drawing.
drawRect(color)
}

Composition (UI 트리) 안에 있는 컴포저블의 인스턴스는 Call Site 라는 것으로 식별됩니다.
Call Site:
@Composable이 호출되는 코드 상 위치. Composition 위치와 UI 트리에 영향을 미칩니다.
하나의 컴포저블 함수가 여러 곳에서 호출되었다면, 각각의 컴포저블이 호출된 소스 코드 상 위치가 다르므로, 하나의 Composable 이 여러 개의 Call Site 를 가질 수 있고 모든 Call Site 는 Unique 한 값을 가집니다.
Recomposition 은 상태가 변경되었을 때 컴포저블이 다시 그려지는 과정입니다. Recomposition 시 이전 Composition 과 다른 컴포저블이 호출되었는 지, 입력이 변경되었는 지에 따라 필요한 경우에만 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() { /* ... */ }
위 코드에서 불린 타입의 showError 값이 변경됨에 따라서 LoginError() 컴포저블이 호출됩니다. 하지만 LoginInput() 컴포저블 자체는 State 나 조건문에 영향을 받지 않습니다. 따라서 초기 Composition 시 UI Tree 를 구성할 때 처음 호출됩니다. showError 값이 변경되어서 Recomposition 이 일어나 LoginError() 라는 컴포저블이 호출되었더라도 LoginInput() 자체는 변경된 매개변수값이 없기 때문에 Compose 가 LoginInput() 을 호출하지 않습니다.
Key 값을 사용해 Recomposition 을 줄이기
Recomposition 은 화면 UI 요소를 다시 렌더링합니다. UI 요소를 렌더링하는 작업은 비용이 많은 작업이고, 이게 무차별적으로 반복된다면 앱의 퍼포먼스가 현저히 떨어질 수 있습니다. 따라서 다양한 방법으로 Recomposition 횟수를 줄이는 게 앱의 성능을 향상시킨다고 볼 수 있습니다.

@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)
}
}
}
이 경우에는 Recomposition 후에도 MovieOverview 의 인스턴스가 같은 Call site 를 가지고 for loop 를 통해서 순서대로 호출하기 때문에 동일한 인스턴스가 유지됩니다.

@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)
/* ... */
}
}
하지만 만약 MovieOverview 컴포저블이 내부에 비동기 작업을 처리해서 for loop 가 돌아가지만 순서대로 UI 트리가 구성되지 않습니다. 따라서 계속해서 for loop 이 돌아가서 새로운 MovieOverview 가 추가된다면, 전체 MovieOverview 인스턴스가 새로이 추가됨을 볼 수 있습니다. (순서가 바뀌어 다른 객체로 인식하기 때문)
key 사용 : 리스트와 같은 동적 UI 요소에서는 key를 사용하여 변경된 요소만 Recomposition 하도록 할 수 있습니다. 이를 통해 성능을 크게 향상시킬 수 있습니다.remember 를 사용해 State를 저장하는 컴포저블을 Stateful 하다고 정의하고, 반대로 State 를 갖지 않는 컴포저블 같은 경우에는 Stateless 하다고 합니다.
Stateful 한 컴포저블은 내부적으로 상태를 스스로 제어, 보존, 수정을 합니다. 호출하는 상위 컴포저블이 내부 상태를 제어할 필요가 없고 State 를 직접 관리하지 않아도 되는 경우에 유용합니다.
하지만 내부적으로 State 를 가지는 컴포저블은 Stateless 한 컴포저블에 비해서 대부분 재사용 가능성이 적고 테스트하기가 더 어려운 경향이 있습니다.
상태 호이스팅(State Hoisting) 은 State 를 상위의 컴포저블로 끌어올리는(이동시키는) 것을 의미합니다.
그렇게 함으로써 하위 컴포저블을 Stateless 하게 만들어서 재사용성을 높입니다.
하위 컴포넌트는 상태를 직접 관리하지 않고, 상위 컴포넌트에서 전달받은 State 를 사용합니다. 이렇게 해서 State 를 부모에서 관리해서 State 관리의 일관성을 유지할 수 있고 가독성과 유지보수성을 높일 수 있습니다.
사용법
다음 두 가지를 컴포저블의 파라미터로 받습니다.
value: T : State 였던 값, 그냥 T 형태로 값을 전달받습니다.onValueChanged: (T) → Unit : T 값이 하위 컴포저블에 의해서 변경되어야 할 경우, 그러한 기능을 외부 호출자로부터 전달받습니다.위 두 가지의 경우에 필요에 따라 변수명을 변경하고 다양하게 활용할 수 있습니다.
@Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }
HelloContent(name = name, onNameChange = { name = it })
}
@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column() {
Text(text = "Hello, $name")
OutlinedTextField(
value = name,
onValueChange = onNameChange, label = { Text("Name") })
}
}

위 처럼 상위 컴포저블인 HelloScreen 을 Stateful 하게 만들고, State 를 상위에서 만들어 관리하고, 하위 컴포저블인 HelloContent 를 Stateless 하게 만듭니다.