Recomposition은 Jetpack Compose에서 UI를 다시 그리는 과정입니다.
화면에 보여주는 데이터가 변경되면 해당 데이터를 사용하는 Composable 함수만 자동으로 다시 호출되어 변경된 내용을 반영합니다.
쉽게 말해, 데이터가 바뀌면 필요한 부분의 UI만 업데이트된다.
이로 인해 성능이 좋아지고, UI 갱신 로직을 따로 작성하지 않아도 된다.
컴포지션 내 컴포저블의 수명 주기. 컴포저블은 컴포지션을 시작하고 0회 이상 재구성되고 컴포지션을 종료합니다.
State<T>
가 변경되면 리컴포지션이 트리거 됨State<T>
를 읽는 모든 컴포저블 및 호출하는 컴포저블 중 건너뛸 수 없는 모든 컴포저블을 실행
맞아! 컴포지션은 컴포저블 함수를 실행하여 UI를 그리는 과정이야.
구체적으로 나누면
컴포지션의 본질은 "컴포저블을 실행한다"
앞에서 입이 닳도록 반복했지만 컴포지션은 컴포저블 함수를 실행하여 UI를 구성하는 작업이야. 컴포저블 함수는 상태와 데이터에 기반하여 "어떤 UI를 보여줄지" 선언하는 함수야. 컴포지션이 발생할 때마다 컴포저블 함수는 실행되고, Compose는 UI를 최신 상태로 유지해.
컴포지션 내 컴포저블의 인스턴스는 호출 사이트로 식별돼. Compose 컴파일러는 각 호출 사이트를 고유한 것으로 간주하거든.
호출 사이트는 컴포저블이 호출되는 소스 코드 위치야. 호출 사이트는 컴포지션 내 위치와 UI 트리에 영향을 미치니까 잘 기억해 둬.
더 이해하기 쉽게 안드로이드 공식 문서에 나와 있는 코드와 다이어그램을 예시로 설명해줄게.
@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
은 두 번 연달아 호출되었어. 그렇지만 고유한 호출 사이트로 리컴포지션 되지 않았어.
그 경우에 사용하는 게 바로 "스마트 리컴포지션"이야.
Compose가 각 컴포저블 호출을 고유하게 식별할 수 있는 정보가 없으므로 인스턴스를 구분하기 위해 호출 사이트 외에 실행 순서가 사용돼. 이 동작만 필요한 경우도 있지만 경우에 따라 원치 않는 동작이 발생할 수도 있으니 주의해서 사용해야 해.
설명만으로는 이해가 안 될 것 같아서 예시를 보여줄게!
@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)
}
}
}
for문을 통해 movies에 있는 movie를 다 털어낼 때까지 MovieOverview(movie)가 반복될 거야. 그럼 어찌되었든 같은 컴포저블이 여러번 반복해서 호출되겠지.
목록의 하단에 새 요소가 추가된 경우 되면 컴포지션 내 MoviesScreen의 표현. 컴포지션의 MovieOverview 컴포저블은 재사용할 수 있습니다. MovieOverview의 색상이 동일하면 컴포저블이 재구성되지 않았음을 의미합니다.
위의 예에서 Compose는 호출 사이트 외에 실행 순서를 사용하여 컴포지션에서 인스턴스를 구분했다고 치자. 새 movie가 목록의 하단에 추가된 경우 Compose는 인스턴스의 목록 내 위치가 변경되지 않았고 따라서 인스턴스의 movie 입력이 동일하므로 컴포지션에 이미 있는 인스턴스를 재사용할 수 있어.
하지만 목록의 상단 또는 가운데에 항목을 추가하거나 항목을 삭제하거나 재정렬하여 movies 목록이 변경되면 목록에서 입력 매개변수의 위치가 변경된 모든 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)
/* ... */
}
}
목록에 새 요소가 추가될 때 컴포지션 내 MoviesScreen의 표현 MovieOverview 컴포저블은 재사용할 수 없으며 모든 부수 효과가 다시 시작됩니다. MovieOverview의 색상이 다르면 컴포저블이 재구성되었음을 의미합니다.
위 다이어그램처럼 다시 재정렬되는 걸 막기 위해선 각 movie에는 movies 사이에 고유한 key가 있어야 해.
@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
Column {
for (movie in movies) {
key(movie.id) { // Unique ID for this movie
MovieOverview(movie)
}
}
}
}
목록에 새 요소가 추가될 때 컴포지션 내 MoviesScreen의 표현 MovieOverview 컴포저블에는 고유 키가 있으므로 Compose가 변경되지 않은 MovieOverview 인스턴스를 인식하고 재사용할 수 있습니다. 인스턴스의 부수 효과는 계속 실행됩니다.
이처럼 고유한 key를 같이 전달해 준다면
목록의 요소가 변경되더라도 Compose는 개별 MovieOverview 호출을 인식하고 재사용할 수 있어
key 컴포저블을 사용하면 Compose가 컴포지션에서 컴포저블 인스턴스를 식별할 수 있고, 이 기능은 여러 컴포저블이 동일한 호출 사이트에서 호출되고 부수 효과 또는 내부 상태가 포함되어 있을 때 유용해
💡 Tip
일부 컴포저블에는 key 컴포저블 지원 기능이 내장되어 있습니다. 예를 들어 LazyColumn의 경우 items DSL에 맞춤 key를 지정할 수 있습니다.
@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
LazyColumn {
items(movies, key = { movie -> movie.id }) { movie ->
MovieOverview(movie)
}
}
}
모든 입력이 안정적이고 변경되지 않았으면 건너뛸 수 있어
* 안정적이다고 추론할 수 없는 경우 Compose가 스마트 재구성을 선호하도록 유형에 @Stable을 표기해야 함
XML과 Compose 비교할 때마다 "Compose는 선언적이고 변경된 부분만 리컴포지션 해줘서 성능이 좋잖아요. 그래서 Compose를 써요."라고 하고 다녔는데 이것도 틀린 이야기가 아니지만 동작이 어떻게 되는지 자세히 알고 있지 못한 상태에서 공식처럼 하는 대답이었다. Compose의 작동 방식을 샅샅이 들여다보니 어느 부분에서 성능이 좋다고 하는 건지 몸소 느끼게 되었다. 또한 Compose에 대해 더 흥미를 갖게 되어 유익한 시간이었다.
저 귀여운 햄찌 덕에 내용 집중이 어렵습니다