Flow.combine 없이 위젯·서비스·UI 상태를 하나로 묶는 법: Jetpack AppState의 Transform과 StateStore 해부
Android Dev Summit 2026에서 공개된 Jetpack AppState(1.0.0-alpha01)의 헤드리스 상태 변환 라이브러리 Transform과 다중 컴포넌트 공유 컨테이너 StateStore를 정리한다. StateFlow 체인의 일관성·세밀함 딜레마 해결 원리와 Activity·Service·Glance 위젯 간 상태 영속화 패턴을 살펴본다.
Jetpack AppState 아키텍처 — 헤드리스 Compose 런타임 기반 Transform과 다중 컴포넌트 StateStore 구조도
결론부터 말하면 Jetpack Compose의 진짜 핵심 엔진은 UI를 그리는 Compose UI가 아니라 스냅샷 상태(Snapshot State)를 추적하는 Compose Runtime이며, 새로 공개된 Jetpack AppState(1.0.0-alpha01)를 사용하면 복잡한 Flow.combine·flatMapLatest·stateIn 체인 없이도 ViewModel, 백그라운드 서비스, 홈 화면 위젯(Glance) 전체에서 단일 State<T> 모델을 일관되게 파생·공유·영속화할 수 있다. Android Developers의 'Take Compose state beyond the UI with Jetpack AppState' 세션에서 구글 아키텍처 팀은 헤드리스(Headless) 컴포지션 기반의 **Transform**과 프로세스 종료(Process Death)에도 살아남는 컴포넌트 간 상태 공유 컨테이너 **StateStore**를 공개했다.
단, 영상과 공식 문서에서 소개된 모든 아티팩트(androidx.appstate.*:1.0.0-alpha01)는 현재 알파(Alpha) 단계의 실험적 API이며 이 글은 공식 세션과 문서를 바탕으로 한 기술 정리이므로, 프로덕션 핵심 결제·인증 파이프라인에 전면 도입하기보다는 멀티 서피스(앱 바 + 홈 위젯 + 서비스) 상태 동기화 구간부터 단계적으로 검증하는 편이 안전하다.
1. 왜 StateFlow 체인 대신 Compose Snapshot State인가: 일관성과 세밀함의 딜레마
Android Developers — Take Compose state beyond the UI with Jetpack AppState 공식 세션
그동안 안드로이드 공식 아키텍처 가이드는 Compose State(androidx.compose.runtime.State)를 UI 계층에만 가두고, ViewModel과 데이터·도메인 계층에서는 무조건 Kotlin Flow / StateFlow를 쓰라고 권장했다. 하지만 앱이 커지고 여러 데이터 스트림과 로컬 입력, 네트워크 연결 상태를 조합하기 시작하면 두 가지 고질적인 병목이 발생한다.
첫째는 combine, flatMapLatest, distinctUntilChanged, stateIn(WhileSubscribed(5000))가 얽히는 연산자 스파게티이고, 둘째는 StateFlow에서 일관성(Consistency)과 세밀한 무효화(Granularity)를 동시에 얻을 수 없다는 구조적 한계다.
| 비교 기준 | 단일 StateFlowNOTION_MARKDOWN_ESCAPED_LT_SENTINELUiState> 번들링 | 개별 프로퍼티별 다중 StateFlow 분리 | Compose Snapshot State • withMutableSnapshot |
| 구독 및 읽기 추적 방식 | 명시적 collectAsStateWithLifecycle() 호출 필요 (전체 객체 단위 구독) | 각 스트림마다 개별 collect 호출 필요 | 자동·동적 읽기 추적(Automatic & Dynamic Read Tracking): 실제 실행된 코드 분기( if)에서 읽은 프로퍼티만 런타임에 구독 등록 |
| 세밀함(Granularity) | 점수( score) 하나만 바뀌어도 UiState.copy()로 인해 무관한 헤더 컴포넌트까지 리컴포지션 검토 대상이 됨 | 변경된 프로퍼티를 읽는 컴포넌트만 무효화됨 (우수) | 각 프로퍼티를 mutableStateOf로 개별 선언하여 실제 읽은 하위 컴포저블만 정확히 무효화 |
| 원자적 일관성(Consistency) | 단일 불변 데이터 클래스 교체로 일관성 보장 (우수) | 멀티스레드 업데이트 중 일부 스트림만 먼저 반영되는 불일치(Tearing) 위험 발생 | Snapshot.withMutableSnapshot { ... } 블록 안에서 여러 상태를 격리 수정한 뒤 한 번에 원자적으로 반영 |
즉, Compose 런타임의 스냅샷 시스템은 별도의 구독 보일러플레이트 없이도 if 조건문에서 실제로 읽힌 값만 동적으로 구독하고, withMutableSnapshot으로 다중 상태 변경의 원자성까지 보장한다.
2. UI 없이 @Composable로 상태를 조합하는 Transform과 Lock-Free 프레임 클럭
Cash App의 오픈소스 Molecule이 헤드리스 컴포지션으로 Flow를 생성하는 아이디어를 개척했다면, 구글의 Transform(androidx.appstate.transform:transform)은 한 걸음 더 나아가 출력 자체를 Flow가 아닌 순수 State<T>로 반환한다. 소비자가 이미 Compose UI나 Glance 위젯이라면 굳이 중간에 Flow로 바꿨다가 다시 collectAsState로 변환할 이유가 없기 때문이다.
내부 동작 원리: 디스플레이에서 분리된 TransformFrameClock
일반 Compose UI의 Recomposer는 화면 주사율(VSync, 60Hz/120Hz)에 맞춘 디스플레이 프레임 클럭을 기다린다. 반면 백그라운드 서비스나 위젯, 데이터 계층에서 도는 Transform은 화면이 없으므로 UI 스레드를 점유해서는 안 된다.
- 즉시 실행(Eager) Lock-Free 상태 머신:
Transform은 전용 헤드리스 클럭인 **TransformFrameClock**을 사용한다. 구독 중인 상태가 바뀌는 즉시 단순CAS(Compare-And-Set)연산으로 새 프레임을 즉시 생성해 지정된 코루틴 디스패처(비-UI 스레드)에서 재계산한다.
- 유휴 시 비용 제로(Zero-Cost Parking): 입력 상태에 변화가 없을 때는 클럭이 완전히 파킹(Park)되어 CPU나 배터리를 전혀 소모하지 않는다.
- 중첩 변환(Nested Transforms)과 세밀한 재실행: 애플리케이션 스코프에서 파생한 상위
State<FocusSummary>를 하단 미니 타이머 바의 ViewModel과 Glance 홈 위젯이 동시에 읽더라도, 미니 타이머 바의 로컬 상태(isExpanded)가 바뀔 때는 미니 타이머 바의transform블록만 재실행되고 앱 레벨 변환이나 홈 위젯은 전혀 깨어나지 않는다.
3. 컴포넌트 경계와 프로세스 종료를 넘는 StateStore와 아티팩트 구성
단일 Activity 안에서는 ViewModel과 SavedStateHandle만으로 충분하지만, Activity·백그라운드 Service·Glance 앱 위젯이 동일한 세션 상태(예: 포커스 타이머 진행 상태)를 공유해야 할 때는 기존 도구들에 빈틈이 있었다. SavedStateHandle은 Activity 번들에 종속되어 서비스에서 쓸 수 없고, 싱글톤의 mutableStateOf는 메모리에만 존재해 백그라운드 프로세스 종료(Process Death) 시 날아간다.
이를 해결하는 **StateStore**는 앱 컴포넌트 전체에서 공유 가능한 타입 안전(Type-safe) State<T> 컨테이너이며, statestore-datastore 통합을 통해 키 단위 영속화를 선언적으로 지원한다.
| 아티팩트 / 플러그인 | Maven 좌표 ( 1.0.0-alpha01 기준) | 핵심 역할 및 빌드 요구사항 |
| Transform | androidx.appstate.transform:transform | UI 외부에서 transform 및 부수 효과 관찰용 listener 실행 (호출 모듈에 buildFeatures { compose = true } 및 Compose Compiler 플러그인 필수) |
| Transform Testing | androidx.appstate.transform:transform-testing | 유닛 테스트용 transformTest 및 TransformTestScope 유틸리티 제공 |
| StateStore | androidx.appstate.statestore:statestore | 타입 안전 공유 상태 맵 컨테이너 및 StateStoreKey 정의 (단순 getState/setState/updateState만 호출하는 모듈은 Compose 컴파일러 불필요) |
| StateStore DataStore 통합 | androidx.appstate.statestore:statestore-datastore | PersistedDataStoreKey 및 @Serializable 상태를 Jetpack DataStore에 자동 동기화 (kotlinx-serialization-core 필수) |
StateStore 생성 시 syncDataStore를 한 번만 연결해 두면, 내부적으로 Transform의 listener API와 LaunchedEffect가 동작해 PersistedDataStoreKey를 구현한 키의 값이 바뀔 때마다 자동으로 Jetpack DataStore에 비동기 저장된다.
4. 실무 도입 체크리스트
- 상태의 3가지 수명주기 분류에 맞춘 저장소 선택: ① 블루투스·네트워크 연결 같은 메모리 전용 Live State는 싱글톤 내부의
mutableStateOf로, ② 화면 로컬 UI State는 ViewModel의SavedStateHandle.saveable로, ③ Activity·Service·Glance 위젯이 함께 읽고 프로세스 종료 후에도 복원해야 하는 공유 Session State는StateStore+statestore-datastore로 분리한다.
- 비-UI 모듈의 Compose 컴파일러 설정 확인: 데이터·도메인 모듈이라도
@Composable람다인transform { ... }이나listener { ... }를 직접 호출한다면 해당 모듈의build.gradle.kts에buildFeatures { compose = true }와org.jetbrains.kotlin.plugin.compose플러그인을 반드시 추가해야 한다(단순히State<T>를 읽고 쓰기만 하는 모듈은 추가할 필요 없음).
- 다중 프로퍼티 동시 갱신 시
withMutableSnapshot래핑: 별도의 스레드나 콜백에서 여러MutableState프로퍼티를 동시에 수정할 때는 중간 상태가 다른 소비자에게 노출되지 않도록Snapshot.withMutableSnapshot { ... }으로 감싸 원자적 일관성을 확보한다.
참고 자료
- Take Compose state beyond the UI with Jetpack AppState — Android Developers YouTube 공식 영상 (October 30, 2026 게시, November 2, 2026 확인)
- AppState | App architecture 공식 가이드 (November 2, 2026 확인)
- Transform | App architecture 공식 문서 (November 2, 2026 확인)
- StateStore | App architecture 공식 문서 (November 2, 2026 확인)
- Appstate Statestore 1.0.0-alpha01 릴리스 노트 | Android Jetpack (November 2, 2026 확인)