컴포지션에 값을 기억시켜 리컴포지션마다 다시 만들지 않게 한다.
var text by remember { mutableStateOf("") } // 리컴포지션을 넘는다
var text by rememberSaveable { mutableStateOf("") } // 구성 변경·프로세스 죽음도 넘는다
세 가지 죽음
| 리컴포지션 | 구성 변경 | 프로세스 죽음 | |
|---|---|---|---|
| 일반 변수 | ✗ | ✗ | ✗ |
| remember | ✓ | ✗ | ✗ |
| rememberSaveable | ✓ | ✓ | ✓ |
| ViewModel | ✓ | ✓ | ✗ |
key로 무효화한다
@Composable
fun UserDetail(userId: Long) {
// userId 가 바뀌면 새로 계산한다. 안 주면 옛 사용자 데이터가 남는다
val formatted = remember(userId) { heavyFormat(userId) }
}
remember { } 는 key 가 없다 = 컴포지션에 남아 있는 한 절대 다시 계산하지 않는다
- 파라미터에 의존하는 값이면 반드시 key 를 준다
- 이것이 "목록을 스크롤했더니 옛 항목 데이터가 보인다" 의 흔한 원인
무거운 계산을 감싼다
// ❌ 리컴포지션마다 정렬한다 (스크롤 중 초당 수십 번)
val sorted = items.sortedBy { it.name }
// ✅ items 가 바뀔 때만
val sorted = remember(items) { items.sortedBy { it.name } }
rememberSaveable의 제약
// Bundle 에 담을 수 있는 타입만 가능하다
@Parcelize data class Filter(val q: String, val tag: String?) : Parcelable
var filter by rememberSaveable { mutableStateOf(Filter("", null)) }
// 그 외에는 Saver 를 직접 준다
val saver = listSaver<Range, Int>(
save = { listOf(it.start, it.end) },
restore = { Range(it[0], it[1]) }
)
var range by rememberSaveable(stateSaver = saver) { mutableStateOf(Range(0, 10)) }
Bundle 크기 한도(약 1MB) 를 공유한다
- 목록 데이터를 rememberSaveable 에 담으면 TransactionTooLargeException
- 큰 것은 ViewModel + DB, 작은 것만 여기
면접 함정
- ❌ "remember를 쓰면 상태가 유지된다" → 컴포지션을 떠나면(조건부로 사라지면) 값도 사라진다.
- ❌ "rememberSaveable이면 ViewModel이 필요 없다" → 비즈니스 상태와 UI 요소 상태는 자리가 다르다.
컴포지션을 떠나면 사라진다
if (expanded) {
var draft by remember { mutableStateOf("") } // 접었다 펴면 초기화된다
TextField(draft, { draft = it })
}
remember 는 '컴포지션에 남아 있는 동안' 만 값을 보관한다 조건부로 사라지는 컴포저블 안의 상태는 그때마다 초기화된다
유지해야 한다면 상태를 위로 올린다 (호이스팅)
remember vs derivedStateOf vs LaunchedEffect
-
remember(key) { 계산 } — key 가 바뀔 때 다시 계산한다. 동기 계산
-
derivedStateOf { 계산 } — 입력 상태가 바뀔 때 재평가하되, 결과가 바뀔 때만 알린다
-
LaunchedEffect(key) { } — suspend 작업. key 가 바뀌면 취소 후 재시작
-
셋을 헷갈려 remember 안에서 네트워크를 부르는 코드가 흔하다
- remember 는 컴포지션 중에 즉시 실행된다 = 메인 스레드 블로킹
mutableStateOf의 세 가지 정책
mutableStateOf(0) // 구조적 동등성 (기본)
mutableStateOf(0, referentialEqualityPolicy()) // 참조 동등성
mutableStateOf(list, neverEqualPolicy()) // 항상 변경으로 본다
// 원시 타입 전용 — 박싱을 피한다
mutableIntStateOf(0) · mutableFloatStateOf(0f) · mutableLongStateOf(0L)
기본 정책은 equals 로 비교해 같으면 아예 알리지 않는다
- data class 를 copy 로 같은 값으로 만들면 리컴포지션이 안 돈다 (의도된 최적화)