Jetpack Compose에서 다양한 Effect에 대한 기능을 제공한다.
@Composable이 실행될 때, 최초에 비동기적으로 실행하도록 해주는 기능도 한다.
또한, 특정 변수를 넣어 그 변수가 바뀌면 실행되도록 할수도 있으며,
@Composable이 소멸할 때도 실행되도록 할 수 있다.
어디서 본 느낌이지 않나?
React의 useEffect로 라이프사이클을 다루는 개념이다.
Jetpack compose도 이름을 유사하게 LaunchedEffect, DisposableEffect, SideEffect로 나눈 것 같다.
LaunchedEffect(true){}
, LaunchedEffect(Unit){}
, LaunchedEffect{}
, 다 같은 표현이다.
위 방식으로 쓴 LaunchedEffect는 @Composable이 최초 렌더링할 때만 1회 실행된다.
이후 mutableStateOf 같은 상태변수의 값이 바뀌어서 리컴포지션(리렌더링)이 되어도 LaunchedEffect의 콜백함수는 실행되지 않는다.
리액트의 useEffect로 willMount를 구현한 것과 유사하다.
LaunchedEffect(변수){}
위 방식은 내부변수(상태값)가 바뀔때도 실행된다.
리액트의 useEffect로 update 라이프사이클을 구현한 것과 유사하다.
LaunchedEffect에 onDispose가 추가된 Effect이다.
기본적으로 LaunchedEffect의 최초실행, 내부변수 감지도 지원하지만, 소멸될 때 실행되는 기능을 추가한 것이다.
DisposableEffect(Unit) {
Log.d("MainComposable", "First Rendering")
onDispose {
Log.d("MainComposable", "On Dispose")
}
}
코드의 영역(역할)을 나누어 정리해보면 다음과 같다.
LaunchedEffect(/*감지할 변수*/) {
// 최초 렌더링 시 실행할 코드
DisposableEffect(/*감지할 변수*/) {
// 최초 렌더링 시 실행할 코드
onDispose {
// 소멸 시 실행할 코드
}
}
이러면 DisposableEffect만 써도될 것 같은데, 왜 LaunchedEffect를 따로 만들었는지는 의문이다.
ChatGPT는 이렇게 말한다.
왜 두개를 따로 굳이 구분한 걸까?
LaunchedEffect는 특정 키가 변경될 때마다 코루틴을 사용하여 비동기 작업을 실행하는 데 사용됩니다. 이는 주로 데이터 로드, 네트워크 요청, 애니메이션 시작과 같은 비동기 작업에 적합합니다.
DisposableEffect는 컴포저블이 처음으로 렌더링될 때 실행되고, 컴포저블이 사라질 때 정리 작업을 수행하는 데 사용됩니다. 이는 주로 리소스 정리, 리스너 등록/해제, 리소스 할당/해제와 같은 작업에 적합합니다.
기능을 분리해서 사용하기 쉽도록 두 가지로 만들어 둔 것 같다.
보통 페이지를 최초에 들어가면 API를 호출하니까(Get 요청을 많이 하겠지?) 그땐 LaunchedEffect를 쓰는 것이다.
그리고 센서정보 등록 같은 리스너 등록/해제를 할때는 DisposableEffect를 쓰는 것이다.
아래 코드를 보면 key1, key2, key3이 아니라 keys로 확장되어 구현되어 있기에, 여러개를 넣고싶으면 파라미터를 여러개로 넣으면 된다.
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
key2: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1, key2) { LaunchedEffectImpl(applyContext, block) }
}
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
key2: Any?,
key3: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1, key2, key3) { LaunchedEffectImpl(applyContext, block) }
}
@Composable
@NonRestartableComposable
@Suppress("ArrayReturn")
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
vararg keys: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(*keys) { LaunchedEffectImpl(applyContext, block) }
}
사실 @Composable 안에서는 Non-Composable 함수를 쓰면 안되는 철칙이 있다.
@Composable
fun MyComposable() {
// 잘못된 예시: Non-Composable 함수 호출
val data = fetchDataFromDatabase() // 비컴포저블 함수 호출
Text(text = data)
}
fun fetchDataFromDatabase(): String {
// 데이터베이스에서 데이터 가져오기 (비컴포저블 함수)
return "Data from database"
}
Non-Composable 함수는 Composable 함수와 달리 Compose의 순수성을 보장하지 않는다.
즉, Non-Composable 함수의 호출 결과는 Compose의 리컴포지션에 영향을 주지 않는다.
이는 Compose의 성능 및 예측 가능성에 영향을 줄 수 있.
또한, Non-Composable 함수의 결과로 불러온 데이터가 업데이트될 때마다 Compose가 알지 못하고 리컴포지션을 하지 않는다.
이는 UI의 상태와 데이터의 불일치를 초래할 수 있다.
그래서 위의 코드는 다음으로 고칠 수 있다.
@Composable
fun MyComposable() {
var data by remember { mutableStateOf("") }
SideEffect {
// 비컴포저블 함수 호출
data = fetchDataFromDatabase()
}
// UI 렌더링
Text(text = data)
}
fun fetchDataFromDatabase(): String {
// 데이터베이스에서 데이터 가져오기 (비컴포저블 함수)
return "Data from database"
}