관리 메뉴

오늘도 더 나은 코드를 작성하였습니까?

Compose UI 성능 최적화 하기 본문

Compose

Compose UI 성능 최적화 하기

hik14 2026. 4. 29. 23:44

참조영상

https://www.youtube.com/watch?v=ahXLwg2JYpc

 

1.  언제 성능을 최적화 할 것인가?  성급한 최적화를 하지 말라!

Compose는 이미 기본적으로 꽤 효율적이기 때문에, 앱을 만들고 실제로 성능 문제가 관찰될 때 최적화하라고 합니다. 

 

성능 최적화는 측정 → 분석 → 개선 → 재검증의 반복이다.

"가장 적은 코딩 노력을 들여 사용자에게 가장 부드러운 경험을 제공하는 지점"을 찾는 것이 진정한 의미의 최적화입니다.

 

조기 최적화를 경계하라 (Avoid Premature Optimization)

"조기 최적화는 만악의 근원"이라는 유명한 격언처럼, 성능 문제가 확인되지 않은 상태에서 복잡한 최적화 기법을 적용하는 것은 피해야 합니다.

  • 유지보수성 우선: 과도한 최적화는 코드를 읽기 어렵게 만들고 버그를 유발합니다. 먼저 깨끗하고 동작하는 코드를 작성한 뒤, 병목 지점이 발견되었을 때만 정밀 타격을 가해야 합니다.
  • Tools, not Rules: "모든 리스트에는 반드시 이 기법을 써야 한다"는 규칙보다는, 현재 상황에서 도구(Layout Inspector, Profiler 등)가 가리키는 문제를 해결하는 데 집중하세요

추측하지 말고 측정하라 (Measure, Don't Guess)

최적화에서 가장 위험한 것은 "이 코드가 느릴 것 같다"는 직감에 의존하는 것

  • 벤치마킹 도구 활용: Android에서는 Macrobenchmark를 통해 실제 사용자가 느끼는 앱 시작 시간이나 스크롤 성능(Jank)을 수치화
  • 비용 확인: 최적화도 시간이라는 비용이 듭니다. 측정을 통해 성능 저하가 미미한 수준이라면, 코드의 가독성을 희생하며 최적화하지 않는 것이 더 나은 전략일 수 있습니다

*성능 테스트는 반드시 R8이 활성화된 릴리스 모드에서 진행해야 정확한 결과를 얻을 수 있다.

 

2.  상태 읽기 지연 (Defer Reading State)

* Compose UI 렌더링 단계 이해하기

 

Compose는 한 프레임을 그리기 위해 다음 단계를 단방향(UDF) 으로 거칩니다.

  1. 컴포지션 (Composition) — 무엇을 그릴지 결정
    • @Composable 함수를 실행해서 UI 트리(레이아웃 노드)를 생성
  2. 레이아웃 (Layout) — 어디에 배치할지 결정
    • 측정(measure) → 자체 크기 결정 → 자식 배치(placement) 순서로 트리를 한 번만 순회 (선형 시간)
  3. 그리기 (Drawing) — 어떻게 렌더링할지 결정
    • 트리를 위→아래로 순회하며 캔버스에 픽셀을 그림

 

상태 읽기(state read) 추적

Compose는 어느 단계에서 어떤 state를 읽었는지를 자동으로 추적해서, 그 state가 변경되면 필요한 단계만 재실행합니다.

// 컴포지션에서 state 읽음 → 매 스크롤마다 리컴포지션
Modifier.offset(
    with(LocalDensity.current) { 
        (listState.firstVisibleItemScrollOffset / 2).toDp() 
    }
)

// 레이아웃 단계에서 state 읽음 → 리컴포지션 스킵
Modifier.offset {
    IntOffset(0, listState.firstVisibleItemScrollOffset / 2)
}

 

3.   Stability(안정성) — Compose가 스킵을 결정하는 방식

리컴포지션을 건너뛸지 말지를 결정하는 가장 중요한 개념입니다.

Compose는 컴포저블의 파라미터가 stable(안정적) 이라고 판단되면, 입력값이 같을 때 해당 컴포저블을 스킵합니다.

 

Stable의 조건 

- 두 인스턴스의 equals() 결과가 항상 일관됨

- public property가 변하면 컴포지션에 알림 (예: MutableState)

- 모든 public property도 stable

 

자동으로 stable한 타입: 원시 타입(Int, Boolean 등), String, 함수 타입(람다), MutableState 등.

Unstable한 대표 케이스:

  • var 프로퍼티를 가진 클래스
  • List, Set, Map 같은 인터페이스 (구현체가 mutable일 수 있어서 컴파일러가 보장 못함)
  • 다른 모듈의 클래스 (컴파일러가 검증 못함)

*Data Class로 val(값)으로 원시타입을 이용하여 상태를 정의한다.

data class UserUiState(
    val id: Int,
    val name: String,
    val isLoggedIn: Boolean,
    val score: Double
)

 

4. Stability를 다루는 방법

  • @Stable / @Immutable 어노테이션: 개발자가 컴파일러에게 "이건 안정적이다"라고 알려주는 약속.
  • data class라도 unstable일 수 있음: 내부에 List<T> 같은 unstable 타입이 있으면 전체가 unstable. 
data class TodoUiState(
    val title: String,
    val items: List<String>   // val이지만 List는 unstable!
)

 

해결책

Kotlin의 List, Set, Map 대신 Kotlinx Immutable Collections (ImmutableList, PersistentList)를 사용 또는 wrapper 클래스를 만들어 @Immutable 처리

 

옵션 1: Kotlinx Immutable Collections 사용 (권장)

import kotlinx.collections.immutable.ImmutableList

data class TodoUiState(
    val title: String,
    val items: ImmutableList<String>   // ✅ stable
)

 

옵션 2: @Immutable 어노테이션으로 약속

@Immutable
data class TodoUiState(
    val title: String,
    val items: List<String>   // 개발자가 "절대 변경 안 한다"고 약속
)

 

 

Compose Compiler Metrics로 어떤 클래스/함수가 unstable로 판정됐는지 리포트 생성 가능. 컴파일러 옵션으로 활성화하면 어떤 컴포저블이 스킵되지 못하는지 정확히 진단할 수 있습니다.