본문 바로가기
Android Jetpack Compose/Jetpack Compose

LaunchedEffect , Side Effect 그리고 rememberCoroutine 정리

by Developer88 2023. 5. 12.
반응형

오늘은 Jetpack Compose 의 Side Effect 와 LaunchedEffect,

그리고 rememberCoroutine 에 대해서 정리해 보도록 하겠습니다.

 

1. Side Effect

Side effect 의 단어 뜻은 원래 부차적이고, 의도하지 않은 효과를 말하는데요.

부작용을 가르키기도 합니다.

 

Jetpack Compose 에서,

Side effectComposable 함수(의 scope)를 벗어난 곳에서 앱의 state 변경이 일어나는 것을 뜻 합니다.

참고로 State 에 대해서는 아래 글을 참조해 주세요.

>> State 를 이해하고 TextField 구현하기 # Jetpack Compose UI Part2

 

공식문서의 언급된 것에 따르면, Composable 의 이상적인 형태는 원래 side-effect 일어나지 않아야 합니다.

Composable 의 State이 Composable 함수의 범위 바깥에서 변경된다는 것 이니까요.

 

하지만 예외는 언제나 필요한 것이지요.

이러한 Side Effect 가 필요한 곳으로는 다음과 같은 경우들이 있습니다.

  • 한번만 일어나는 UI이벤트로 변경사항이 State로 관리될 필요가 없는 경우
    • SnackBar, ToastMessage 등등
  • 다른 Screen 으로 이동하는 Navigation
    • Navigation 도 State에 의해서 관리될 필요없이 한번만 일어나면 되는 케이스임
  • system services 들 과 interacting 하는 것
  • Coroutine 을 이용한 네트워킹이나 디스크 IO

 

 

이제 SideEffect 와 그 용도에 대해서 알아보았으니,

그것을 구현할 때 사용할 LaunchedEffect, rememberCoroutineScope 들의 API 에 대해서 알아보겠습니다.

 

2. Launched Effect

Launched Effect 는 Composable 함수 안에서,

Coroutine 의 suspend 함수를 실행할 수 있도록 해 줍니다.

Coroutine 코드를 통해서 State 값에 영향을 줄 수 있게 됩니다.

 

아래 3가지 경우, LaunchedEffect 의 Coroutine이 Launch 되어집니다.

  • Composable 의 composition 시작 (단순하게 애기해서 UI 화면이 시작되는 것)
  • state 값의 변화에 따른 Composable 의 recomposition 시작(State값이 변화되서 UI가 recompose 되는 것)
  • LaunchedEffect의 인자로 들어오는 key값변화 (LaunchedEffect 함수는 인자로 Key를 받는데 그것이 변하는 것)

 

 

composition이 종료되면 Coroutine은 Cancel 이 되어지구요.

recompose가 되거나, key값이 변화하면,

cancel 된 후 다시 launch 되어 집니다.

 

공식문서를 보면 아래와 같이 key값을 첫번째 인자로 받도록 되어 있습니다.

 

 

코드를 보면서 정리해 보겠습니다.

state 로 관리되는 text 는 네트워크에서 새로운 데이터를 가져와서 state값이 변경되고 recomposition이 일어나게 됩니다.

네트워크에서 데이터를 불러오는 일은, 비동기로 Coroutine 에서 실행되어야 하는 것 이지요.

그래서 LaunchedEffect()에서 실행되도록 해야합니다.

 

@Composable
fun MyComposable() {
    var text by remember { mutableStateOf("") }
    LaunchedEffect(Unit) {
        text = fetchNetworkData()
    }
    Text(text)
}

 

 

2-1. 한번만 실행되게 하는 경우

Coroutine 코드가 한번만 실행되게 한다는 말은,

LaunchedEffect 가 다시 Launch 되지 않게 한다는 말인데요.

위에서 본 것처럼, 다시 launch 되게 하려면, Recompose 되거나 key값에 변화가 있어야 합니다.

 

이 중 key값에 변화를 주지 않아서,

최초 한번만 실행되도록 하게 하는 것 입니다.

key 값에 변화를 주지 않게 하기 위해서는,

Unit 이라든가 Boolean의 true 같이 static 한 값을 주면 되는데요.

이렇게 해주면 key 값에 따라서 재실행되는 일은 발생하지 않습니다.

 

Navigation 같은 경우, state로 관리할 필요없이 한번만 실행시켜주면 되는데요.

아래와 같이 LaunchedEffect 에서 실행시켜 주면 됩니다.

key 값에 따른 변화도 필요없으므로, Unit 을 인자로 넣어주었습니다.

 

@Composable
fun TestScreen(navController: NavController) {
    Text("테스트 화면")
    LaunchedEffect(Unit) {
        delay(3000)
        navController.navigate("home") {
            popUpTo("splash") { inclusive = true }
        }
    }
}

 

 

2-2. rememberUpdatedState

만약 key 값이 변해서 LaunchedEffect 가 다시 실행되더라도,

rememberUpdatedState 를 이용하면,

새롭게 초기화된 값이 아니라, 값을 계속 들고 있다가 사용할 수 있습니다.

 

아래에서는 currentCount 는,

유저가 Button을 click해서,

LaunchedEffect 의 ket값인 toggel 값이 바뀌어서,

LaunchedEffect 가 다시 실행되더라도,

계속 기존의 값을 유지시켜 증가시킬 수 있습니다.

rememberUpdatedState 을 이용하였기 때문입니다.

 

@Composable
fun SimpleExample() {
    var toggle by remember { mutableStateOf(true) }
    var count by remember { mutableStateOf(0) }

    Column {
        Text("Count: $count")
        Button(onClick = {
            toggle = !toggle
            count++
        }) { Text("키 전환과 count 증가시키기") }

        val currentCount by rememberUpdatedState(count)
        LaunchedEffect(toggle) {
            println("현재 count: $currentCount")
        }
    }
}

 

 

3. rememberCoroutineScope

위에서 보았던 LaunchedEffect는 다른 Composable 함수들안에서만 사용될 수 있습니다.

Composable 밖에서 State에 영향을 줄 수 있는,

Coroutine 을 launch 시키기 위해서는,

rememeberCoroutineScope 를 사용해 주면됩니다.

 

rememberCoroutineScope API는,

Composable 밖에서 Coroutine 을 launch 시켜주기는 하지만,

Composable의 lifecycle을 따라서 Coroutine 의 launch와 cancel 을 관리해 줍니다.

 

 

그리고 Composable 밖에서 실행된다는 부분에서 의아하실 수 있는데요.

이런 경우들은 굉장히 가까이에 있습니다.

예를 들어, 유저가 무언가를 클릭했을 때 실행되는,

onClick의 람다함수 블록도 Composable 함수의 범위를 벗어나는 곳 입니다.

 

코드를 보면서 이해해 보도록 하겠습니다.

다음은 rememberCoroutineScope()를 이용해서 네트워크에서 데이터를 가져와 text로 뿌려주는 코드입니다.

onClick의 람다함수내는 Composable 의 범위를 벗어나는 곳이므로,

rememberCoroutineScope()함수가 필요하겠지요.

 

@Composable
fun TestComposable() {
    val coroutineScope = rememberCoroutineScope()
    var text by remember { mutableStateOf("") }
    Column {
        Button(onClick = {
            coroutineScope.launch {
                text = fetchDataFromNetwork()
            }
        }) {
            Text("데이터 가져오기")
        }
        Text(text)
    }
}

 

 

4. 팁 및 주의할 점

4-1. 유저인터랙션

LaunchedEffect 는 아래 3가지의 경우 launch 되도록 설계되어 있는데요.

  • composition 시작
  • state 값의 변화에 따른 recomposition 시작
  • LaunchedEffect의 인자로 들어오는 key값변화

이 API 는 key 값이 변하거나 Composable이 composition을 떠나게 되면,

유저인터랙션 여부와 무관하게, Coroutine이 cancel 되어 초기화 되어버립니다.

원하지 않는 타이밍에 Coroutine 안의 값이 초기화 되어버릴 수 있다는 것 입니다.

 

LaunchedEffect의 key 값이 변하지 않더라도,

Composition 초기단계에 한 번만 실행된다는 점이 있기 때문에,

이 점에서도 인터랙션에 사용하기는 어려운 점이 있습니다.

 

따라서 기본적으로,

유저인터랙션 관련해서는 rememberCorotuineScope 를 이용하는 것이 좋습니다.

이것을 사용하는 것이 코드 사용시의 변수도 줄어들기 때문에 안정적입니다.

 

다만, flow를 collect 하는 곳으로서는 launchedEffect 도 무리가 없습니다.

왜냐하면, flow의 emit 이 트리거가 되어서,

side effect 가 반응하기 때문입니다.

어차피 계속 실행시키는 것이 아니라, 한번만 실행시켜두면, 알아서 트리거되어서 반응하기 때문에,

 

LaunchedEffect(Unit) {
    viewModel.testFlow.collect { data ->
        println("New data: $data")
    }
}

 

728x90

댓글