24.02.12 StateFlow SharedFlow

KSang·2024년 2월 16일
0

TIL

목록 보기
58/101

뷰모델을 다루다보면 계속 듣던 StateFlow SharedFlow에 대해 알아보려고한다.

그 동안 라이브 데이터를 사용해서 구현했는데

현업에선 StateFlow SharedFlow만 쓰고 라이브 데이터는 이제 잘 안쓴다는 얘기를 들었다.

이 둘의 라이브 데이터와 다른점은 코루틴에서 작동한다는점으로 알고 있는데

정확히 차이가 뭐고 라이브 데이터 대신 사용하는 이유가 뭘까?

StateFlow

StateFlow는 상태 관리에 최적화되어 있다.

StateFlow는 항상 초기 상태를 가지고, 구독자에게 최신 상태를 즉시 제공하는데 주요 특징은 상태의 현재 값에 항상 접근할 수 있다는점이다.

vs LiveData

그래서 라이브 데이터와의 차이점이 뭔데?

라이브 데이터와의 가장 큰 차이점은 결국 코루틴이다

코루틴을 사용하기 때문에

높은 수준의 비동기 처리, 에러 핸들링, 그리고 데이터 스트림의 세밀한 제어가 가능해서

복잡한 데이터처리를 고효율로 가독성 좋게 처리할 수 있다.

라이브 데이터 같은 경우 간단한 데이터 바인딩이나 생명주기에 민감한 UI 업데이트가 필요한 경우에 사용한다고 한다.

Flow는 코루틴을 사용하기 때문에 백그라운드 에서도 데이터 스트림의 수집을 계속할수 있다.

라이브 데이터는 생명주기에 따라가기 때문에 메모리 누수에 대한 걱정이 없는데 Flow는 백그라운드에서 데이터 스트림을 수집할때 메모리 누수가 발생 할 수 있어서 코루틴 스코프를 관리해 줘야한다.

결국 Flow의 강점은 코루틴을 통한 비동기 처리와 에러 핸들링을 보다 세밀하게 제어하는데 있다.

StateFlow와 SharedFlow는 데이터 스트림을 더 세밀하게 제어할 수 있는 다양한 연산자를 제공하는데, 예를 들어, filter, map, debounce 등의 연산자를 이용해 데이터 스트림을 변환하고, 특정 조건에 따라 업데이트를 방출할 수 있어 복잡한 데이터 처리 로직을 구현할 때 매우 유용하다.

코루틴을 점점 사용하는 현업 특성상 Flow는 선호 될 수 밖에 없다.

예제

여기 앱에 사용자가 검색어를 입력하고, 해당 검색어에 기반한 결과를 비동기적으로 가져와 표시하는 기능이 있다고 치자. 이때, 사용자가 입력하는 동안 너무 많은 네트워크 요청을 방지하고, 최신 검색어에 대한 결과만 표시하고 싶을때 어떻게 해야할까

LiveData

class SearchViewModel : ViewModel() {
    private val searchRepository = SearchRepository()
    private val _searchQuery = MutableLiveData<String>()
    private val handler = Handler(Looper.getMainLooper())
    private var searchRunnable: Runnable? = null

    // 검색 결과를 저장하는 LiveData
    val searchResults = Transformations.switchMap(_searchQuery) { query ->
        liveData {
            if (query.length >= 3) {
                val result = searchRepository.getSearchResults(query)
                emit(result)
            }
        }
    }

    fun setSearchQuery(query: String) {
        searchRunnable?.let { handler.removeCallbacks(it) }

        searchRunnable = Runnable {
            _searchQuery.value = query
        }

        // 사용자 입력 후 300ms 딜레이를 주는 디바운싱 로직
        handler.postDelayed(searchRunnable!!, 300)
    }
}

사용자가 값을 입력할때 마다 비동기 호출을 하면 굉장히 빠르게 변하는 데이터를 계속 서버에 보내서 호출을 하는것은 비효율 적이니, 라이브 데이터로 그부분을 해결하기 위해선 별도의 핸들러나 타이머 로직을 구현해서 수동으로 디바운싱 로직을 만들어야한다

그리고 Transformations.switchMap을 사용해서 LiveData의 각 값 변화에 대해 새로운 LiveData를 생성하고, 새로운 값이 들어올 때마다 이전 LiveData의 구독을 중단하고 새로운 LiveData의 구독을 시작하는 방식으로, 마지막 입력에 대한 결과만 UI에 반영할 수 있다.

StateFlow

class SearchViewModel : ViewModel() {
    private val searchRepository = SearchRepository()

    // 사용자 입력을 관리하는 StateFlow
    private val _searchQuery = MutableStateFlow("")

    // 검색 결과를 관리하는 Flow
    val searchResults = _searchQuery
        .debounce(300) // 사용자 입력 후 300ms 동안 대기
        .filter { query ->
            // 너무 짧은 검색어는 무시
            query.length >= 3
        }
        .flatMapLatest { query ->
            // 최신 검색어에 대한 검색 결과만 가져옴
            searchRepository.getSearchResults(query)
        }
        .asLiveData() // UI에서 관찰하기 위해 LiveData로 변환

    fun setSearchQuery(query: String) {
        _searchQuery.value = query
    }
}

플로우를 사용하게 되면 제공하는 기능 debounce로 쉽게 디바운싱 로직을 구현할수 있고

flatMapLatest 연산자는 각각의 입력에 대해 새로운 Flow를 생성하고, 새로운 입력이 들어올 때마다 이전에 생성된 Flow를 취소하고 최신 Flow의 결과만을 계속해서 방출하는데.
이는 특히 검색 기능 같이 사용자의 입력이 빠르게 변할 때 유용하며, 사용자가 마지막으로 입력한 검색어에 대한 결과만을 얻기 위해 사용된다.

SharedFlow

StateFlow는 상태 관리에 최적화된 플로우인데 SharedFlow는 이벤트 관리에 최적화 되어 있다.

SharedFlow는 구성 가능한 버퍼링, 배압(backpressure) 전략, 그리고 중복 데이터 전송 제어 같은 고급 기능을 제공해 여러 구독자가 동일한 이벤트 시퀀스를 관찰할 수 있게 한다.

상태와 이벤트

StateFlow나 SharedFlow를 구분하고 사용하기 위해선 상태와 이벤트를 명확히 구별할줄 알아야 한다.

둘의 차이는 쉽게 말해서 상태는 "현재 어떤 상태인가?"에 초점을 맞추며, 이벤트는 "무엇이 발생했나?"에 대한 반응을 다룬다

  • 상태(State)

    • 네트워크 연결 상태, 사용자 로그인 상태, UI의 현재 표시 내용 등
    • 상태는 시간에 따라 변할 수 있지만, 어느 한 시점에서는 정적이다.
      상태는 보통 최신 값만 중요하며, 상태의 변화(업데이트)가 있을 때마다 관련된 컴포넌트(예: UI)를 업데이트하는 데 사용된다.
  • 이벤트(Event)

    • 이벤트는 애플리케이션에서 발생하는 특정 액션이나 사건, 사용자의 클릭, 네트워크 요청의 완료, 메시지 수신 등
    • 이벤트는 일시적이며, 발생 순간에만 중요하다.
      이벤트는 상태와 달리 지속적으로 유지되지 않으며, 이벤트의 발생을 감지하고 적절한 반응을 하는 로직이 필요하다.

예제

class EventViewModel : ViewModel() {
    // SharedFlow 설정: 최대 3개의 이벤트를 버퍼링하고, 추가 이벤트 발생 시 가장 오래된 이벤트를 드랍
    private val _userEvents = MutableSharedFlow<String>(
        replay = 0,
        extraBufferCapacity = 3,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    val userEvents: SharedFlow<String> = _userEvents.asSharedFlow()

    fun sendUserEvent(event: String) {
        viewModelScope.launch {
            _userEvents.emit(event) // 이벤트 방출
        }
    }
}

MainActivity: 이벤트 구독 및 처리

class MainActivity : AppCompatActivity() {
    private val eventViewModel: EventViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 첫 번째 구독자
        lifecycleScope.launch {
            eventViewModel.userEvents.collect { event ->
                showToast("첫 번째 구독자: $event")
            }
        }

        // 두 번째 구독자
        lifecycleScope.launch {
            eventViewModel.userEvents.collect { event ->
                showToast("두 번째 구독자: $event")
            }
        }
    }

    private fun showToast(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
}

이 예제에서는 SharedFlow를 사용하여 EventViewModel 내에서 이벤트를 발생시키고, MainActivity에서 이 이벤트를 구독하여 받은 메시지를 TextView에 표시하는 방식을 보여준다.

SharedFlow를 사용함으로써, 이벤트를 여러 구독자에게 동시에 방송할 수 있고, 각 구독자는 이벤트를 독립적으로 처리할 수 있다

이벤트의 버퍼링과 오버플로우 정책을 세밀하게 설정할 수 있어, 이벤트 스트림의 흐름을 효과적으로 제어할 수 있는 등 이벤트 처리에 특화 되어있다.

0개의 댓글