Development/Android

StateFlow, SharedFlow에 대해서 쉽게 이해하기

Jamie 2026. 3. 31. 13:22
반응형

안녕하세요. Jamie입니다.

 

이번 포스팅은 Flow에 대해서 쉽게 이해할 수 있도록 정리해보았습니다.

Flow는 현재 Android 앱 개발에서는 Coroutine과 함께 비동기 + 스트림 처리에는 필수적으로 사용되는 스택입니다.

 

 

우선 Flow에는 크게 3대 구성요소가 있습니다.

Flow의 3대 구성요소

  • 생산자 (Producer)
  • 중간 연산자(Intermediary)
  • 소비자 (Consumer)

 

이 요소들은 다음과 같은 특징이 있습니다.

  1. Producer (생산자): 데이터를 스트림에 흘려보냅니다.
    • flow { ... } 블록 안에서 emit()을 통해 데이터를 전달합니다. (예: Repository 레이어의 DB 쿼리 결과)
  2. Intermediary (중개자, 중간 연산자): 흘러가는 데이터를 가공합니다.
    • map, filter, take, zip 등 익숙한 연산자들이 여기 해당하며, 코루틴 위에서 동작하므로 이 안에서 다른 suspend 함수를 호출할 수 있습니다.
  3. Consumer (소비자): 데이터를 최종적으로 받아서 처리합니다.
    • collect를 호출하는 시점에 비로소 Flow가 동작하기 시작합니다. (예: ViewModel이나 UI 레이어)

 

간단한 예시를 통해 이해하기 쉽게 설명하겠습니다.

// 1. 생산자 (Producer): 1초마다 숫자를 하나씩 내보냄
val numberFlow: Flow<Int> = flow {
    for (i in 1..5) {
        delay(1000)
        emit(i) // 데이터를 스트림에 흘려보냄
    }
}

// 2. 중개자 (Intermediary): 짝수만 골라내고 값을 10배로 뻥튀기
val transformedFlow = numberFlow
    .filter { it % 2 == 0 } // 짝수만 통과 (필터)
    .map { it * 10 }        // 값 변환 (가공)


// 3. 소비자 (Consumer): 변환된 데이터를 받아서 UI에 출력
suspend fun displayNumbers() {
    transformedFlow.collect { value ->
        println("최종 받은 값: $value") // 소비 (사용)
    }
}

 

코드를 보고 설명하자면 생산자, 중개자, 소비자의 개념을 아래와 같이 정리할 수 있습니다.

  • 생산자: 데이터를 만들어 emit 합니다. 보통 Repository 레이어에서 서버나 DB 데이터를 가져올 때 담당합니다.
  • 중개자: 데이터를 소비하기 전, 입맛에 맞게 수정합니다. (Optional, 생략 가능)
  • 소비자: collect를 호출하여 데이터를 실제로 사용합니다. 주로 UI(Activity/Fragment)나 ViewModel에서 이 역할을 수행합니다.

 

 

자 Flow의 3대 구성 요소를 설명했으니 이제 조금 더 중요한 부분에 대해 설명하겠습니다.

Flow는 기본적으로 Stream 데이터를 2가지로 구분하게 됩니다.

 

Cold Stream vs Hot Stream

Cold Flow (flow { ... })

  • 특징: 소비자가 collect를 호출할 때마다 생산자가 새로 시작됩니다.
  • 비유: CD 플레이어. 내가 재생 버튼(collect)을 눌러야만 음악이 처음부터 나옵니다.
  • 용도: 데이터베이스 쿼리, 네트워크 요청 등 일회성 스트림.

Hot Flow (StateFlow, SharedFlow)

  • 특징: 소비자가 있든 없든 데이터를 계속 생산하고 있거나, 상태를 유지합니다. 여러 소비자가 하나의 스트림을 공유할 수 있습니다.
  • 비유: 라디오 방송. 내가 언제 라디오를 켜든(collect) 현재 방송 중인 내용을 듣게 됩니다.
  • 용도: UI 상태 관리(StateFlow), 클릭 이벤트나 푸시 알림(SharedFlow).

 

그림으로 쉽게 설명해보겠습니다. Gemini 나노바나나로 제작했습니다.

 

 

StateFlow와 SharedFlow의 예제 코드입니다.

class MainViewModel : ViewModel() {

    // 1. StateFlow: UI 상태 관리 (초기값 필수)
    // 화면을 돌리거나 다시 들어와도 마지막 상태를 유지하고 보여줌
    private val _uiState = MutableStateFlow(MainUiState())
    val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()

    // 2. SharedFlow: 일회성 이벤트 관리
    // 관찰자가 있을 때만 전달, 처리 후 재발생하지 않음 (이벤트 유실 주의 전략 필요)
    private val _eventFlow = MutableSharedFlow<MainEvent>()
    val eventFlow: SharedFlow<MainEvent> = _eventFlow.asSharedFlow()

    // 상태 변경 로직
    fun incrementCounter() {
        _uiState.update { it.copy(count = it.count + 1) }
    }

    // 이벤트 발생 로직
    fun showToast() {
        viewModelScope.launch {
            _eventFlow.emit(MainEvent.ShowSnackbar("데이터가 업데이트되었습니다!"))
        }
    }
}

// 상태 데이터 클래스
data class MainUiState(
    val count: Int = 0,
    val isLoading: Boolean = false
)

// 이벤트 정의 (Sealed Interface로 깔끔하게)
sealed interface MainEvent {
    data class ShowSnackbar(val message: String) : MainEvent
    object NavigateToDetail : MainEvent
}

 

@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
    val context = LocalContext.current
    
    // StateFlow 수집: UI 상태는 Lifecycle을 따르며 항상 최신 값을 유지함
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // SharedFlow 수집: 일회성 이벤트는 LaunchedEffect 내에서 처리
    LaunchedEffect(Unit) {
        viewModel.eventFlow.collect { event ->
            when (event) {
                is MainEvent.ShowSnackbar -> {
                    Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
                }
                MainEvent.NavigateToDetail -> { /* 화면 이동 로직 */ }
            }
        }
    }

    // UI 레이아웃
    Column {
        Text(text = "현재 카운트: ${uiState.count}")
        Button(onClick = { viewModel.incrementCounter() }) {
            Text("카운트 증가")
        }
        Button(onClick = { viewModel.showToast() }) {
            Text("토스트 띄우기")
        }
    }
}

 

StateFlow와 SharedFlow를 정리하자면 다음과 같습니다.

비교 포인트 StateFlow SharedFlow
성격 State (상태) Event (이벤트)
초기값 필수 (initialValue) 없음
Sticky 기능 항상 마지막 값을 들고 있음 (새 구독자에게 즉시 전달) 기본적으로 없음 (설정에 따라 replay 가능)
중단 시점 UI가 멈춰도 상태를 유지함 구독자가 없으면 이벤트를 버릴 수 있음 (설정 가능)
주요 활용 텍스트 필드 값, 리스트 데이터, 로딩 상태 스낵바, 토스트, 화면 이동, 앱 종료 등
반응형