[Android] Compose Effect 비교

hyun·2024년 10월 13일

Compose Side Effect란?

Composable은 상태가 바뀔 때마다 몇 번이고 다시 실행될 수 있고(Recomposition), 실행 순서나 횟수를 개발자가 정할 수 없다.
그래서 Composable 본문에 네트워크 요청, 리스너 등록, 외부 객체 변경 같은 코드를 바로 쓰면 의도치 않게 여러 번 실행되거나 정리되지 않는 문제가 생긴다.

이렇게 Composable 함수 밖의 세계에 영향을 주는 작업을 부수 효과(Side Effect)라고 하고, Compose는 이를 안전하게 처리할 수 있도록 Effect API를 제공한다.

이 글에서는 가장 많이 쓰는 세 가지를 비교한다.

  • LaunchedEffect
  • DisposableEffect
  • SideEffect

1. LaunchedEffect

동작

  • Composable이 컴포지션에 들어갈 때 코루틴을 시작한다.
  • Composable이 컴포지션을 떠나면 코루틴이 자동으로 취소된다.
  • key가 바뀌면 기존 코루틴을 취소하고 새로 시작한다.
  • 블록 안이 코루틴 스코프라서 suspend 함수를 바로 호출할 수 있다.

사용 사례

  • 화면 진입 시 데이터 불러오기
  • 특정 값(id 등)이 바뀔 때 다시 요청하기
  • delay를 이용한 타이머, 자동 넘김
  • Flow 수집, 스낵바 표시처럼 시간이 걸리는 UI 작업

예시

@Composable
fun UserScreen(userId: Long, viewModel: UserViewModel) {
    // userId가 바뀔 때마다 이전 요청은 취소되고 새로 요청한다
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)
    }
}

key 정리

key동작
Unit, true 같은 고정값컴포지션에 들어갈 때 한 번만 실행
userId 같은 변하는 값값이 바뀔 때마다 취소 후 재시작
여러 개 (key1, key2)하나라도 바뀌면 재시작

LaunchedEffect(Unit)도 "앱 전체에서 한 번"이 아니다.
화면 회전 등으로 컴포지션이 새로 만들어지면 다시 실행된다.
정말 한 번만 해야 하는 작업은 ViewModel의 init이나 Application에서 처리하는 게 맞다.


2. DisposableEffect

동작

  • Composable이 컴포지션에 들어갈 때 블록이 실행된다.
  • 블록 마지막에 반드시 onDispose { }를 반환해야 한다. (없으면 컴파일 에러)
  • Composable이 컴포지션을 떠날 때 onDispose가 실행된다.
  • key가 바뀌면 onDispose가 먼저 실행되고, 그다음 블록이 다시 실행된다.
  • 코루틴이 아니라 일반 블록이라 suspend 함수는 호출할 수 없다.

사용 사례

등록하면 반드시 해제해야 하는 것들에 쓴다.

  • 이벤트 리스너, 콜백 등록/해제
  • LifecycleObserver 등록/해제
  • 센서, 위치 업데이트 시작/중지
  • BroadcastReceiver 등록/해제

예시

@Composable
fun LocationTracker(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit,
    onStop: () -> Unit
) {
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_START -> onStart()
                Lifecycle.Event.ON_STOP -> onStop()
                else -> Unit
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)

        // 컴포지션을 떠나거나 lifecycleOwner가 바뀌면 해제된다
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

onStart, onStop처럼 바뀔 수 있는 람다를 오래 쓰는 경우에는 rememberUpdatedState로 감싸서 항상 최신 람다를 호출하게 하는 것이 안전하다.


3. SideEffect

동작

  • 리컴포지션이 성공적으로 끝날 때마다 매번 실행된다.
  • key가 없다.
  • 코루틴이 아니며, 메인 스레드에서 바로 실행된다.
  • 컴포지션이 실제로 화면에 반영된 뒤에만 실행된다. (중간에 취소된 컴포지션에서는 실행되지 않음)

사용 사례

Compose 상태를 Compose가 관리하지 않는 외부 객체에 반영(동기화)할 때 쓴다.

  • 상태바, 내비게이션 바 색상 지정
  • 애널리틱스 라이브러리에 현재 사용자 속성 전달
  • Compose 밖의 객체(View, 외부 라이브러리 설정)에 현재 상태 값 넘기기

예시

@Composable
fun StatusBarColor(color: Color) {
    val view = LocalView.current
    val darkTheme = isSystemInDarkTheme()

    if (!view.isInEditMode) {
        // color나 darkTheme이 바뀌어 리컴포지션될 때마다 최신 값으로 반영된다
        SideEffect {
            val window = (view.context as Activity).window
            window.statusBarColor = color.toArgb()
            WindowCompat.getInsetsController(window, view)
                .isAppearanceLightStatusBars = !darkTheme
        }
    }
}
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics = remember { FirebaseAnalytics.getInstance(context) }

    // 리컴포지션마다 현재 사용자 정보를 외부 객체에 맞춰준다
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

주의: SideEffect에 넣으면 안 되는 것

하고 싶은 작업SideEffect가 안 맞는 이유대신 쓸 것
한 번만 하는 초기화 (라이브러리, 블루투스 연결 등)리컴포지션마다 반복 실행된다LaunchedEffect(Unit), ViewModel init, Application
파일, DB, 네트워크에서 데이터 로딩메인 스레드에서 매번 실행되어 UI가 버벅인다LaunchedEffect + ViewModel
리스너 등록해제할 방법이 없다DisposableEffect

왜 Composable 본문에 바로 쓰지 않고 SideEffect로 감싸나?

Composable 본문은 리컴포지션 도중에 취소되거나 결과가 버려질 수 있다.
본문에서 바로 외부 객체를 바꾸면, 실제로 화면에 반영되지 않은 값이 밖으로 새어나갈 수 있다.
SideEffect는 컴포지션이 성공적으로 적용된 뒤에만 실행되기 때문에 화면과 외부 상태가 어긋나지 않는다.


결론: 한눈에 비교

구분실행 시점정리(취소) 시점코루틴주 용도
LaunchedEffect컴포지션 진입 시, key 변경 시컴포지션 이탈 시, key 변경 시 자동 취소Osuspend 작업 (데이터 요청, delay, Flow 수집)
DisposableEffect컴포지션 진입 시, key 변경 시onDispose (이탈 시, key 변경 시)X등록/해제가 짝을 이루는 작업 (리스너, Observer)
SideEffect리컴포지션 성공 시 매번없음XCompose 상태를 외부 객체에 동기화

어떤 걸 쓸지 고르는 기준

  1. suspend 함수를 호출해야 한다 → LaunchedEffect
  2. 등록했으면 반드시 해제해야 한다 → DisposableEffect
  3. 현재 상태를 Compose 밖 객체에 계속 맞춰줘야 한다 → SideEffect
  4. 한 번만 해야 하는 초기화 → Effect가 아니라 ViewModel init이나 Application

함께 알아두면 좋은 API

  • rememberCoroutineScope : 버튼 클릭처럼 Composable 밖 콜백에서 코루틴을 시작할 때
  • rememberUpdatedState : 오래 도는 Effect 안에서 재시작 없이 최신 값을 쓰고 싶을 때
  • derivedStateOf : 자주 바뀌는 상태에서 덜 자주 바뀌는 값을 계산해 리컴포지션을 줄일 때

참고

profile
Developer

2개의 댓글

comment-user-thumbnail
2024년 10월 17일

아 이건 좀 ...

1개의 답글