Jetpack Compose의 사이드 이펙트(Side Effects)
Jetpack Compose의 사이드 이펙트(Side Effects)
Jetpack Compose에서 사이드 이펙트(side effect)란, 컴포저블 함수의 범위를 벗어나 리컴포지션(Recomposition) 이후에도 지속되는 모든 작업을 의미합니다. 컴포저블 함수는 본질적으로 입력 상태에 대한 순수 함수여야 하며, 동일한 상태가 주어지면 항상 동일한 UI를 생성해야 합니다. 하지만 실제 앱 개발에서는 외부 시스템과의 상호작용, 코루틴 실행, 리스너 등록, 혹은 Compose 외부의 상태와 동기화하는 작업이 필요한 경우가 많습니다. 이러한 상황을 위해 Compose는 컴포지션 생명주기와 통합되는 전용 사이드 이펙트 API를 제공합니다. 이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- Compose 리컴포지션 맥락에서 사이드 이펙트가 무엇인지 정의할 수 있습니다.
LaunchedEffect가 코루틴 생명주기를 컴포지션 진입 및 키 변경에 어떻게 연결하는지 설명할 수 있습니다.DisposableEffect가 리소스의 설정과 해제를 어떻게 관리하는지 이해할 수 있습니다.SideEffect를 활용하여 리컴포지션 이후 Compose 상태를 외부 시스템과 동기화하는 방법을 적용할 수 있습니다.- 작업의 요구사항에 따라 올바른 사이드 이펙트 API를 선택할 수 있습니다.
LaunchedEffect와 코루틴 스코핑
LaunchedEffect는 컴포지션에 스코핑된 코루틴을 실행합니다. LaunchedEffect가 컴포지션에 진입하면 코루틴이 시작되고, 컴포지션을 벗어나면 자동으로 취소됩니다. 키 매개변수가 변경되면 기존 코루틴은 취소되고 새로운 코루틴이 실행됩니다. 이는 Compose의 생명주기 인식 기반 코루틴 관리 메커니즘으로, 면접에서도 자주 질문되는 핵심 개념입니다.
@Composable
fun UserProfile(userId: String) {
var user by remember { mutableStateOf<User?>(null) }
// userId가 변경되면 기존 코루틴 취소 후 새 코루틴 실행
LaunchedEffect(userId) {
user = repository.fetchUser(userId) // 네트워크 요청
}
user?.let { Text(it.name) }
}
키 매개변수(userId)는 이펙트가 언제 재시작되는지를 결정합니다. userId가 변경되면 이전 사용자 데이터를 가져오던 코루틴은 취소되고, 새로운 사용자에 대한 코루틴이 시작됩니다. 리컴포지션이 발생하더라도 userId가 동일하게 유지되면 코루틴은 중단 없이 계속 실행됩니다. 이러한 특성 덕분에 LaunchedEffect는 특정 입력에 의존하는 비동기 작업에 적합합니다.
Unit을 키로 사용하면 컴포저블이 컴포지션에 진입할 때 한 번만 이펙트가 실행되고, 이후 재시작되지 않습니다.
// 화면 진입 시 한 번만 로그를 기록하는 패턴
LaunchedEffect(Unit) {
analytics.logScreenView("home")
}
코루틴은 Composition의 코루틴 컨텍스트에서 실행되며, 기본적으로 Dispatchers.Main을 사용합니다. 백그라운드 작업이 필요한 경우 블록 내부에서 withContext를 사용하여 디스패처를 전환할 수 있습니다. 면접에서 "LaunchedEffect 내부에서 네트워크 호출을 직접 하면 안 되는 이유"를 묻는다면, 기본 디스패처가 Dispatchers.Main이라는 점을 근거로 답변하시면 됩니다.
DisposableEffect와 리소스 정리
DisposableEffect는 설정(setup)과 정리(cleanup)가 모두 필요한 작업을 처리합니다. LaunchedEffect와 마찬가지로 키 매개변수를 받으며, 이펙트가 컴포지션을 벗어나거나 키가 변경되어 재시작될 때 실행되는 onDispose 블록을 제공합니다. 이 구조 덕분에 등록과 해제가 쌍으로 보장되어 리소스 누수를 방지할 수 있습니다.
@Composable
fun LocationTracker(locationManager: LocationManager) {
val callback = remember { LocationCallback() }
DisposableEffect(locationManager) {
// 설정: 위치 업데이트 등록
locationManager.requestLocationUpdates(
LocationRequest.create(), callback, Looper.getMainLooper()
)
onDispose {
// 정리: 리스너 해제 (반드시 호출됨)
locationManager.removeUpdates(callback)
}
}
}