Android 개발/Jetpack Compose

최근 안정화된 Compose API, MeshGradient 말고 매일 쓸 3가지

stackD 2026. 5. 13. 18:00

 

화려한 MeshGradient보다 따분한 lint 경고 하나가 팀 전체 성능 기준을 바꿔놓습니다. 이번 릴리즈의 진짜 가치는 제일 눈에 안 띄는 API 세 개에 있더라구요.

 

젯팩 컴포즈(Jetpack Compose)의 최근 업데이트 내역을 처음 봤을 때, 솔직히 MeshGradient에 잠깐 눈이 갔습니다. 그런데 릴리즈 노트를 조금 더 읽다 보니까, 매일 손 가는 코드 문제들을 건드린 변경사항 세 개가 눈에 띄더라구요. 직렬화 보일러플레이트, 재컴포지션 성능 버그, 서비스에서의 다이얼로그 처리 — 다들 한 번씩은 머리를 싸맨 지점들이에요. 이런 기능들은 과거에 소개된 이후 이제는 안정화되어 더 편하게 쓸 수 있게 되었죠.

 

rememberSerializable + KSerializer: Parcelable 없이 상태 저장

과거 컴포즈에서 복잡한 객체를 저장하려면 Parcelable 구현이 사실상 필수였습니다. writeToParcel, CREATOR, describeContents — 데이터 클래스에 필드 하나 추가할 때마다 이 코드를 같이 건드려야 했거든요. 안드로이드 스튜디오(Android Studio) 자동 생성을 써도 관리 포인트가 늘어나는 건 피할 수 없었어요.

 

이제는 kotlinx.serialization을 활용한 방식이 훨씬 편해졌습니다. 데이터 클래스에 @Serializable 어노테이션을 붙이고, 기존 rememberSaveable 대신 rememberSerializable을 사용하면 돼요. 대략 이런 모양이거든요.

 
@Serializable
data class FilterState(
    val selectedCategory: String = "ALL",
    val sortOrder: SortOrder = SortOrder.RECENT,
    val isExpanded: Boolean = false
)

@Composable
fun FeedScreen() {
    // 이제 복잡한 별도 Saver나 Parcelable 없이도 상태 저장이 가능합니다.
    var filterState by rememberSerializable {
        mutableStateOf(FilterState())
    }
}

 

이 방향성은 명확하게 자리 잡았습니다. Parcelable 관련 코드가 통째로 사라지고, 같은 @Serializable 클래스를 네트워크 요청 직렬화나 최신 네비게이션(Navigation Compose)의 Safe Args에도 재사용할 수 있어서 직렬화 로직이 한 곳으로 모이는 셈이거든요.

 

개인적으로는 지금 만들고 있는 사이드 프로젝트에서 이 변경사항이 제일 반가웠어요. 컴포즈로 필터 화면을 만드는 중인데, 화면 회전에도 필터 상태를 유지해야 해서 Parcelable 필드를 손으로 관리하다 실수가 나오던 참이었기 때문이죠.

 

 

Compose @FrequentlyChangingValue: 성능 버그를 빌드 전에 잡기

스크롤 위치나 센서 값처럼 매 프레임 바뀌는 상태를 derivedStateOf 없이 일반 state로 선언하면 연쇄 재컴포지션이 발생합니다. 화면이 버벅이는 느낌이 드는데 원인을 못 찾겠다면, 안드로이드 스튜디오 레이아웃 검사기에서 재컴포지션 카운터를 직접 찍어봐야 알아챌 수 있는 경우가 많았습니다. 코드 리뷰 단계에서는 잡아내기가 어렵거든요.

 

Compose 1.9.0-alpha01부터 추가되었던 @FrequentlyChangingValue 어노테이션은 이제 아주 유용한 도구가 되었습니다. 이 어노테이션이 붙은 변수를 derivedStateOf 없이 직접 읽으면, IDE가 lint 경고를 컴파일 시점에 바로 띄워줘요.

 
// 선언 측: 자주 바뀌는 값임을 명시
@FrequentlyChangingValue
val scrollOffset: Int = ...

// 사용 측
val isHeaderVisible = scrollOffset > 100               // ⚠️ lint 경고: derivedStateOf 권장
val isHeaderVisible by remember { derivedStateOf {
    scrollOffset > 100                                  // ✅
} }

 

런타임 성능에는 영향이 없습니다. 순수 컴파일 시점 검사라서 도입 비용이 사실상 0에 가까운 편이에요. 게다가 새로 합류한 팀원도 IDE 경고 덕분에 재컴포지션 주의사항을 자연스럽게 챙기게 되거든요. 따로 팀 공유 문서를 만들지 않아도 되는 셈이죠.

 

제 생각에는, 이 어노테이션의 진짜 가치는 팀 코드 기준을 자동화한다는 데 있다고 봅니다. 이번 세 가지 중에 도입 비용이 제일 낮기도 하구요.

 

 

서비스에서 Dialog 띄우기: 플랫폼 정책과 우회 방식의 한계

전화 수신 화면, 플로팅 알림처럼 백그라운드 서비스에서 UI를 올려야 하는 경우가 있습니다. 기존에는 ComposeView를 WindowManager에 수동으로 붙이거나 우회 라이브러리를 써야 했어요. 코드가 복잡해지는 건 물론이고, 컴포즈와 서비스 생명주기를 직접 연결해야 해서 엣지 케이스가 나오기 쉬운 구조였거든요.

 

다이얼로그 컴포저블의 DialogProperties를 활용해 기본 동작을 제어할 수는 있지만, 백그라운드에서 UI를 띄우는 방식 자체에 대한 안드로이드 플랫폼 정책이 점점 엄격해지고 있다는 점을 반드시 고려해야 합니다. 특히 시스템 오버레이 권한(SYSTEM_ALERT_WINDOW) 사용은 사용자 경험과 보안상의 이유로 최신 안드로이드 버전에서는 더욱 까다로워졌습니다.

 

Dialog(
    onDismissRequest = { /* dismiss 처리 */ },
    properties = DialogProperties(
        dismissOnBackPress = false,
        dismissOnClickOutside = false
        // 컴포즈 표준 DialogProperties에 맞게 플랫폼 버전에 따른 추가 처리가 필요할 수 있습니다.
    )
) {
    IncomingCallCard(
        caller = callerInfo,
        onAccept = { /* ... */ },
        onDecline = { /* ... */ }
    )
}

 

따라서 상용 코드에 적용할 때는 서비스에서 억지로 컴포즈 UI를 띄우는 것이 정말 최선인지 다시 검토하는 것이 좋습니다. 전화 수신과 같은 일부 필수적인 경우를 제외하고는, 포그라운드 서비스 알림(Foreground Service Notification)과 전체 화면 인텐트(FullScreenIntent)를 가진 액티비티를 조합하는 방식이 훨씬 더 안정적이고 플랫폼에서 공식적으로 권장하는 대안입니다.

 

Compose 신규 기능 도입 판단 기준 3가지

새로운 기능을 모두 같은 시점에 적용하면 안 됩니다. 저는 아래 세 가지 기준으로 나눠서 보고 있어요.

  1. API 안정성 — 이제 안정화된 기능인가? 공식 로드맵이나 릴리즈 노트를 통해 해당 기능이 실험(Experimental) 단계를 벗어나 안정(Stable) API가 되었는지 확인하는 것이 첫 번째입니다.
  2. 변경 비용 — API가 바뀌면 수정 범위가 어디까지인가 lint 어노테이션처럼 선언부만 건드리면 되는 건 변경 비용이 낮습니다. 반면 저장 포맷이나 구조가 바뀌는 경우, 과거 데이터와 마이그레이션해야 할 수도 있거든요.
  3. 현재 가치 — 지금 쓰는 우회책이 더 비싼가 기술 부채가 눈에 띄게 쌓이고 있다면 새로운 안정 기능을 도입할 가치가 충분합니다. 반대로 현재 방식이 완벽하게 동작 중이라면 서두를 이유가 없거든요.

이 기준으로 이번 세 가지를 나눠보면 이렇게 됩니다. @FrequentlyChangingValue는 런타임 위험이 없으니 지금 바로 적용. rememberSerializable은 신규 화면부터 적용하면서 기존 Parcelable 코드를 점진적으로 교체. 서비스에서 Dialog를 띄우는 방식은 플랫폼 정책 변화에 민감하므로, 더 안전한 대안(액티비티 조합)을 우선적으로 고려하고 꼭 필요한 경우에만 신중하게 도입하는 것이 좋습니다.

 

 

마치며

새로운 기능이라고 무조건 기다리는 것도 비용이에요. 기술 부채는 기다린다고 사라지지 않으니까요. 세 가지 기준 중 "괜찮다"는 결과가 하나라도 나오는 기능이 있다면, 팀과 도입 일정을 잡아두시는 것도 좋을 것 같습니다.