기존의 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와 상태가 자동으로 동기화, 버그 발생량 감소

 

+ Recent posts