기존의 xml으로 ui를 구현하고, viewbinding & databinding으로 객체의 상태관리를 했던 것과 다르게 compose는 선언형으로
flutter, swiftUI와 비슷한 구조를 보인다. 익숙했던 방식이 아니라 생소하지만, android 생태계를 이해하고 있으면 금방 이해할 수 있다.
SwiftUI → 선언형, Apple이 UIKit 대체로 밀고 있음
Compose → 선언형, Google이 XML View 대체로 밀고 있음
Flutter → 선언형, 근데 자체 렌더링 엔진 (Skia/Impeller) 사용

1. Recomposition — 언제 일어나고 어떻게 최소화하나
개념
Compose는 UI를 함수로 표현한다.
@Composable 함수가 호출되면서 화면을 그리는데, 상태(State)가 바뀌면 그 상태를 읽는 composable만 다시 호출된다.
이를 Recomposition이라고 한다.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Text(text = "Count: $count") // count 바뀌면 여기만 recompose
Button(onClick = { count++ }) {
Text("증가") // count 안 읽으니까 recompose 안 됨
}
}
최소화 원칙
- composable이 읽는 State의 범위를 좁게 유지
- 람다를 넘길 때 remember로 감싸서 불필요한 recompose 방지
// ❌ 매 recompose마다 새 람다 객체 생성
Button(onClick = { viewModel.onEvent(Event.Click) })
// ✅ 람다 안정화
val onClick = remember { { viewModel.onEvent(Event.Click) } }
Button(onClick = onClick)
2. remember vs rememberSaveable
remember
recomposition 사이에서 값을 유지해줘. 근데 화면 회전이나 프로세스 재시작하면 날아간다.
@Composable
fun SearchBar() {
var query by remember { mutableStateOf("") }
// 화면 회전하면 query 초기화됨
}
rememberSaveable
Bundle에 저장할 수 있는 값은 화면 회전, 시스템에 의한 재시작에서도 유지된다.
@Composable
fun SearchBar() {
var query by rememberSaveable { mutableStateOf("") }
// 화면 회전해도 query 유지됨
}
간단한 UI 변경은 remember or rememberSavable로 구현하고, 비즈니스 로직( 계정 상태 관리..)와 같은 역할은 viewmodel에서 관리할 수 있도록 설정해야한다.
3. State Hoisting — stateless composable 설계
핵심 아이디어
State를 composable 안에 두지 말고 위로 끌어올려서, composable이 값을 받고 이벤트만 위로 전달하게 만드는 패턴
// ❌ Stateful — 테스트 어렵고 재사용 불가
@Composable
fun NameInput() {
var name by remember { mutableStateOf("") }
TextField(value = name, onValueChange = { name = it })
}
// ✅ Stateless — 재사용 가능, 테스트 쉬움
@Composable
fun NameInput(
name: String, // 값은 위에서 받고
onNameChange: (String) -> Unit // 이벤트는 위로 전달
) {
TextField(value = name, onValueChange = onNameChange)
}
// 부모가 상태 관리
@Composable
fun ProfileScreen() {
var name by remember { mutableStateOf("") }
NameInput(name = name, onNameChange = { name = it })
}
왜 중요한가
- 테스트: 순수 함수처럼 입력 → 출력 구조라서 Preview, 테스트 둘 다 용이
- 재사용: 다른 화면에서도 같은 composable 쓸 수 있음
4. derivedStateOf / snapshotFlow
derivedStateOf
다른 State로부터 계산된 State가 필요할 때. 원본 State가 바뀌어도 결과값이 달라질 때만 recompose 발생.
@Composable
fun TodoList() {
val todos = remember { mutableStateListOf<Todo>() }
// ❌ todos 바뀔 때마다 매번 재계산 + recompose
val hasCompleted = todos.any { it.isDone }
// ✅ 결과값(true/false)이 바뀔 때만 recompose
val hasCompleted by remember {
derivedStateOf { todos.any { it.isDone } }
}
}
사용 시점: 리스트 스크롤 위치로 버튼 표시 여부 결정할 때 자주 씀
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
snapshotFlow
Compose State를 Flow로 변환할 때 사용. Composable 밖 (ViewModel, Repository)에서 State 변화를 관찰하고 싶을 때.
@Composable
fun SearchScreen(viewModel: SearchViewModel) {
val listState = rememberLazyListState()
// 스크롤이 끝에 가까워지면 다음 페이지 로드
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.debounce(300)
.collect { index ->
if (index > totalItems - 5) {
viewModel.loadNextPage()
}
}
}
}
실질적인 Compose의 이점
| 성능 | View 계층 구조가 없어서 measure/layout/draw 단계가 단순 |
| 개발 속도 | Live Preview, Hot Reload로 빠른 피드백 |
| 코드량 | XML + ViewBinding + Adapter 조합 대비 코드의 단순화 |
| 상태 동기화 | UI와 상태가 자동으로 동기화, 버그 발생량 감소 |
'짬짬히 기술 블로그✍️ > Android' 카테고리의 다른 글
| 코코..코루틴.. (1) | 2026.03.27 |
|---|---|
| 메모리 캐시 & 디스크 캐시 deepdive (0) | 2026.03.26 |
| hilt의 dependency injection 간단 정리 (0) | 2026.03.06 |
| Fragment랑 Compose 비교하면서 공부! (0) | 2026.03.05 |