안녕하세요. Narvis2 입니다.
이번 포스팅에서는 Side-Effect
에 대하여 알아보도록 하겠습니다.
🍎 Side Effect
Composable
범위 밖에서 발생하는 앱 상태에 대한 변경Composable
은 각각의Lifecycle
을 가지고 있음Composable
은 단방향으로만State
를 전달Composable
을 사용할 떄 여러Composable
들을 겹쳐서 사용함. 그러면System
은 각Composable
에 대한Lifecycle
을 만들고Composable
별로 재구성이 필요할때만 재구성 시킨다.Composable
은 기본적으로 바깥쪽Composable
이 안쪽Composable
로State
를 내려줌. 이로 인해 단방향으로만 의존성이 생김⚠️ 하지만, 만약 안쪽에 있는
Composable
에서 바깥쪽에 있는Composable
의 상태에 대한 변경을 준다면?? 혹은Composable
에서Composable
이 아닌 앱State
에 대한 변화를 준다면?? 👇- 양방향 의존성으로 인해 예측할 수 없는
Effect
가 생긴다. 이를Side Effect
라 부름
- 양방향 의존성으로 인해 예측할 수 없는
🍎 Side Effect 처리하기
LaunchedEffect
👉Composable Lifecycle Scope
에서suspend fun
을 실행하기 위해 사용DisposableEffect
👉Composable
이Dispose
될 때 정리되어야 할Side Effect
를 정의하기 위해 사용SideEffect
👉Composable
의State
를Compose
에서 관리하지 않는 객체와 공유하기 위해 사용Compose
는 위 3가지와 함께 사용할 수 있는 여러CoroutineScope
와State
관련 함수를 제공rememberCoroutineScope
👉Composable
의CoroutineScope
를 참조하여 외부에서 실행할 수 있도록 해줌rememberUpdatedState
👉Launded Effect
는Composable
의State
가 변경되면 재실행되는데 재실행되지 않아도 되는State
를 정의하기 위해 사용produceState
👉Compose State
가 아닌 것을Compose
의State
로 변환derivedStateOf
👉State
를 다른State
로 변환하기 위해 사용,Composable
은 변환된State
에만 영향을 미침snapshotFlow
👉Composable
의State
를Flow
로 변환
🍀 1. Launched Effect
Composable
에서Composition
이 일어날 때suspend fun
을 실행해주는Composable
임- ⚠️
Recomposition
은Composable
의State
가 바뀔때마다 일어나므로Recomposition
이 일어날때마다 이전Launched Effect
가 취소되고 다시 수행된다면 매우 비효율적✅ 이를 해결하기 위해
LaunchedEffect
는key
라 불리는 기준값을 두어key
가 바뀔때만LaunchedEffect
의suspend fun
을 취소하고 재실행함
예제 👇
LaunchedEffect
에서 한번만 실행되어야 하는 동작 처리
- 한번만 실행해야 하는 경우
key
값에true
나Unit
을 넘겨주는 방향으로 설계
1
2
3
4
5
6
@Composable
fun KotlinWorldScreen(oneTimeEffect: () -> String) {
LaunchedEffect(true) {
onTimeEffect()
}
}
예제 👇
LaunchedEffect
에서 한번만 실행되어야 하는데 동작이 길때
- 긴 동작의 람다식을 처리할 때 👉
rememberUpdatedState
를 사용하여launch
를 기억해야 함
1
2
3
4
5
6
7
fun KotlinWorldScreen(longTimeJob: suspend () -> String) {
val rememberLongTimeJob by rememberUpdatedState(longTimeJob)
LaunchedEffect(true) {
println(rememberLongTimeJob())
}
}
🍀 2. Disposable Effect
Composable
이Dispose
된 후에 정리해야 할Side Effect
가 있는 경우에 사용되는Effect
Composable
의Lifecycle
에 맞춰 정리되어야 하는Listener
나 작업이 있는 경우에Listener
나 작업을 제거하기 위해 사용되는Effect
- ⚠️
Lifecycle
에 따라Side Effect
를 발생시킨 다음 정리되어야 하는 부분이 많을 경우 제대로Side Effect
에 대한 정리를 하지 않으면Memory Leak(메모리 누수)
가 발생할 수 있음 - 첫 번쨰 인자
key
👉key
값이 바뀔때 마다effect
호출 - 두 번째 인자
effect
Effect
블럭은 처음에는 초기화 로직만 수행하고 이후에는key
값이 바뀔때마다onDispose
블록을 호출한 후 초기화 로직을 다시 호출함onDispose
블록의return
값이 바로DisposableEffect
여서onDispose
블록은effect
람다식의 맨 마지막에 무조건 와야함
예제 👇
1
2
3
4
5
6
DisposableEffect(key) {
// Composable 이 제거될 때 Dispose 되어야 하는 효과 초기화
onDispose {
// Composable 이 Dispose 될 때 호출되어 Dispose 되어야 하는 효과 제거
}
}
예제 👇
- 사용자의 사용 패턴 분석을 위한 로깅 (
Activity
의onStart()
에서 시작되어onStop()
에서 끝나야 함)Lifecycle
이 바뀔 때 새로운Observer
가Lifecycle
에 붙어 변화를 구독하고Composable
이 제거될 떄Observer
또한 제거되도록 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
_onStartLogging: () -> Unit,
_onStopLogging: () -> Unit,
) {
val startLoggingOnStart by rememberUpdatedState(_onStartLogging)
val stopLoggingOnStop by rememberUpdatedState(_onStopLogging)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
startLoggingOnStart()
} else {
stopLoggingOnStop()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
🍀 3. SideEffect
Composable
의Composition
이 성공적으로 되었을 때 발생하는Effect
Compose
에서 관리하지 않는 객체와Compose
내부의 데이터를 공유하기 위해 사용- ⚠️
SideEffect
의 한계점SideEffect
로 수행하는Effect
는Composable
이Dispose
될 때 정리가 불가능SideEffect
는LaunchedEffect
orDisposableEffect
로 충분히 대체 가능
예제 👇
FocusRequester
의requestFocus
는Composable
이 아닌System
의Event
이므로Composable
이 관리하는Event
가 아님- 따라서,
Composable
의 구성이 완료된 이후에requestFocus
가 호출되도록 보장하려면SideEffect
를 사용해야 함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Composable
fun HomeScreen() {
var isVisible by remember { mutableStateOf(false) }
// Composable 이 아닌 System의 Event
val focusRequester = remember { FocusRequester() }
Column (modifier = Modifier.fillMaxSize()) {
Button(onClick = { isVisible = true }) {
Text(text = "버튼 클릭")
}
if (isVisible) {
OutlinedTextField(
modifier = Modifier.fillMaxSize()
.focusRequester(focusRequester),
value = "",
onValueChange={}
)
}
}
SideEffect {
if (isVisible) {
focusRequester.requestFocus()
}
}
}