[Android]Jetpack Compose 에서 collectAsState 활용하기

KIMGEUNTAE·2025년 3월 4일
0

Android

목록 보기
11/11

안드로이드 개발에서 제트팩 컴포즈(Jetpack Compose)는 UI를 구축하는 현대적인 방식을 제공합니다. 특히 상태 관리는 컴포즈 애플리케이션에서 중요한 부분인데, 이를 효과적으로 관리하기 위해 ViewModel과 함께 collectAsState()를 사용하는 방법을 알아보겠습니다.


📌 collectAsState()

collectAsState()는 Jetpack Compose에서 Flow(또는 StateFlow)를 쉽게 구독하고, 구독 결과를 Compose의 State로 변환해주는 확장 함수입니다. 이 함수를 사용하면 Flow가 새 값을 emit할 때마다 UI가 자동으로 재컴포지션되어 최신 상태를 표시해줍니다.

왜 사용 하는 가?

  • Kotlin Coroutines의 Flow는 비동기 데이터를 스트리밍 방식으로 전달합니다.
  • 하지만 Compose에서는 상태(State) 변화를 관찰해 UI를 업데이트합니다.
  • collectAsState()를 사용하면 Flow를 직접 launch해서 collect할 필요 없이, 자동으로 Flow를 구독하며 현재 값을 Compose 상태로 변환해 줍니다.

📌 ViewModel과 Flow

ViewModel에서 상태를 Flow로 노출하는 패턴

class MainViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(MyUiState())
    val uiState: StateFlow<MyUiState> = _uiState.asStateFlow()

    fun updateSomething(newValue: String) {
        _uiState.update { currentState ->
            currentState.copy(something = newValue)
        }
    }
}
  • MutableStateFlow : 내부에서 변경 가능한 상태 정의
  • StateFlow로 : 외부에는 읽기 전용

collectAsState() 이해하기

@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {

    val uiState by viewModel.uiState.collectAsState()

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Column {
            Text(text = "Current value: ${uiState.something}")

            Button(onClick = { viewModel.updateSomething("New Value") }) {
                Text("Update Value")
            }

            if (uiState.isLoading) {
                CircularProgressIndicator()
            }

            uiState.error?.let { error ->
                Text(
                    text = error,
                    color = MaterialTheme.colorScheme.error
                )
            }
        }
    }
}
  • uiState : Flow를 State로 변환
  • Compose가 자동으로 Flow 구독의 생명주기를 관리합니다.
  • 컴포저블이 Composition에서 제거될 때 자동으로 Flow 구독이 취소됩니다.

이제 uiState는 State 타입으로,
viewModel.uiState의 값이 변경될 때마다 이 컴포저블이 재구성됩니다


📌 collectAsState 없이 방식 비교

@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {

    var uiState by remember { mutableStateOf(MyUiState()) }
  
    LaunchedEffect(viewModel) {
        viewModel.uiState.collect { newState ->
            uiState = newState
        }
    }

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Column {
            Text(text = "Current value: ${uiState.something}")

            Button(onClick = { viewModel.updateSomething("New Value") }) {
                Text("Update Value")
            }

            if (uiState.isLoading) {
                CircularProgressIndicator()
            }

            uiState.error?.let { error ->
                Text(
                    text = error,
                    color = MaterialTheme.colorScheme.error
                )
            }
        }
    }
}
  • LaunchedEffect를 사용하여 수동으로 코루틴의 생명주기를 관리해야 합니다.
  • 잘못 구현하면 메모리 누수가 발생할 수 있습니다.

📌 여러 Flow 처리

collectAsState 사용:

val state1 by flow1.collectAsState()
val state2 by flow2.collectAsState()
val state3 by flow3.collectAsState()

collectAsState 없이 방식:

var state1 by remember { mutableStateOf(initialValue1) }
var state2 by remember { mutableStateOf(initialValue2) }
var state3 by remember { mutableStateOf(initialValue3) }

LaunchedEffect(Unit) {
    launch { flow1.collect { state1 = it } }
    launch { flow2.collect { state2 = it } }
    launch { flow3.collect { state3 = it } }
}
  • 여러 Flow를 처리할 때 collectAsState()를 사용하면 각 Flow를 독립적으로 관리할 수 있어 코드가 더 명확해집니다.

📌 초기값 처리

collectAsState 사용:

val state by flow.collectAsState(initial = initialValue)

collectAsState 없이 방식:

var state by remember { mutableStateOf(initialValue) }
LaunchedEffect(Unit) {
    flow.collect { state = it }
}
  • collectAsState()는 Flow에서 첫 번째 값이 수집되기 전에 표시할 초기값을 직접 지정할 수 있습니다.

📌 에러 처리

collectAsState 사용:

  • 에러 처리가 내부적으로 이루어지므로 추가 코드가 필요하지 않습니다.

collectAsState 없이 방식:

LaunchedEffect(Unit) {
    try {
        flow.collect { state = it }
    } catch (e: Exception) {
        // error
    }
}
  • Flow 수집 중 발생하는 예외를 직접 처리해야 합니다.

📌 마무리

성능 비교

메모리 사용: 두 방식 모두 비슷한 메모리 사용량을 보이지만 collectAsState()는 내부적으로 최적화되어 있어 약간 더 효율적일 수 있습니다.
재구성 효율성: collectAsState()는 Compose의 상태 시스템과 긴밀하게 통합되어 변경된 부분만 효율적으로 재구성합니다.
개발자 효율성: collectAsState()를 사용하면 개발자는 보일러플레이트 코드를 줄이고 핵심 비즈니스 로직에 집중할 수 있습니다.

사용 시나리오 비교

collectAsState가 더 적합한 경우:

  • 간단한 UI 상태 관찰
  • 여러 독립적인 Flow를 관찰할 때
  • 표준적인 패턴을 따르는 앱에서
  • 코드의 간결성과 가독성이 중요할 때

collectAsState 쓰지 않는 방식이 더 적합한 경우:

  • 복잡한 Flow 변환이나 결합이 필요할 때
  • 특별한 에러 처리가 필요한 경우
  • Flow 수집에 대한 세밀한 제어가 필요할 때
  • 특정 코루틴 컨텍스트에서 Flow를 수집해야 할 때 (물론 collectAsState(context = ...)로도 가능합니다)

결론

collectAsState()는 제트팩 컴포즈에서 Flow를 관찰하는 가장 간결하고 효율적인 방법입니다. 기존의 방식과 비교했을 때 코드가 더 간결해지고 생명주기 관리가 자동으로 이루어지며 여러 Flow를 쉽게 처리할 수 있다는 장점이 있습니다.

그러나 특별한 요구사항이 있는 경우 전통적인 방식을 사용하여 Flow 수집에 대한 더 세밀한 제어가 가능합니다. 각 방식의 장단점을 이해하고 상황에 맞게 선택하는 것이 중요 한 것 같습니다.


깃허브 : https://github.com/GEUN-TAE-KIM/collectAsState_study

profile
Study Note

2개의 댓글

comment-user-thumbnail
2025년 3월 26일

좋은글이네요. 조금 제경험을 이야기하자면
아래와 같이 통으로 뷰모델을 감시하는 경우는 실무에선 없었던거 같아요.

LaunchedEffect(viewModel) {
        viewModel.uiState.collect { newState ->
            uiState = newState
        }
    }
LaunchedEffect(Unit) {
    // 화면이 처음 구성될 때 한 번만 실행
    viewModel.loadData()
}


LaunchedEffect(userId) {
    // 특정값이 바뀔때만 실행하고 싶을때. userId가 바뀔 때마다 실행됨
    viewModel.fetchUser(userId)
}

LaunchedEffect(Unit) {
    viewModel.eventFlow.collect {
        // low나 Event 수신 (단발성 이벤트 처리) Toast, Navigation, SnackBar 등
    }
}

그리고 아래의 패턴이 많이 사용되는데 이 경우도 직접 View에 uiState를 넘기는것보다는 DTO를 만들어서 넘기는 식으로 구현했어요.
val uiState by viewModel.uiState.collectAsState()

요런식으로..

class MyViewModel : ViewModel() {
    val titleState = MutableStateFlow("Hello Compose")

    fun onClicked() {
        println("Button clicked! Business logic here")
    }
}

@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val title by viewModel.titleState.collectAsState()

    // 여기서 UI 모델을 만듦 (상태 + ViewModel 메서드 연결)
    val uiModel = MyUiModel(
        title = title,
        onClick = viewModel::onClicked
    )

    MyContent(uiModel)
}

data class MyUiModel(
    val title: String,
    val onClick: () -> Unit
)

@Composable
fun MyContent(uiModel: MyUiModel) {
    Column {
        Text(uiModel.title)
        Button(onClick = uiModel.onClick) {
            Text("Click me")
        }
    }
}
1개의 답글