|

Jetpack Compose LaunchedEffect와 SideEffect 차이

Compose effect 차이를 설명하는 대표 이미지
effect는 이름보다 언제 실행되고 무엇을 다루는지를 기준으로 구분해야 한다

Jetpack Compose LaunchedEffect와 SideEffect 차이는 effect API 이름만 보면 금방 헷갈립니다. 둘 다 “뭔가를 실행하는 것”처럼 보이지만 실제로는 언제 실행되는지, 코루틴이 필요한지, 무엇을 건드리는지가 다릅니다.

이번 글에서는 LaunchedEffect, SideEffect, DisposableEffect, rememberCoroutineScope를 실제 코드 예시와 함께 비교해서 언제 무엇을 쓰는 편이 맞는지 정리하겠습니다.

Compose effect 개념은 Android Developers side-effects 문서 흐름을 기준으로 보는 것이 가장 안전합니다. 이 글에서는 그 개념을 실전 예시 위주로 다시 풀어 설명합니다.


LaunchedEffect는 언제 쓸까

LaunchedEffect는 suspend 작업이나 코루틴이 필요한 side effect에 잘 맞습니다. 보통 특정 key가 바뀔 때 네트워크 요청, delay, collect 같은 비동기 작업을 시작하고 싶을 때 자연스럽습니다.

@Composable
fun UserScreen(userId: String) {
    var user by remember { mutableStateOf<User?>(null) }

    LaunchedEffect(userId) {
        user = repository.loadUser(userId)
    }

    Text(user?.name ?: "loading...")
}

이 예시에서 핵심은 userId가 바뀔 때마다 코루틴 블록이 다시 시작된다는 점입니다. 즉 LaunchedEffect는 구성 안에서 시작하는 비동기 흐름에 가깝습니다.


SideEffect는 무엇이 다를까

SideEffect는 suspend 작업을 돌리는 API가 아닙니다. recomposition이 성공적으로 끝난 뒤, Compose 바깥 세계에 현재 상태를 반영할 때 더 자연스럽습니다.

@Composable
fun AnalyticsScreen(screenName: String) {
    SideEffect {
        analytics.setCurrentScreen(screenName)
    }
}

이 코드는 화면 이름 같은 현재 상태를 외부 analytics 객체에 전달하는 데 잘 맞습니다. 즉 SideEffect는 코루틴보다 외부 시스템에 현재 값을 밀어 넣는 동기적 성격에 더 가깝습니다.


DisposableEffect는 생명주기 연결에 강하다

@Composable
fun LifecycleLogger(owner: LifecycleOwner = LocalLifecycleOwner.current) {
    DisposableEffect(owner) {
        val observer = LifecycleEventObserver { _, event ->
            println("event=$event")
        }
        owner.lifecycle.addObserver(observer)

        onDispose {
            owner.lifecycle.removeObserver(observer)
        }
    }
}

등록과 해제가 같이 있어야 하는 리스너, observer, callback 연결은 DisposableEffect가 훨씬 분명합니다. 즉 시작보다 정리 시점까지 같이 중요할 때 잘 맞습니다.


rememberCoroutineScope는 왜 따로 있을까

@Composable
fun SnackbarButton(snackbarHostState: SnackbarHostState) {
    val scope = rememberCoroutineScope()

    Button(onClick = {
        scope.launch {
            snackbarHostState.showSnackbar("saved")
        }
    }) {
        Text("save")
    }
}

사용자 클릭처럼 event handler 안에서 코루틴을 시작해야 할 때는 rememberCoroutineScope가 더 자연스럽습니다. effect API가 아니라 사용자 액션에 반응하는 코루틴 시작점으로 보면 이해가 쉽습니다.


실무에서 자주 하는 실수

  1. LaunchedEffect 안에서 현재 상태 반영만 하면 되는 일을 불필요하게 코루틴으로 처리하는 것
  2. SideEffect로 suspend 작업을 하려는 것
  3. DisposableEffect가 필요한 리스너 등록을 LaunchedEffect에 넣고 해제를 놓치는 것
  4. 버튼 클릭 로직까지 LaunchedEffect로 끌고 가는 것

핵심은 effect 이름을 외우는 것보다, 이 작업이 비동기인지, 외부 상태 반영인지, 등록/해제가 같이 필요한지를 먼저 보는 일입니다.

상태 수명 주기 쪽은 remember와 rememberSaveable 글, UI 적용 범위 쪽은 Modifier 글과 함께 보면 더 잘 이어집니다.


마무리

LaunchedEffect와 SideEffect 차이는 결국 코루틴이 필요한가, 언제 실행되는가, 무엇을 바깥으로 반영하는가의 차이에 가깝습니다. DisposableEffect와 rememberCoroutineScope까지 같이 놓고 보면 effect API들이 조금 더 선명하게 나뉘어 보입니다.

즉 Compose effect는 이름보다 실행 시점과 책임으로 구분하는 편이 훨씬 덜 헷갈립니다.

함께보면 좋은 글