이전 글: [Android] Compose의 모든 것(6) - Stateful, Stateless, State Hoisting (feat. by remember와 Recomposition 최적화)
https://yujinius45.tistory.com/144
[Android] Compose의 모든 것(6) - Stateful, Stateless, State Hoisting (feat. by remember와 Recomposition 최적화)
이전 글: [Android] Compose의 모든 것(5) - Slot Table, Stability, Key를 활용한 Recomposition 최적화https://yujinius45.tistory.com/143 [Android] Compose의 모든 것(5) - Slot Table, Stability, Key를 활용한 Recomposition 최적화지난
yujinius45.tistory.com
먼저 읽으면 좋은 글 : [Android] Compose의 모든 것(1) - Compose, View System, Kotlin
https://yujinius45.tistory.com/139
[Android] Compose의 모든 것(1) - Compose, View System, Kotlin
시작하기에 앞서최근 Nature Album 프로젝트를 통해 Compose를 처음 학습하고 프로젝트에 적용하며 많은 것을 배울 수 있었다. 이번 글은 프로젝트에서 사용한 Compose를 제대로 알기 위해 정리 차원에
yujinius45.tistory.com

Jetpack Compose를 사용하면서 여러 곳에서 LaunchedEffect, DisposableEffect를 사용해봤다. 하지만 당시에는 이를 단순히 외부 상태와 UI를 연결하거나, 특정 작업을 처리하기 위한 도구로만 생각하고 깊이 이해하지 못한 채로 사용했었다. 이번 기회에 Compose에서 SideEffect란 무엇이며, 왜 필요하고 어떻게 사용하는지를 제대로 이해하고자 학습한 내용을 정리하였다.
SideEffect란 무엇인가?
Jetpack Compose는 선언형 UI 프레임워크로, 컴포저블 함수는 순수 함수처럼 동작하도록 설계되어 있다. 즉, 동일한 입력에 대해 항상 동일한 출력을 반환하며, 이는 UI의 예측 가능성과 재사용성을 높이는 데 중요한 역할을 한다.
이러한 철학은 다음 Compose 공식 문서에서 확인할 수 있다:
This function is fast, idempotent, and free of side-effects.
- The function behaves the same way when called multiple times with the same argument, and it does not use other values such as global variables or calls to random().
- The function describes the UI without any side-effects, such as modifying properties or global variables.
In general, all composable functions should be written with these properties, for reasons discussed in Recomposition.
또한 Compose, 함수형 프로그래밍, Kotlin 등에 대한 내용은 이전 포스팅 글인 “[Android] Compose의 모든 것(1) - Compose, View System, Kotlin” 를 읽고 오면 도움이 될 것이다.
https://yujinius45.tistory.com/139
[Android] Compose의 모든 것(1) - Compose, View System, Kotlin
시작하기에 앞서최근 Nature Album 프로젝트를 통해 Compose를 처음 학습하고 프로젝트에 적용하며 많은 것을 배울 수 있었다. 이번 글은 프로젝트에서 사용한 Compose를 제대로 알기 위해 정리 차원에
yujinius45.tistory.com
그러나 실제 애플리케이션에서는 컴포저블이 외부 시스템과 상호작용해야 하는 경우가 빈번하다. 예를 들어, 네트워크 요청, 데이터베이스 업데이트, 로그 출력 등의 작업은 컴포저블 내부 상태를 벗어나 외부 상태와의 동기화를 필요로 한다. 이러한 부수 효과(Side Effect)를 안전하고 예측 가능하게 처리하기 위해 Jetpack Compose는 여러 Effect API를 제공한다. 이와 관련된 내용은 하단의 공식 문서에서도 확인할 수 있다.
A side-effect is a change to the state of the app that happens outside the scope of a composable function. Due to composables' lifecycle and properties such as unpredictable recompositions, executing recompositions of composables in different orders, or recompositions that can be discarded, composables should ideally be side-effect free.
However, sometimes side-effects are necessary, for example, to trigger a one-off event such as showing a snackbar or navigate to another screen given a certain state condition. These actions should be called from a controlled environment that is aware of the lifecycle of the composable. In this page, you'll learn about the different side-effect APIs Jetpack Compose offers.
참고 링크: https://developer.android.com/develop/ui/compose/side-effects?utm_source=chatgpt.com
왜 SideEffect가 필요한가?
Jetpack Compose에서 SideEffect는 다음과 같은 이유로 필요하다:
- 재구성(Recomposition) 관리
- 컴포저블 함수는 입력 상태가 변경될 때마다 재구성될 수 있다. 이 과정에서 부수 효과를 관리하지 않으면, 동일한 작업(예: 네트워크 호출)이 중복 실행될 위험이 있다.
- UI와 외부 시스템 간 동기화
- 외부 시스템(네트워크, 데이터베이스 등)과 UI 상태를 일관성 있게 동기화하려면, 컴포저블 생명주기에 맞는 적절한 작업 처리가 필요하다.
- 리소스 정리 및 관리
- 컴포저블이 컴포지션에서 제거될 때, 리스너 해제나 리소스 정리와 같은 작업을 처리하지 않으면 리소스 누수가 발생할 수 있다.
[참고] 다시 보는 Composable의 생명주기
- 생성 (Entering Composition)
- Composable이 컴포지션에 진입하면 UI를 초기화하고 상태를 설정한다.
- 재구성 (Recomposition)
- 입력 데이터나 상태 변경으로 인해 UI를 다시 그린다.
- 제거 (Leaving Composition)
- Composable이 컴포지션에서 제거되면서 리소스를 정리한다.
SideEffect를 관리하기 위한 Compose의 Effect API
Jetpack Compose는 다양한 SideEffect 관리 도구를 제공한다. 주요 API와 사용 목적은 다음과 같다:
1. SideEffect
- 발생 시점:
- 컴포지션이 성공적으로 완료된 후.
- 사용 목적:
- 컴포지션이 성공적으로 완료된 상태에서 Compose 상태를 비-Compose 코드와 동기화.
- 예를 들어, 로그 출력, 전역 변수 업데이트, 외부 시스템 데이터 동기화.
- 흐름도:
1. 컴포지션 완료 (Composition Successful)
└── SideEffect 실행
└── 외부 시스템과 동기화 작업 수행
- 코드 예시:
@Composable
fun MyComposable(counter: Int) {
// UI 구성
Text("Counter: $counter")
// 컴포지션 완료 후 외부 상태 동기화
SideEffect {
println("Counter 값: $counter")
}
}
2. LaunchedEffect
- 발생 시점:
- Composable이 컴포지션에 진입하거나, key 값이 변경될 때.
- 기존 코루틴은 취소되고 새 코루틴이 시작됨.
- 사용 목적:
- 컴포지션의 생명주기 동안 비동기 작업을 안전하게 실행.
- 예: 네트워크 호출, 데이터 로드, 애니메이션 실행.
- 흐름도:
1. 컴포지션 진입 (Entering Composition)
└── LaunchedEffect 실행
└── 비동기 작업 시작
2. Key 값 변경 (Key Change)
├── 기존 작업 취소
└── 새 작업 실행
- 코드 예시:
@Composable
fun DataFetcher(dataId: Int) {
val state = remember { mutableStateOf("Loading...") }
// 데이터 로드 비동기 작업 시작
LaunchedEffect(dataId) {
val data = fetchDataFromNetwork(dataId)
state.value = data
}
Text(text = state.value)
}
3. DisposableEffect
- 발생 시점:
- 컴포지션에 진입하거나 제거될 때.
- 키 값이 변경되면 기존 작업 정리 후 재실행.
- 사용 목적:
- 컴포지션에서 리소스 초기화 및 정리 작업.
- 예: 리스너 등록/해제, 데이터 스트림 시작/중단.
- 주의 사항
Note: DisposableEffect는 반드시 onDispose 블록을 포함해야 하며, 그렇지 않으면 컴파일 타임 에러가 발생합니다. 비워진 onDispose 블록을 사용하는 것도 권장되지 않으므로, 반드시 필요한 클린업 작업을 정의하세요.
- 흐름도:
1. 컴포지션 진입 (Entering Composition)
├── DisposableEffect 실행
│ └── 리소스 초기화
2. Key 값 변경 (Key Change)
├── 기존 DisposableEffect 클린업 (onDispose 호출)
└── 새 DisposableEffect 실행
3. 컴포지션 제거 (Leaving Composition)
└── DisposableEffect 클린업 (onDispose 호출)
- 코드 예시:
@Composable
fun MyComposable() {
DisposableEffect(Unit) {
val observer = MyObserver()
observer.startObserving()
onDispose {
observer.stopObserving()
}
}
}
4. rememberUpdatedState
- 발생 시점:
- LaunchedEffect, DisposableEffect 등 키가 변경되지 않도록 하는 값을 유지할 때.
- 사용 목적:
- 값이 변경되더라도 SideEffect를 재시작하지 않고 최신 값을 유지.
- 장시간 실행되는 작업에서 비용이 큰 초기화를 방지.
- 흐름도:
1. 값 변경 (Value Change)
├── rememberUpdatedState로 최신 값 유지
└── 기존 작업 재시작 방지
- 코드 예시:
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(true) {
delay(3000)
currentOnTimeout()
}
Text("Welcome!")
}
5. produceState
- 발생 시점:
- 비-Compose 상태를 Compose 상태로 변환할 때.
- 컴포지션 진입 시 코루틴 실행, 컴포지션 종료 시 취소.
- 사용 목적:
- 외부 상태(예: Flow, LiveData, RxJava)를 Compose 상태로 변환.
- 구독 기반 데이터 소스를 사용할 때.
- 흐름도:
1. 컴포지션 진입 (Entering Composition)
└── produceState 실행
└── 코루틴 실행 및 데이터 구독 시작
2. 컴포지션 제거 (Leaving Composition)
└── 구독 해제 및 리소스 정리
- 코드 예시:
@Composable
fun loadNetworkImage(url: String): State<Result<Image>> {
return produceState(initialValue = Result.Loading, url) {
val image = loadImageFromNetwork(url)
value = if (image != null) Result.Success(image) else Result.Error
}
}
6. snapshotFlow
- 발생 시점:
- Compose 상태를 Flow로 변환할 때.
- 상태가 변경될 때 Flow에서 새 값을 방출.
- 사용 목적:
- Compose 상태를 React-like 흐름으로 처리.
- UI 상태를 외부 비동기 처리와 연결.
- 흐름도:
1. State 값 변경 (State Change)
└── snapshotFlow로 Flow 방출
└── Flow 수집기로 전달
2. Flow 연산 처리 (Flow Operators)
└── 결과에 따라 UI 상태 갱신
- 코드 예시:
@Composable
fun TrackScrollPosition(listState: LazyListState) {
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.filter { it > 0 }
.collect {
println("Scrolled past the first item!")
}
}
}
요약
| API | 발생 시점 | 사용 목적 |
| SideEffect | 컴포지션 완료 후 | 외부 상태와 동기화 |
| LaunchedEffect | 컴포지션 진입/Key 변경 시 | 비동기 작업 실행 |
| DisposableEffect | 컴포지션 진입/제거/Key 변경 시 | 리소스 초기화 및 정리 |
| rememberUpdatedState | 값 변경 시 | SideEffect 내부에서 최신 값 유지 |
| produceState | 외부 데이터 상태를 Compose 상태로 | Flow, LiveData, RxJava 등과 통합 |
| snapshotFlow | Compose 상태 변경 시 | Compose 상태를 Flow로 변환 |
각 API는 특정 사용 사례와 생명 주기에 적합하게 설계되었으며, 적절히 사용하면 안정적이고 예측 가능한 UI를 구현할 수 있다.
참고 링크 : https://developer.android.com/develop/ui/compose/side-effects?hl=ko
상황에 맞는 SideEffect API를 선택해보자
간단하게 언제 무엇을 쓸지 정리해봤다.
- SideEffect는 외부 상태와의 동기화가 필요한 경우에 사용.
- LaunchedEffect는 비동기 작업 실행 시 적합.
- DisposableEffect는 리소스 정리가 필요한 작업에 사용.
SideEffect 없이 처리한다면 어떻게 되는가?
SideEffect 없이 외부 상태를 변경하거나 동기화 작업을 처리하면 다음과 같은 문제가 발생할 수 있다:
- 중복 작업: 컴포저블 재구성 시 동일한 작업이 반복 실행되어 불필요한 자원 낭비가 발생한다.
- 리소스 누수: 컴포지션 제거 시 리소스를 정리하지 않으면 메모리 누수가 발생할 수 있다.
- 상태 불일치: UI와 외부 시스템 간의 상태가 동기화되지 않아 불일치가 발생한다.
SideEffect 사용 시 유의사항
- 필요한 곳에만 사용하기
- 모든 컴포지션에 SideEffect를 사용하는 것은 바람직하지 않다. 꼭 필요한 작업에만 사용하여 부수 효과를 최소화해야 한다.
- key 값을 신중하게 설정하기
- LaunchedEffect나 DisposableEffect에서 사용하는 key 값은 재구성 조건을 결정하므로, 의도한 동작을 보장하도록 적절히 설정해야 한다.
- LaunchedEffect(true) 를 특히 주의하자
Warning: LaunchedEffect(true) is as suspicious as a while(true). Even though there are valid use cases for it, always pause and make sure that's what you need.
마무리
Jetpack Compose에서 SideEffect는 선언형 UI 철학을 유지하면서도 외부 상태와의 상호작용을 안전하게 처리할 수 있도록 돕는 중요한 도구이다. 컴포저블의 생명주기를 고려한 SideEffect 관리로 예측 가능한 UI를 유지하고, 부수 효과로 인한 문제를 예방할 수 있다.