Logo
Droid Lab by Bonny

Compose Side Effect에서 내가 잘 못 이해한 것들 🙅‍♀️

2023년 4월

최근 회사에서도 사이드 프로젝트에서도 대부분의 UI를 Jetpack Compose 로 작성하고 있습니다. UI에 동작을 추가하기 위해 SideEffect 들을 자주 사용하게 되는데, 공식 문서만 읽었을 때 확실치 않은 부분들이 있어 LaunchedEffect, DisposableEffect 에서 궁금한 것들을 실험해보았습니다. 그 과정에서 Side Effect들에 대하여 잘 못 이해하고 있던 것들이 있어 이를 공유해보려합니다.

🤔 LaunchedEffect, DisposableEffect 는 언제 실행될까?

저는 LaunchedEffect(Unit) 를 딱 한 번 기록되어야 할 로깅 등 Composable 첫 시작시 1회 수행되어야 할 동작을 담곤 했습니다. 이 첫 1회 라는 개념이 애매하게 느껴져 여러 경우를 테스트 해보았습니다. 첫 1회라는 것이 Composable 이 로직 흐름상 처음 호출되는 순간일 수도, Compose UI 트리에 추가되는 순간일수도 있다고 생각했기 때문입니다.

그래서 Greeting 이라는 Composable 함수에 아래 같이 LaunchedEffect, DisposableEffect를 추가했습니다.

show 버튼으로 Greeting Composable의 visibility를, change버튼으로 Greeting 에 전달할 파라미터의 값을 변경할 수 있습니다. 화면으로는 아래 그림처럼 생겼습니다.

2023 03 check side effect

@Composable
fun Greeting(name: String) {
  Text(text = "Hello $name!")

  LaunchedEffect(Unit) {
    Log.d("SideEffectTest", "LaunchedEffect")
  }

  DisposableEffect(name) {
    Log.d("SideEffectTest", "DisposableEffect")

    onDispose {
      Log.d("SideEffectTest", "DisposableEffect - OnDispose")
    }
  }
}

Greeting 외부는 이렇습니다.

var showGreeting by remember { mutableStateOf(true) }
var name by remember { mutableStateOf(Math.random().toString()) }

Column {
  if (showGreeting) {
    Greeting(name)
  }
  Button(onClick = {
    showGreeting = !showGreeting
    Log.d("SideEffectTest", "Show Clicked!")
  }) {
    Text("show")
  }
  Button(onClick = {
    name = Math.random().toString()
    Log.d("SideEffectTest", "Change Clicked!")
  }) {
    Text("change")
  }
}

예상했듯이 앱 실행 시에 Greeting 컴포넌트가 그려지면서 LaunchedEffect가 실행됩니다. 이 후 show 버튼이 클릭되어 dispose가 실행되고 다시 show 버튼이 클릭된 후 LaunchedEffect가 시작됩니다.

2023 03 check side effect2

그 외에 눈여겨 볼 것은,

1. if 조건에 의해 Composable이 컴포즈 UI Tree에서 삭제되었다가 다시 추가될 경우 LaunchedEffect 가 기록된다.

이 부분이 제가 가장 잘 못 이해하고 있던 부분이라고 생각합니다. LaunchedEffect 기록 시점을 Composable 함수의 로직상 첫 호출이라고 생각해 if 조건에 의해서 LaunchedEffect는 찍히지 않을 것이라 생각했습니다. 하지만 show 버튼에 의해 화면에 보이지 않게 되면 onDispose 이벤트도 찍히고 다시 화면에 보이게 되면 LaunchedEffect가 찍히는 것을 볼 수 있습니다.

2. DisposableEffect의 onDispose 콜백 외부 로그(테스트 결과의 1, 6 라인)는 LaunchedEffect가 시작되는 시점에 같이 로깅되고 있습니다.

이는 DisposableEffect 내부 코드를 살펴보면 DisposableEffectImpl이 RememberObserver의 onForgotten 뿐만 아니라 onRemembered API를 활용하고 있는 것을 통해 코드로도 확인할 수 있습니다.

2023 03 check side effect3

3. (그 외) 전체 스크린 사이즈 리사이징에 의한 화면 변화에는 configChanges 설정에 따라 LaunchedEffect가 실행될수도 안될 수도 있다.

처음 테스트에서는 앱을 분할화면으로 열어 앱 스크린 사이즈를 조절할 때 마다 onDispose와 LaunchedEffect가 찍혔습니다. 하지만 이는 스크린 크기 조정에 따른 configurationChange 로 Composable을 담은 Activity가 재시작되기 때문이었습니다. AndroidManifest 에서 configChanges 수정을 통해 스크린 사이즈에 의해 Activity가 재시작되지 않게 설정하면 LaunchedEffect는 실행되지 않습니다.

2023 03 check side effect4

🤔 LaunchedEffect(Unit) 을 여러번 선언하면 같은 코루틴 스콥일까?

LaunchedEffect와 DisposableEffect의 차이점을 꼽자면 두 콜백의 실행시점도 있겠지만, coroutineScope이냐 아니냐도 있습니다. 아래 각 함수의 선언부에서 이 차이를 바로 볼 수 있습니다.

LaunchedEffect의 선언부

2023 03 check side effect5

DisposableEffect의 선언부

2023 03 check side effect6

여기에서 LaunchedEffect를 한 Composable 내에서 여러 번 선언할 경우 같은 코루틴 스콥을 공유하는지 궁금했습니다. 사실 이 부분은 LaunchedEffect 내부 코드를 보면 아니라는 것을 쉽게 알 수 있습니다.

아래와 같이 부모의 CoroutineContext를 사용하지만 LaunchedEffectImpl 클래스 생성 시마다 새 CoroutineScope을 생성하고 있습니다.

LaunchedEffectImpl 생성시 부모의 coroutienContext를 사용하는 모습 2023 03 check side effect7

LaunchedEffectImpl 내부 변수로 CoroutineScope을 생성하는 모습 2023 03 check side effect8

위에 사용하였던 Greeting Composable을 아래처럼 변형하여 Scope을 한 번더 로깅해보았습니다.

@Composable
fun Greeting(name: String) {
  Text(text = "Hello $name!")

  LaunchedEffect(Unit) {
    Log.d("SideEffectTest", "LaunchedEffect 1")
    Log.d("SideEffectTest", this.coroutineContext.toString())
  }

  LaunchedEffect(Unit) {
    Log.d("SideEffectTest", "LaunchedEffect 2")
    Log.d("SideEffectTest", this.coroutineContext.toString())
  }
}

CoroutineContext에서 StandaloneCoroutine 객체만이 다른 것을 볼 수 있습니다.

2023 03 check side effect9

테스트가 끝나고 보니 LaunchedEffect에 전달한 key값이 변할 때마다 해당 코루틴을 cancel하고 재실행해야 하는 LaunchedEffect의 기본 동작을 생각하면 코루틴 스콥을 공유하지 않아야 한다는 것을 쉽게 생각할 수 있었습니다.

✍️ 결론

궁금한 것들을 떠오르는 대로 살펴보다 보니 흐름이 자연스럽게 진행된 글은 아닌 것 같습니다. 그래도 여러 애매한 것들을 테스트하고 코드를 쫓아가다 보니 각 기능이 정확히 어떤 기능인지 더 이해하게 된 것 같습니다. 테스트해 본 SideEffect 들 외에도 derivedStateOf 등 도 파봐야겠네요!

추천
클린 아키텍쳐를 지향해야하는 이유
클린 아키텍쳐를 지향해야하는 이유
2024년 1월
MVC, MVP, MVVM, MVI 아키텍쳐 안드로이드 코드로 알아보기 - 1
MVC, MVP, MVVM, MVI 아키텍쳐 안드로이드 코드로 알아보기 - 1
2023년 11월
안드로이드에서 graphql 시작하기🛫
안드로이드에서 graphql 시작하기🛫
2023년 2월
Android에서 Open API Generator 사용해보기 ⚙️
Android에서 Open API Generator 사용해보기 ⚙️
2022년 12월
목록으로 돌아가기
Loading script...
Designed by Tony / Written by Bonny