컴포지션에 들어올 때 코루틴을 시작하고, key가 바뀌거나 떠나면 취소한다.
@Composable
fun UserScreen(userId: Long, vm: UserViewModel) {
LaunchedEffect(userId) { // userId 가 바뀌면 이전 코루틴을 취소하고 새로 시작
vm.load(userId)
}
}
key가 전부다
- LaunchedEffect(Unit) — 컴포지션에 들어올 때 한 번만. 떠나면 취소
- LaunchedEffect(userId) — userId 가 바뀔 때마다 취소 후 재시작
- LaunchedEffect(a, b) — 둘 중 하나라도 바뀌면 재시작
// ❌ 매 리컴포지션마다 취소·재시작된다
LaunchedEffect(vm.uiState) { ... } // 상태 객체가 매번 새 인스턴스
// ❌ 값이 바뀌어도 반영되지 않는다
LaunchedEffect(Unit) { vm.load(userId) } // 다른 사용자로 이동해도 안 다시 부른다
스낵바 · 스크롤처럼 화면에 묶인 작업
LaunchedEffect(state.errorMessage) {
state.errorMessage?.let {
snackbarHostState.showSnackbar(it) // suspend 함수를 안전하게 부른다
vm.onErrorShown()
}
}
rememberUpdatedState — 재시작 없이 최신 값을 쓴다
@Composable
fun Splash(onTimeout: () -> Unit) {
val current by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) { // 타이머는 한 번만 돈다
delay(3000)
current() // 그런데 최신 콜백을 부른다
}
}
onTimeout 을 key 로 주면 콜백 람다가 매번 새로 만들어져 타이머가 계속 리셋된다 key 없이 쓰면 옛 콜백을 붙잡는다
- rememberUpdatedState 가 정확히 이 간극을 메운다
언제 쓰지 않나
버튼 클릭 같은 '사용자 이벤트' 에는 쓰지 않는다
- LaunchedEffect 는 '컴포지션 진입/키 변경' 이 트리거다
- 콜백 안에서는 rememberCoroutineScope 를 쓴다
비즈니스 데이터 로딩은 ViewModel 의 init 이나 stateIn 이 더 낫다
- LaunchedEffect 는 화면 재진입마다 다시 돈다
면접 함정
- ❌
LaunchedEffect에서 상태를 직접 바꾼다 → 그 상태를 읽는 곳이 다시 그려지고 다시 효과가 돌아 무한 루프가 될 수 있다. - ❌ "코루틴이니 아무 데서나 시작해도 된다" → 컴포지션을 떠날 때 취소되는 것이 핵심 가치다.
실행 순서
컴포지션 → 레이아웃 → 드로잉 → (프레임 커밋) → LaunchedEffect 블록 실행
즉, 첫 화면이 그려진 '뒤에' 돈다
- 초기 데이터 로딩을 여기 두면 빈 화면이 한 프레임 보인다
- ViewModel 의 init 이나 stateIn 이 더 빠르다
화면 재진입마다 다시 돈다
Navigation 으로 상세 → 목록 → 상세 로 오가면
- LaunchedEffect(Unit) 이 매번 다시 실행된다
- 로그를 두 번 찍고, API 를 두 번 친다
ViewModel 은 그 목적지 범위라 함께 재생성되지만, 상위 그래프 범위 ViewModel 이면 init 은 한 번만 돈다
- "한 번만" 이 중요하면 ViewModel 쪽이 안전하다
무한 루프를 만드는 형태
// ❌ 효과가 상태를 바꾸고, 그 상태가 key 라서 효과가 다시 돈다
LaunchedEffect(count) { count++ }
// ❌ 매 리컴포지션마다 새 객체가 key 로 들어간다
LaunchedEffect(listOf(a, b)) { ... } // listOf 가 매번 새 인스턴스
// ✅ 원시 값이나 안정적인 참조를 key 로
LaunchedEffect(a, b) { ... }
snapshotFlow — 상태를 Flow로
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { analytics.log("scrolled_to", it) }
}
Compose 상태를 Flow 연산자(debounce · distinctUntilChanged) 로 다루고 싶을 때 쓴다