-
Kotlin Flow와 StateFlow로 안드로이드 상태 관리 제대로 하기카테고리 없음 2026. 4. 4. 16:47

Kotlin Flow가 필요한 이유
안드로이드 앱을 개발하다 보면 UI 상태를 반응형으로 관리해야 하는 상황이 자주 발생합니다.
기존 LiveData만으로는 복잡한 비동기 흐름을 처리하는 데 한계가 있었고, 특히 여러 스트림을 조합하거나 변환하는 작업이 번거로웠습니다.
Kotlin Flow는 코루틴 기반의 비동기 데이터 스트림으로, 이 문제를 우아하고 간결하게 해결해 줍니다.Flow, SharedFlow, StateFlow의 차이
Kotlin의 Flow 계열에는 세 가지 주요 타입이 있으며, 각 타입은 용도와 동작 방식이 다릅니다.
상황에 맞게 올바른 타입을 선택하는 것이 안정적인 앱 아키텍처의 출발점입니다.- Flow: Cold Stream입니다. 수집(collect)이 시작될 때 데이터 생성이 시작되며, 일회성 데이터 요청이나 파이프라인 처리에 적합합니다.
- SharedFlow: Hot Stream입니다. 여러 구독자에게 이벤트를 브로드캐스트할 때 사용하며, 토스트 메시지나 네비게이션 이벤트 전달에 유용합니다.
- StateFlow: Hot Stream이며 항상 최신 상태 값을 보유합니다. UI 상태 관리에 가장 적합한 타입입니다.
Flow는 terminal operator(collect, first, toList 등)를 호출해야 실행되며, StateFlow는 초기값을 반드시 가지고 항상 마지막 값을 유지한다는 점이 핵심 차이입니다.
세 타입의 차이를 명확히 이해해야 각각을 올바른 문맥에서 사용할 수 있습니다.ViewModel에서 StateFlow 사용하기
ViewModel에서 StateFlow를 사용하면 UI 상태를 체계적이고 일관되게 관리할 수 있습니다.
MutableStateFlow로 내부 상태를 관리하고, 외부에는 읽기 전용StateFlow만 노출하는 패턴이 표준으로 권장됩니다.
상태를 data class로 모델링하면 모든 상태 변화를 한 곳에서 추적할 수 있어 디버깅이 쉬워집니다.data class UiState( val isLoading: Boolean = false, val items: List<String> = emptyList(), val errorMessage: String? = null ) class MainViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> = _uiState.asStateFlow() fun loadData() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } try { val result = repository.fetchItems() _uiState.update { it.copy(isLoading = false, items = result) } } catch (e: Exception) { _uiState.update { it.copy(isLoading = false, errorMessage = e.message) } } } } }update함수는 스레드 안전하게 상태를 변환해주므로, 동시 업데이트가 발생하더라도 안전하게 처리됩니다.
로딩, 성공, 에러 상태를 하나의 data class에 통합하면 UI 렌더링 로직이 단순해집니다.Jetpack Compose에서 StateFlow 수집하기
Jetpack Compose에서는
collectAsStateWithLifecycle()을 사용해 StateFlow를 구독하는 것이 권장됩니다.
이 방식은 Lifecycle을 인식해 앱이 백그라운드 상태일 때 자동으로 수집을 멈추고 리소스를 절약합니다.lifecycle-runtime-compose라이브러리를 Gradle에 추가하면 즉시 사용할 수 있습니다.// build.gradle.kts implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")@Composable fun MainScreen(viewModel: MainViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() when { uiState.isLoading -> CircularProgressIndicator() uiState.errorMessage != null -> ErrorView(message = uiState.errorMessage!!) else -> ItemList(items = uiState.items) } }collectAsState()대신collectAsStateWithLifecycle()을 사용하는 것이 중요합니다.
후자는 앱이 백그라운드로 전환될 때 불필요한 리컴포지션을 방지해 성능 측면에서도 유리합니다.기존 View 시스템에서 StateFlow 수집하기
Fragment나 Activity 기반의 기존 View 시스템에서도 StateFlow를 안전하게 수집할 수 있습니다.
repeatOnLifecycle을 사용하면 UI가 화면에 보일 때만 데이터를 수집하고, 백그라운드에서는 코루틴이 자동으로 중단됩니다.
이전에 많이 사용하던lifecycleScope.launchWhenStarted는 Deprecated 상태이므로 반드시repeatOnLifecycle로 전환해야 합니다.class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> updateUI(state) } } } } private fun updateUI(state: UiState) { progressBar.isVisible = state.isLoading recyclerView.isVisible = !state.isLoading && state.errorMessage == null errorTextView.isVisible = state.errorMessage != null errorTextView.text = state.errorMessage } }launchWhenStarted는 코루틴을 일시 중단(suspend)만 하고 취소하지 않아 메모리 낭비가 발생할 수 있습니다.repeatOnLifecycle은 Lifecycle 상태에 따라 코루틴을 완전히 취소하고 재시작하므로 훨씬 안전합니다.마무리 — StateFlow 도입 체크리스트
Kotlin Flow와 StateFlow는 현대 안드로이드 개발에서 핵심적인 상태 관리 도구로 자리잡았습니다.
올바른 패턴으로 사용하면 버그가 적고 테스트하기 쉬운 코드를 작성할 수 있습니다.
LiveData에서 StateFlow로 전환을 고려하고 있다면 아래 체크리스트를 참고해 보세요.- ViewModel에서
MutableStateFlow를private으로 선언했는가? - UI에는
asStateFlow()로 변환한 읽기 전용 Flow를 노출하는가? - Compose라면
collectAsStateWithLifecycle()을 사용하는가? - View 시스템이라면
repeatOnLifecycle을 사용하는가? - 에러 상태도 UiState data class에 포함했는가?
StateFlow로 상태 관리를 단일화하면 예측 가능한 UI와 명확한 데이터 흐름을 동시에 얻을 수 있습니다.
다음 포스팅에서는 MVI(Model-View-Intent) 아키텍처와 StateFlow를 결합한 고급 패턴을 살펴보겠습니다.