LaunchedEffect, Flow는 뭔가?

이윤진·2024년 7월 28일
0

Android 개발

목록 보기
11/13

프로젝트 GitHub 링크

한 애플리케이션에서 여러 플랫폼의 웹툰을 볼 수 있는 프로젝트를 개발하던 중 발생한 일입니다.

웹뷰를 이용하여 네이버 웹툰처럼 요일 별, 웹툰을 클릭했을 때 웹뷰를 통해 그 화면으로 넘어갑니다.

	fun moveToWeb(work: Work){
        val urlMap = mapOf(
            "네이버" to "https://m.comic.naver.com/search/result?keyword=${work.title}",
            "카카오페이지" to "https://page.kakao.com/search/result?keyword=${work.title}",
            "리디" to "https://ridibooks.com/search?q=${work.title}&adult_exclude=n",
            "레진코믹스" to "https://www.lezhin.com/ko/search?t=all&q=${work.title}",
            "봄툰" to "https://www.bomtoon.com/search?q=${work.title}",
            "기타" to work.url
        )

        val intent = Intent(context, ViewerActivity::class.java)
        intent.putExtra("url", urlMap[work.platform])
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        startActivity(context, intent, null)
    }

플랫폼에 따라 미리 설정해둔 url을 통해 웹툰을 보여줍니다.

화면의 하단을 보면 <-, <, > 버튼이 있는 것을 알 수 있습니다.
이는 웹뷰에서 웹뷰 나가기, 이전 화면 이동, 다음 화면 이동 기능을 제공하기 위한 버튼입니다.
웹뷰의 이전 화면으로 가기 위해 안드로이드의 뒤로 가기 버튼을 누르면 웹뷰에서 나가지기 때문에 이 부분을 보완하기 위해 버튼을 만들었습니다.

이 버튼을 만들기 위해 LaunchedEffect를 사용하였다.

LaunchedEffect?

: composable 함수에서 side effect를 처리하기 위한 도구
(side effect - composable 함수의 리컴포지션과 별개로 실행되는 코드. 비동기 작업이나 상태 변화와 같은 작업을 수행할 때 사용)

LaunchedEffect는 매개변수로 key 값을 가집니다. 그리고 key 중 하나가 변경될 때마다 블록 내의 동작을 취소하고, 다시 실행합니다.

저는 LaunchedEffect의 key 값으로 Unit을 넣어주었습니다.

	LaunchedEffect(Unit) {
        viewModel.undoSharedFlow.collectLatest {
            if (webView.canGoBack()) {
                webView.goBack()
            } else {
                Toast.makeText(context, "뒤로 갈 수 없습니다.", Toast.LENGTH_SHORT).show()
            }
        }
    }

Unit?

package kotlin

/**
 * The type with only one value: the `Unit` object. This type corresponds to the `void` type in Java.
 */
public object Unit {
    override fun toString(): String = "kotlin.Unit"
}

Unit의 코드를 확인하면 다음과 같습니다.
주석에는 java의 void와 대응되는 타입으로, 하나의 값만 가진다고 설명하고 있습니다.

🤔 그러면 왜 void가 아닌 Unit을 만들었는지 궁금해졌습니다.

https://medium.com/@sandeepkella23/everything-about-unit-in-kotlin-e40a01829098
링크의 게시글에서 그 이유를 찾을 수 있었습니다.

  • Unit은 하나의 값을 가지는 실제의 타입입니다.
  • Unit은 싱글톤 객체입니다.
  • kotlin에서 아무것도 반환하지 않는 함수의 리턴 타입은 Unit입니다.
  • java에서의 void는 타입이 아닌 아무것도 반환하지 않는다는 키워드입니다.

이러한 차이 때문에 kotlin에서는 변수에 expression (식)을 자유롭게 넣을 수 있고, 이를 매개변수로 받을 수 있습니다.


다시 LaunchedEffect 이야기로 돌아와서, LaunchedEffect의 key 값을 Unit으로 설정하면 컴포저블 수명 주기 동안 1번만 side effect을 트리거할 수 있습니다.

컴포저블(composable) 수명 주기

  • composition : 앱의 UI를 설명하는 것. composable을 실행하면 생성됩니다.
  • recomposition : 변경될 수 있는 composable을 다시 실행 후, composition을 업데이트하는 것.
  • composition은 UI를 설명해야 하기 때문에, 호출하는 composable을 추적합니다. 만약 앱 상태가 변경되었다면, recomposition을 예약하여 업데이트 합니다.

따라서 컴포저블의 생명 주기는 다음과 같이 설명할 수 있습니다.

  1. 구성 (처음 composable이 실행되어 UI 트리가 생성되는 단계)
  2. 재구성 (기존 UI 트리를 변경하여 새로운 상태를 반영)
  3. 제거 (UI 트리가 제거되는 단계)

웹뷰에서 이전 화면으로 돌아가고, 다음 화면으로 넘어가는 것은 화면은 바뀔지라도 UI 구성에서 변화가 생기지 않습니다.
또한 저는 Flow를 이용하여 명령을 수집할 것이기 때문에 Flow를 collect하도록 하는 함수를 한 번만 실행시키면 됩니다.

Flow?

: 코틀린의 코루틴 라이브러리에서 제공하는 비동기 데이터 스트림 처리를 위한 API

  • cold stream : 구독자가 collect 함수를 호출하여 데이터를 요청할 때 데이터 생산이 시작됨. -> 구독자가 없으면 데이터 생성하지 않음.
  • suspend 함수 내에서 사용
  • map,filter, take 등의 연산자를 통해 데이터 스트림 변환 가능 (중간 연산자)
  • collect, toList, single 등의 연산자를 사용하여 데이터 스트림 수집, 처리 가능
  • 순차적으로 비동기적으로 처리됨
    -> 데이터를 순서대로 처리함, 각 데이터 처리 사이에 suspend를 사용할 수 있다.

알람이 왔다는 사실을 알리기 위해 MutableStateFlow를 사용하는 것은 적절하지 않습니다.
(MutableStateFlow - stateFlow의 변형. 읽기와 쓰기 모두 가능)
왜냐하면 MutableStateFlow은 같은 값을 연속해서 보내면 구독자가 이를 인식하지 않기 때문입니다. 이를 사용하게 되면 webView가 실행되고 딱 1번만 MutableStateFlow 값을 인식하게 됩니다.
따라서 우리는 sharedFlow를 사용해야 합니다.

SharedFlow?

: 여러 수집기에게 동일한 데이터를 전송할 수 있는 데이터 스트림

  • hot stream : 구독자가 없더라도 데이터 발행 가능
  • concurrency : 여러 코루틴이 동시에 데이터 수집, 처리 가능

마찬가지로 MutableSharedFlow는 SharedFlow의 변형으로 데이터 일기와 쓰기 모두 가능합니다.

MutableSharedFlow는 동일한 값을 발행하여도 구독자가 모두 수신하기 때문에 이벤트 처리에 더 적합합니다.

따라서 ViewModel에선 아래와 같이 코드를 작성하였습니다.

	private val _undoSharedFlow = MutableSharedFlow<Unit>()
    val undoSharedFlow = _undoSharedFlow.asSharedFlow()

    private val _redoSharedFlow = MutableSharedFlow<Unit>()
    val redoSharedFlow = _redoSharedFlow.asSharedFlow()

    fun undo() {
        viewModelScope.launch {
            _undoSharedFlow.emit(Unit)
        }
    }

    fun redo() {
        viewModelScope.launch {
            _redoSharedFlow.emit(Unit)
        }
    }

이젠 이전 화면, 다음 화면으로 잘 이동할 수 있습니다.😁

이벤트가 발생할 때마다 값을 바꿔 입력하기는 번거로우니까, 그냥 MutableSharedFlow를 사용하자!

profile
Android/Flutter 개발

0개의 댓글