Jetpack Compose (Runtime / Internals) - Runtime의 Snapshot 구조 정리

느린달팽이·2025년 11월 9일

Android

목록 보기
3/4

1️⃣ Snapshot이란?

Compose Runtime이 상태(State) 의 변경을 감지하고

UI 재구성을 효율적으로 수행하기 위해 사용하는 데이터 일관성(Consistency) 관리 시스템입니다.

간단히 말해,

  • “현재 상태가 어떤 값인지”
  • “어떤 컴포저블이 그 값을 참조 중인지”
  • “값이 바뀌었을 때 어디를 다시 그려야 하는지” 이 모든 것을 Snapshot 시스템이 내부에서 추적합니다.

2️⃣ Snapshot이 필요한 이유

Compose는 멀티스레드에서 상태를 바꿀 수도 있고,

Recomposition은 비동기적으로 발생합니다.

그래서 다음 두 가지 문제가 생길 수 있죠:

문제설명
데이터 불일치한 스레드가 상태를 읽는 도중 다른 스레드가 값을 바꾸면 UI가 꼬임
불필요한 Recomposition바뀌지 않은 상태까지 다시 그림

💡 Snapshot 시스템의 역할:

상태를 읽고 쓰는 시점을 “스냅샷(사진)”처럼 캡처해서

변경과 참조를 정교하게 추적하고, 필요한 UI만 다시 그리게 한다.


3️⃣ Snapshot의 내부 핵심 개념

구성 요소설명
Snapshot상태 값의 일관된 시점(View)을 나타내는 객체
SnapshotMutableStateCompose의 mutableStateOf()가 내부적으로 사용하는 상태 래퍼
SnapshotRecord상태 변경을 추적하기 위한 레코드(버전 정보 포함)
SnapshotObserver어떤 상태를 읽었는지 관찰하고, 변경 시 재구성을 트리거
ApplyScope상태 변경을 실제로 커밋하는 영역 (atomic commit)

4️⃣ 내부 구조 흐름

[mutableStateOf(0)]
      │
      ▼
 SnapshotMutableState
      │
  ┌───┴───────────────────────────────────┐
  │  SnapshotRecord(version, value)       │
  │  SnapshotObserver: who read this?     │
  └───────────────────────────────────────┘
      │
      ▼
 Compose Runtime registers observer
 (so it knows which composable depends on this state)

5️⃣ 상태 읽기(Read)와 쓰기(Write) 과정

🧩 (1) Read Phase

  • 컴포저블이 state.value를 읽을 때, Snapshot Observer가 “이 컴포저블이 이 상태를 읽었다”고 기록합니다.
val count by remember { mutableStateOf(0) }
Text("Count: $count")

➡ 내부 동작:

SnapshotObserver:
  Text() 컴포저블이 count 상태를 참조함

🧩 (2) Write Phase

  • count++ 처럼 상태가 바뀌면 Snapshot 시스템이 “해당 상태를 읽은 모든 구독자(컴포저블)”를 탐색합니다.
  • Compose Runtime이 그 구독자에 대해 Recomposition 요청을 보냅니다.

➡ 즉, Snapshot이 변경을 감지하고

“누가 이 데이터를 보고 있었는지”를 알고 있기 때문에

필요한 부분만 정확히 다시 그려줍니다.


6️⃣ Snapshot의 Commit Cycle

Compose는 상태 변경을 트랜잭션처럼 처리합니다.

┌───────────────────────────┐
│  1️⃣ Snapshot Open        │ ← 상태 변경 시작
│  2️⃣ Modify Record        │ ← 새 값 기록 (임시)
│  3️⃣ Apply (Commit)       │ ← 모든 변경을 일괄 반영
│  4️⃣ Notify Observers     │ ← 관련 컴포저블 재구성 요청
└───────────────────────────┘

이 덕분에 여러 상태가 동시에 바뀌어도

UI는 일관된 한 시점의 값만 본다.


7️⃣ Coroutine·Runtime과의 관계

rememberCoroutineScope() 등에서 비동기로 상태를 바꾸는 경우도,

Snapshot은 코루틴 내부의 상태 변경까지 감지합니다.

val scope = rememberCoroutineScope()
val state = remember { mutableStateOf(0) }

Button(onClick = { scope.launch { state.value++ } }) {
    Text("Click ${state.value}")
}
  • 코루틴이 백그라운드에서 state.value++ 수행
  • Snapshot이 write operation을 감지
  • Compose Runtime이 관련 컴포저블을 재구성

💡 즉, CoroutineScope는 실행의 단위,

Snapshot은 상태 일관성 관리의 단위,

그리고 Runtime이 그 둘을 통합적으로 제어합니다.


8️⃣ 전체 내부 흐름 요약

CoroutineScope.launch { state.value++ }   // suspend context에서 상태 변경
        │
        ▼
Snapshot Write detected by Runtime
        │
        ▼
Snapshot Commit → Notify Observers
        │
        ▼
Runtime marks affected Composables as invalid
        │
        ▼
Recomposition Scheduler executes → affected UI redraw

9️⃣ Snapshot의 장점

항목설명
일관성(Consistency)여러 상태를 동시에 변경해도 UI는 한 시점만 본다
정확한 변경 감지어떤 UI가 어떤 상태를 읽었는지 추적
부분적 재구성전체 UI 대신 필요한 컴포저블만 다시 그림
스레드 안전성스냅샷 단위로 변경을 관리하여 병행 접근 안정화
비동기 안정성Coroutine 등 비동기 변경도 안전히 반영

비유로 이해하기

Snapshot은 런타임의 타임머신입니다.

런타임이 매 순간 상태의 “사진(Snapshot)”을 찍어두고,

새로운 변경이 생기면

“이 사진을 참조하던 컴포넌트만 다시 갱신”하는 구조입니다.


한 줄 요약

Snapshot은 Compose Runtime 내부의 상태 관리 핵심 엔진으로,

모든 mutableStateOf 값을 트랜잭션처럼 추적·커밋하며

필요한 컴포저블만 재구성되도록 보장한다.

즉, Compose의 반응형·일관된 UI를 가능하게 하는 런타임 레벨의 뇌(Brain) 이다.

profile
한걸음이라도 제대로... 쓰임있는 앱을 만들자

0개의 댓글