---
id: android-compose-effect-patterns
title: Compose Effects — LaunchedEffect / DisposableEffect / etc
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [android, compose, vibe-coding]
tech_stack: { language: "Kotlin", applicable_to: ["Android"] }
applied_in: []
aliases: [LaunchedEffect, DisposableEffect, SideEffect, rememberCoroutineScope, snapshotFlow, derivedStateOf]
---

# Compose Effects

> Compose 의 lifecycle / side-effect 다루기. **LaunchedEffect (suspend), DisposableEffect (cleanup), SideEffect (every recompose), rememberCoroutineScope**. 잘못 = recomposition 폭발 / leak.

## 📖 핵심 개념
- Composable 가 pure (no side effect).
- Side effect = effect API.
- Key 가 effect re-run trigger.
- Cleanup = onDispose.

## 💻 코드 패턴

### LaunchedEffect (suspend, 1번)
```kotlin
@Composable
fun UserScreen(userId: String) {
    var user by remember { mutableStateOf<User?>(null) }
    
    LaunchedEffect(userId) {  // userId 변경 = 다시 실행
        user = api.fetchUser(userId)
    }
    
    Text(user?.name ?: "Loading...")
}
```

→ Coroutine. Composable 가 떠나면 자동 cancel.

### DisposableEffect (cleanup)
```kotlin
@Composable
fun OrientationListener() {
    val context = LocalContext.current
    
    DisposableEffect(Unit) {
        val listener = object : OrientationEventListener(context) {
            override fun onOrientationChanged(orientation: Int) { ... }
        }
        listener.enable()
        
        onDispose {
            listener.disable()
        }
    }
}
```

→ 매 외부 resource 가 cleanup.

### SideEffect (매 recompose)
```kotlin
@Composable
fun Screen(state: ScreenState) {
    SideEffect {
        // 매 successful recompose 시
        analytics.track("Screen viewed", state.toMap())
    }
}
```

→ 거의 X. Specific 외부 sync 만.

### rememberCoroutineScope
```kotlin
@Composable
fun ButtonScreen() {
    val scope = rememberCoroutineScope()
    
    Button(onClick = {
        scope.launch {
            delay(1000)
            doWork()
        }
    }) { Text("Click") }
}
```

→ Event handler 안에서 coroutine.

### LaunchedEffect vs rememberCoroutineScope
```
LaunchedEffect:
- Composable 안 (key 의존)
- Composable 떠나면 cancel
- Initial / dependency 변경 시

rememberCoroutineScope:
- Event handler (onClick)
- Composable 떠나면 cancel
- 사용자 action 시
```

### Multiple keys
```kotlin
LaunchedEffect(userId, page) {
    // userId 또는 page 변경 = re-launch
    items = api.fetchItems(userId, page)
}
```

### 함정: key 가 Unit
```kotlin
LaunchedEffect(Unit) {
    api.fetchUser(userId)  // ❌ userId 변경 시 안 다시
}

// 의도가 "1번만" = OK. 그렇지 않으면 위험.
```

### derivedStateOf
```kotlin
val items by remember { mutableStateOf(longList) }

val hasMore by remember {
    derivedStateOf { items.size > 100 }
}

// items 변경 시 만 재계산.
// hasMore 가 변경 안 = 의존 composable 재실행 X.
```

→ Computed value 의 memoize.

### snapshotFlow (state → flow)
```kotlin
LaunchedEffect(Unit) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .distinctUntilChanged()
        .collect { idx ->
            log("scrolled to $idx")
        }
}
```

→ Compose state 가 Flow.

### produceState
```kotlin
@Composable
fun loadUser(id: String): State<User?> {
    return produceState<User?>(initialValue = null, id) {
        value = api.fetchUser(id)
    }
}

@Composable
fun UserCard(id: String) {
    val user by loadUser(id)
    Text(user?.name ?: "Loading")
}
```

→ Async data → State. Custom hook 식.

### remember vs rememberSaveable
```kotlin
var count by remember { mutableStateOf(0) }
// → process death 시 잃음.

var count by rememberSaveable { mutableStateOf(0) }
// → bundle 에 저장. Process death 후 복구.
```

→ Critical state = saveable.

### Custom saver
```kotlin
val UserSaver = listSaver<User, Any>(
    save = { listOf(it.id, it.name) },
    restore = { User(id = it[0] as String, name = it[1] as String) }
)

var user by rememberSaveable(stateSaver = UserSaver) { mutableStateOf(initialUser) }
```

### LaunchedEffect with dependency on changing state
```kotlin
@Composable
fun Screen() {
    val viewModel: MyViewModel = viewModel()
    val state by viewModel.state.collectAsState()
    
    // ❌ Effect 가 state 변경 시 매번 — 의도 X.
    LaunchedEffect(state) {
        if (state.shouldShowToast) showToast(...)
    }
    
    // ✅ Discrete event 는 channel / flow.
    LaunchedEffect(Unit) {
        viewModel.events.collect { event ->
            when (event) {
                is Event.ShowToast -> showToast(event.message)
            }
        }
    }
}
```

→ Event 가 state 와 다름.

### Channel-based event
```kotlin
class MyViewModel : ViewModel() {
    private val _events = Channel<Event>()
    val events = _events.receiveAsFlow()
    
    fun login() {
        viewModelScope.launch {
            _events.send(Event.ShowToast("Logged in"))
        }
    }
}
```

→ One-shot event.

### LifecycleEventEffect (Compose 1.7+)
```kotlin
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
    log("resumed")
}

LifecycleStartEffect(Unit) {
    log("started")
    onStopOrDispose {
        log("stopped")
    }
}
```

→ Activity / Fragment lifecycle awareness.

### Pull to refresh
```kotlin
@Composable
fun ItemList(viewModel: ItemViewModel) {
    val state by viewModel.state.collectAsState()
    val pullRefreshState = rememberPullRefreshState(
        refreshing = state.isRefreshing,
        onRefresh = viewModel::refresh,
    )
    
    Box(Modifier.pullRefresh(pullRefreshState)) {
        LazyColumn { ... }
        PullRefreshIndicator(state.isRefreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
    }
}
```

### 함정: leak via remember
```kotlin
@Composable
fun Screen() {
    // ❌ Activity / Context 가 remember
    val context = LocalContext.current
    val obj = remember { ContextHelper(context) }  
    // → context 가 변경 시 stale.
}

// ✅ context 가 key
val obj = remember(context) { ContextHelper(context) }
```

### Recomposition 폭발 함정
```kotlin
@Composable
fun Screen() {
    val state by viewModel.state.collectAsState()
    
    // ❌ 매 recompose 시 새 lambda
    Button(onClick = { viewModel.action() }) { ... }
    
    // ✅ remember + ref 안정
    val onAction = remember { { viewModel.action() } }
    Button(onClick = onAction) { ... }
}
```

→ Compose Compiler (Strong skipping mode 1.6.0+) 가 자동 fix.

### State hoisting + effect
```kotlin
// Stateless composable
@Composable
fun MyButton(text: String, onClick: () -> Unit) {
    Button(onClick = onClick) { Text(text) }
}

// Stateful caller
@Composable
fun Screen() {
    var count by remember { mutableStateOf(0) }
    MyButton(text = "$count", onClick = { count++ })
}
```

→ Effect 가 stateful caller — child 는 pure.

### Compose Multiplatform (KMP)
```kotlin
// 같은 effect API, 다른 platform.
// → iOS / Android / Desktop / Web 가 같은 코드.
```

→ Compose Multiplatform 가 Compose 의 cross-platform.

### Test
```kotlin
@get:Rule
val composeRule = createComposeRule()

@Test
fun loads_user() {
    composeRule.setContent {
        UserScreen(userId = "1")
    }
    composeRule.onNodeWithText("Loading").assertExists()
    composeRule.onNodeWithText("Alice").assertExists()  // wait for effect
}
```

### Performance
```
LaunchedEffect: 매 key 변경 = re-launch.
SideEffect: 매 recompose = 호출 (성능 주의).
DisposableEffect: re-key 시 dispose + create.

→ 매 effect 의 cost 인지.
```

### iOS / SwiftUI 비교
```
iOS:
- onAppear / onDisappear
- task { }
- onChange(of:)

Compose:
- LaunchedEffect
- DisposableEffect
- key 가 변경 trigger

→ Compose 가 더 명시적 + flexible.
```

## 🤔 의사결정 기준
| 작업 | API |
|---|---|
| Async load | LaunchedEffect |
| Cleanup resource | DisposableEffect |
| Every recompose | SideEffect (rare) |
| Event handler | rememberCoroutineScope |
| Async → state | produceState |
| State → flow | snapshotFlow |
| Computed | derivedStateOf |
| Lifecycle | LifecycleEventEffect |
| State 보존 | rememberSaveable |

## ❌ 안티패턴
- **Composable 안 직접 launch**: leak.
- **LaunchedEffect(Unit) + dynamic dep**: stale.
- **State 가 event**: Channel 사용.
- **rememberCoroutineScope 가 LaunchedEffect 대체**: scope 같지만 의미 다름.
- **No key**: 매 recompose 다시 실행.
- **SideEffect 안 비싼 work**: 성능.
- **DisposableEffect cleanup 없음**: leak.

## 🤖 LLM 활용 힌트
- LaunchedEffect 가 가장 자주.
- DisposableEffect 가 외부 resource.
- Event 는 Channel / Flow (state X).
- derivedStateOf 가 computed memoize.

## 🔗 관련 문서
- [[Android_Compose_State_Hoisting]]
- [[Android_Compose_Recomposition_Pitfalls]]
- [[React_useEffect_Pitfalls]]
