[Android] stable

MariGold·2025년 10월 23일

[Android]

목록 보기
6/12
post-thumbnail

Jetpack Compose에서는 상태 기반의 선언적 UI 덕분에 UI를 직관적으로 작성할 수 있습니다.
하지만 상태 변경에 따라 발생하는 리컴포지션(Recomposition)이 너무 잦다면,
불필요한 UI 재그림으로 성능 저하가 발생할 수 있습니다.

이번 글에서는 Compose의 안정성(Stable) 시스템과 @Stable 애너테이션을 활용해
리컴포지션을 최소화하고 성능을 높이는 방법을 정리해보겠습니다.


🚀 @Stable이란?

@Stable은 Compose 컴파일러에게 “이 객체는 안정적인 상태를 가진다”라고 알려주는 애너테이션입니다.

즉,

“이 객체의 상태가 변하지 않으면 다시 그릴 필요가 없다.”

라고 Compose에 명시적으로 알려주는 역할을 합니다.

@Stable을 올바르게 사용하면 Compose가 불필요한 UI 갱신을 방지하고,
성능을 최적화할 수 있습니다.


⚙️ Compose의 안정성(Stable) 판정 구조

Compose는 객체가 “안정적인지”를 자동으로 판별합니다.
하지만 완전히 추론되지 않는 경우가 많기 때문에, 개발자가 직접 @Stable을 붙여야 할 때가 있습니다.

항목자동으로 Stable로 인식됨직접 @Stable 필요
Int, String, Float 등 기본 타입
모든 프로퍼티가 불변인 data class
@Immutable로 표시된 클래스
var가 있는 클래스
외부 라이브러리 객체 (Compose가 내부 구조를 모름)
mutableStateOf로 감싸진 상태⚠️ (조건부)⭕ (보통 권장)

💡 @Stable vs @Immutable

두 애너테이션은 비슷해 보이지만, 의미가 다릅니다.

구분@Immutable@Stable
의미완전히 불변 (내부 값이 절대 안 변함)내부 상태는 변하지만 Compose가 변화를 추적 가능
변경 가능성❌ 불가능⭕ 가능
리컴포지션 발생값이 바뀌면 새 객체 생성 → 리컴포즈내부 변경을 Compose가 감지 가능
사용 예시DTO, UI 모델StateHolder, ViewModel 상태

🧠 예시: Stable한 상태 클래스 만들기

@Stable
class CounterState {
    var count by mutableStateOf(0)
        private set

    fun increment() {
        count++
    }
}

이 클래스는 count라는 상태가 변하더라도,
CounterState 객체 자체는 안정적(Stable)입니다.

즉, Compose는 CounterState의 내부 값이 바뀔 때만 필요한 부분만 리컴포즈하고,
다른 UI는 다시 그리지 않습니다.


⚡ Stable이 없을 때의 문제

class CounterState {
    var count by mutableStateOf(0)
}

이 클래스엔 @Stable이 없습니다.
Compose는 내부 상태 추적은 하더라도,
CounterState 객체 자체가 "불안정"하다고 간주할 수 있습니다.

그 결과:

  • Compose는 이 객체를 포함하는 Composable을 더 자주 리컴포즈할 수 있습니다.
  • 상태 변경이 없더라도 불필요한 UI 갱신이 발생할 수 있습니다.

🧩 상태 추적과 안정성의 관계

Compose는 @Stable 객체를 다음과 같이 다룹니다:

  1. @Stable 객체는 내부 필드가 변하지 않으면 다시 그리지 않음
  2. mutableStateOf로 감싸진 필드는 자동으로 Compose에 변경을 알림
  3. 결과적으로 필요한 부분만 리컴포즈

즉, @StablemutableStateOf를 조합하면
UI는 변화가 있는 부분만 효율적으로 갱신됩니다.


🎨 실전 예시: 불필요한 리컴포지션 줄이기

❌ 잘못된 예시

@Composable
fun CounterView(count: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("Count: $count")
    }
}

이 경우 count 값이 바뀔 때마다 CounterView 전체가 리컴포즈됩니다.
규모가 커질수록 성능 부담이 커질 수 있습니다.


✅ 개선된 예시 — Stable 상태 객체 활용

@Stable
class CounterState {
    var count by mutableStateOf(0)
}

@Composable
fun CounterView(state: CounterState) {
    Button(onClick = { state.count++ }) {
        Text("Count: ${state.count}")
    }
}

이렇게 하면 CounterState 내부 값이 변경될 때만
해당 Text 부분만 리컴포즈되고,
다른 UI 구조는 그대로 유지됩니다.


🧩 @Stable 사용 시 주의할 점

주의 항목설명
⚠️ 실제로 변하지 않는 객체에만 사용내부 값이 자주 바뀌면 캐시 효과가 없음
⚠️ 상태 추적이 불가능한 필드 포함 금지Compose가 변화 감지 불가
⚠️ 외부 라이브러리 객체를 감쌀 때 신중히Compose는 해당 타입의 안정성 판단 불가
mutableStateOf와 함께 사용Compose가 자동으로 변화를 추적

🧾 정리: 리컴포지션 최소화 전략

전략설명
@Stable 사용Compose가 불필요한 리컴포지션을 피하도록 명시
mutableStateOf로 상태 추적내부 값 변화를 Compose가 감지하도록 처리
@Immutable 병행완전 불변 데이터는 Immutable로 처리
⚡ 상태 객체 단일화여러 개의 작은 상태 대신 안정적인 StateHolder로 묶기
⚠️ Stable을 남발하지 말 것잘못 쓰면 Compose가 변화를 감지하지 못함

🧩 결론

Compose 성능 최적화의 핵심은 “언제 다시 그릴지를 Compose에 정확히 알려주는 것”입니다.

@Stable은 Compose에게 “이 객체는 안전하게 유지해도 된다”는 신호를 주어
불필요한 리컴포지션을 줄이고,
UI 갱신을 정확하고 효율적으로 제어할 수 있게 합니다.

  • 완전 불변 데이터 → @Immutable
  • 내부 상태를 가진 안정적 객체 → @Stable
  • 단순 상태 값 → mutableStateOf

이 원칙을 지키면, Compose UI는 빠르고 부드럽게 동작합니다 🚀


📚 참고 자료


💡 TIP
@Stable을 붙이면 “리컴포지션을 안 한다”가 아니라,
“필요할 때만 정확히 리컴포지션한다”는 의미입니다.
즉, 안정성(Stable)은 리컴포지션의 빈도 제어 도구라고 생각하세요.

profile
많은 것을 알아가고 싶은 Android 주니어 개발자

0개의 댓글