안녕하세요. 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가 있는 경우에 사용되는EffectComposable의Lifecycle에 맞춰 정리되어야 하는Listener나 작업이 있는 경우에Listener나 작업을 제거하기 위해 사용되는Effect- ⚠️
Lifecycle에 따라Side Effect를 발생시킨 다음 정리되어야 하는 부분이 많을 경우 제대로Side Effect에 대한 정리를 하지 않으면Memory Leak(메모리 누수)가 발생할 수 있음 - 첫 번쨰 인자
key👉key값이 바뀔때 마다effect호출 - 두 번째 인자
effectEffect블럭은 처음에는 초기화 로직만 수행하고 이후에는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이 성공적으로 되었을 때 발생하는EffectCompose에서 관리하지 않는 객체와Compose내부의 데이터를 공유하기 위해 사용- ⚠️
SideEffect의 한계점SideEffect로 수행하는Effect는Composable이Dispose될 때 정리가 불가능SideEffect는LaunchedEffectorDisposableEffect로 충분히 대체 가능
예제 👇
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()
}
}
}