0Pricing
Android Academy · 강의

재구성 길들이기

안정적인 매개변수로 재구성 횟수를 줄입니다.

재구성 길들이기은(는) CoddyKit의 무료 Android Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Android Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Android Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

재구성: Compose 성능을 조절하는 손잡이

Jetpack Compose에서는 함수로 UI를 설명합니다. 상태가 변경되면 Compose가 재구성합니다. 즉, 해당 상태를 읽는 컴포저블을 다시 실행하여 화면을 업데이트합니다.

범위를 잘 지정하면 재구성은 정상적이고 비용도 낮습니다. 하지만 너무 자주 발생하거나 트리의 너무 넓은 부분에서 발생하면 Compose 끊김의 가장 큰 원인이 됩니다. 이 수업에서는 재구성을 작고 드물게 유지하는 방법을 배웁니다.

재구성 횟수 확인하기

수정하기 전에 측정하세요. Android Studio의 레이아웃 검사기는 실시간으로 컴포저블별 재구성 횟수를 보여 줍니다. Compose 컴파일러의 지표나 간단한 디버그 카운터를 사용할 수도 있습니다.

간단한 방법으로 SideEffect를 사용하면 컴포저블이 재구성될 때마다 참조값을 증가시킬 수 있으므로 개발 중 예상치 못한 재구성을 기록할 수 있습니다.

@Composable
fun RecompositionCounter(tag: String) {
    val count = remember { mutableStateOf(0) }
    SideEffect { count.value++ }
    Log.d("Recompose", "$tag recomposed ${count.value} times")
}

// Drop RecompositionCounter("PriceLabel") inside a composable
// to watch how often it re-runs while you interact.

가능한 한 늦게 상태 읽기

Compose는 상태 값을 읽는 컴포저블만 재구성합니다. 부모가 상태를 읽으면 부모 전체가 재구성되고, 작은 자식만 읽으면 해당 자식만 재구성됩니다.

따라서 상태 읽기를 트리의 아래쪽으로 내리세요. 여기서 잘못된 버전은 매 틱마다 전체 Column을 재구성하지만, 올바른 버전은 라벨로 범위를 제한합니다.

// BAD: Column reads `seconds`, so everything recomposes each second
@Composable
fun TimerBad(seconds: Int) {
    Column {
        ExpensiveHeader()
        Text("Elapsed: $seconds")
    }
}

// GOOD: only the Text reads the value via a lambda
@Composable
fun TimerGood(seconds: () -> Int) {
    Column {
        ExpensiveHeader()
        Text("Elapsed: ${seconds()}")
    }
}

람다로 읽기 지연하기

강력한 패턴이 있습니다. 자주 변경되는 값을 전달하는 대신 값을 반환하는 람다를 전달하세요. 최종적으로 람다를 호출하는 컴포저블만 재구성됩니다.

이것이 Modifier.offset { ... }과 graphicsLayer { ... }가 람다를 받는 이유입니다. 스크롤 및 애니메이션 값은 매 프레임 변경되지만, 람다를 사용하면 레이아웃 단계에서 재구성이 완전히 제외됩니다.

// Passing the value: parent recomposes every frame of scroll
Box(Modifier.offset(y = scrollOffset.dp))

// Passing a lambda: skips recomposition, updates in the layout phase
Box(Modifier.offset { IntOffset(x = 0, y = scrollOffset.roundToInt()) })

// Same idea for alpha/scale during animation:
Image(
    painter = painter,
    contentDescription = null,
    modifier = Modifier.graphicsLayer { alpha = animatedAlpha() }
)

안정성: Compose가 건너뛰는 이유

모든 매개변수가 안정적이고 변경되지 않았다면 Compose는 컴포저블의 재구성을 건너뛸 수 있습니다. Compose가 equals가 실제 변경을 반영하고 공개 필드가 자신도 모르게 변경되지 않는다고 신뢰할 수 있을 때 해당 타입은 안정적입니다.

  • 안정적: 기본 타입, String, 변경 불가능한 데이터 클래스, State
  • 불안정: List/Map 인터페이스, var 필드가 있는 클래스, 컴파일러가 없는 모듈의 타입

불안정한 매개변수는 아무것도 변경되지 않았을 때에도 재구성을 강제합니다.

// UNSTABLE: List is an interface; Compose cannot assume immutability,
// so UserList recomposes even if the contents are identical.
@Composable
fun UserList(users: List<User>) { /* ... */ }

data class User(val id: Long, val name: String) // stable: all vals

매개변수를 안정적으로 만들기

불안정한 매개변수를 수정하는 일반적인 방법은 두 가지입니다.

  • kotlinx.collections.immutable의 변경 불가능한 컬렉션(예: ImmutableList)을 사용합니다. Compose는 이를 안정적인 타입으로 취급합니다.
  • 직접 제어하는 클래스에 @Immutable 또는 @Stable을 지정하여 변경되지 않는다고 Compose에 약속합니다.

이제 같은 인스턴스를 다시 전달하면 Compose가 안전하게 재구성을 건너뛸 수 있습니다.

import kotlinx.collections.immutable.ImmutableList
import androidx.compose.runtime.Immutable

@Immutable
data class UiState(
    val title: String,
    val users: ImmutableList<User>
)

// Stable parameter -> Compose can skip this when state is unchanged
@Composable
fun UserList(users: ImmutableList<User>) { /* ... */ }

remember: 매 프레임 다시 계산하지 않기

컴포저블은 여러 번 실행될 수 있습니다. 본문에서 직접 수행하는 중요 계산이 있다면 모든 재구성에서 실행됩니다. 이를 remember로 감싸면 키가 변경될 때만 다시 계산됩니다.

다른 상태에서 파생된 값에는 계산 결과가 실제로 변경될 때만 다시 내보내는 derivedStateOf를 우선 사용하세요.

// Recomputes the sorted list only when `items` changes
val sorted = remember(items) { items.sortedBy { it.name } }

// derivedStateOf: only triggers readers when the BOOLEAN flips,
// not on every scroll pixel
val showButton by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
if (showButton) ScrollToTopButton()

지연 목록에서 안정적인 키 사용하기

LazyColumn/LazyRow에서는 각 항목에 안정적인 키를 지정하세요. 키가 없으면 Compose가 항목을 위치로 추적하기 때문에 항목을 삽입하거나 순서를 변경할 때 실제로 변경되지 않은 항목까지 재구성하고 다시 측정합니다.

안정적인 키를 사용하면 Compose가 업데이트 사이에서 항목을 대응시키고 변경되지 않은 항목을 건너뛸 수 있습니다.

LazyColumn {
    items(
        items = users,
        key = { user -> user.id }   // stable identity
    ) { user ->
        UserRow(user)
    }
}
// Now adding a user at the top reuses existing rows
// instead of recomposing the whole list.

상태 끌어올리기, 이벤트 위로 전달하기

불안정한 람다 매개변수도 매 재구성마다 새 람다 인스턴스가 생성되면 건너뛰기를 방해할 수 있습니다. 람다를 기억하거나 안정적인 함수를 참조하여 콜백을 안정화하세요.

이를 상태 끌어올리기(상태는 호출자에 있고 이벤트는 위로 흐름)와 결합하면 말단 컴포저블을 저렴하고 건너뛸 수 있는 상태로 유지할 수 있습니다.

@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
    TextField(value = query, onValueChange = onQueryChange)
}

// In the caller, a remembered lambda keeps the reference stable:
val onChange = remember { { newValue: String -> viewModel.setQuery(newValue) } }
SearchBar(query = query, onQueryChange = onChange)

스크롤/애니메이션 상태를 너무 높은 곳에서 읽지 않기

흔히 하는 실수는 빠르게 변경되는 상태(스크롤 오프셋, 애니메이션 진행률)를 높은 수준의 컴포저블에서 읽는 것입니다. 그러면 전체 하위 트리가 매 프레임 재구성됩니다.

이러한 읽기는 Modifier 람다(offset {}, graphicsLayer {}, drawBehind {}) 안에 두어 작업이 재구성이 아니라 레이아웃 또는 그리기 단계에서 수행되게 하세요. 이것이 매끄러운 60fps 스크롤과 끊기는 스크롤의 차이입니다.

// Animated color used only for drawing -> stay in the draw phase
Box(
    Modifier.drawBehind {
        drawRect(color = animatedColor())  // lambda read, no recomposition
    }
)

재구성 확인 목록

상호작용 중 화면이 끊기는 것처럼 느껴지면 다음 목록을 확인하세요.

  • 매개변수가 안정적인가요(변경 불가능한 데이터, ImmutableList)?
  • 빠르게 변경되는 상태를 가능한 한 낮은 위치에서, 이상적으로는 수정자 람다 안에서 읽고 있나요?
  • 지연 목록에 안정적인 키가 있나요?
  • 비용이 큰 계산이 remember / derivedStateOf 뒤에 있나요?
  • 레이아웃 검사기로 실제 횟수가 줄었는지 확인했나요?

각 항목을 확인할 때마다 모든 프레임에서 낭비되는 작업이 줄어듭니다.

빠른 확인

List<User>를 컴포저블에 전달했는데 데이터가 변경되지 않았을 때에도 재구성되는 것을 확인했습니다. 가장 직접적인 수정 방법은 무엇일까요?

복습: 더 작고 드문 재구성

Compose의 가장 큰 성능 문제인 재구성을 제어하는 방법을 배웠습니다.

  • 레이아웃 검사기 또는 디버그 카운터로 횟수를 측정합니다.
  • 상태를 가능한 한 낮은 위치에서 읽고, 람다로 읽기를 지연합니다.
  • 변경 불가능한 데이터와 @Immutable/ImmutableList로 매개변수를 안정적으로 만듭니다.
  • remember와 derivedStateOf를 사용하여 매 프레임 다시 계산하지 않게 합니다.
  • 지연 목록 항목에 안정적인 키를 지정하고, 빠르게 변경되는 상태 읽기는 수정자 람다 안에 둡니다.

다음에는 CPU/재구성에서 메모리로 넘어가 누수를 찾고 수정합니다.

자주 묻는 질문

“재구성 길들이기” 강의는 무료인가요?

네 — “재구성 길들이기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Android Academy 강의 전체를 잠금 해제할 수 있습니다. Android Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“재구성 길들이기”에서 뭘 배우나요?

안정적인 매개변수로 재구성 횟수를 줄입니다. 브라우저에서 직접 실행하는 실습 코드로 Android Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Android Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Android Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“재구성 길들이기” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Android Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Android Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 성능 측정
  2. 재구성 길들이기
  3. 메모리 누수와 해결 방법
  4. 시작 시간과 기준선 프로필
← Android Academy(으)로 돌아가기