
Compose에서 rememberUpdatedState가 헷갈리는 이유는 이름만 보면 무슨 상태 저장 도구처럼 보이기 때문입니다.
결론부터 말하면 이 API는 effect를 다시 시작하지 않고도 effect 안에서 최신 값이나 최신 callback을 읽고 싶을 때 씁니다.

즉 상태를 오래 보관하는 도구라기보다, 오래 살아 있는 effect가 예전 값을 붙잡는 문제를 풀어 주는 도구에 가깝습니다.
왜 이런 문제가 생길까
LaunchedEffect나 DisposableEffect는 composable 본문보다 오래 살아 있을 수 있습니다.
문제는 그 안에서 잡은 lambda나 값이, effect가 시작된 시점의 참조로 고정될 수 있다는 점입니다.
예를 들어 3초 뒤에 timeout callback을 실행한다고 해보겠습니다.
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
LaunchedEffect(Unit) {
delay(3_000)
onTimeout()
}
}처음 보면 문제 없어 보이지만, 3초 안에 recomposition이 일어나고 onTimeout 구현이 바뀌면 effect 안에서는 예전 callback을 부를 수 있습니다.
이게 실무에서 말하는 stale lambda 문제입니다.
rememberUpdatedState가 하는 일
rememberUpdatedState는 최신 값을 담아 두는 State를 만들어 줍니다.
그리고 effect 안에서는 원래 값 대신 그 State를 읽습니다.
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(3_000)
currentOnTimeout()
}
}이제 effect 자체는 다시 시작하지 않아도, 마지막에 실행되는 callback은 최신 버전이 됩니다.
핵심은 아래 두 줄입니다.
- effect의 생명주기:
LaunchedEffect(Unit)이 결정 - effect 안에서 읽을 최신 값:
rememberUpdatedState(...)가 결정
재시작 기준과 최신 참조 유지 기준을 분리해서 생각하면 rememberUpdatedState를 언제 써야 하는지가 훨씬 선명해집니다.
key를 callback까지 넣으면 안 될까
물론 가능합니다.
LaunchedEffect(onTimeout) {
delay(3_000)
onTimeout()
}하지만 이 방식은 onTimeout이 바뀔 때마다 effect를 다시 시작합니다.
이게 의도라면 괜찮습니다.
반대로 아래 상황이면 곤란합니다.
- 타이머를 처음부터 다시 세고 싶지 않다
- 네트워크 polling을 다시 열고 싶지 않다
- 등록한 listener를 다시 붙였다 떼고 싶지 않다
이럴 때는 key를 늘리는 대신 rememberUpdatedState가 더 자연스럽습니다.
대표 예시 1: splash / timeout
이 API를 가장 이해하기 쉬운 장면은 timeout입니다.
@Composable
fun SplashGate(
onTimeout: () -> Unit,
) {
val latestOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(2_000)
latestOnTimeout()
}
}여기서 우리가 원하는 것은 보통 아래 둘입니다.
- 화면에 들어왔을 때 타이머는 한 번만 시작
- 하지만 2초 뒤에는 최신 callback 실행
rememberUpdatedState는 이 두 요구를 동시에 만족시킵니다.
대표 예시 2: DisposableEffect 안의 listener
이 API는 LaunchedEffect에서만 쓰는 것이 아닙니다.
오래 살아 있는 listener나 observer 안에서 최신 callback이 필요할 때도 자주 씁니다.
@Composable
fun PlayerObserver(
player: VideoPlayer,
onPlayingChanged: (Boolean) -> Unit,
) {
val latestOnPlayingChanged by rememberUpdatedState(onPlayingChanged)
DisposableEffect(player) {
val listener = object : PlayerListener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
latestOnPlayingChanged(isPlaying)
}
}
player.addListener(listener)
onDispose {
player.removeListener(listener)
}
}
}여기서 listener 등록/해제의 기준은 player입니다.
반면 callback 최신화의 기준은 rememberUpdatedState입니다.
그래서 effect의 key와 listener가 호출할 최신 lambda를 따로 다룰 수 있습니다.
언제 쓰면 좋을까
아래 셋이 동시에 맞으면 검토할 가치가 큽니다.
- effect가 비교적 오래 살아 있다
- effect 안에서 callback이나 값을 나중에 사용한다
- 그 값이 바뀐다고 effect를 다시 시작하고 싶지는 않다
대표적으로 이런 장면입니다.
- delay 후 callback 호출
- animation 완료 후 후속 동작 호출
- observer/listener 내부에서 최신 lambda 사용
- lifecycle event가 왔을 때 최신 handler 호출
언제 굳이 안 써도 될까
모든 callback에 붙일 필요는 없습니다.
다음 경우에는 보통 불필요합니다.
- 값이 바뀌면 effect를 다시 시작하는 것이 맞다
- effect가 아니라 일반 composable 본문에서 바로 쓰는 값이다
- 오래 살아 있는 작업이 없다
- 이벤트 핸들러에서 바로 현재 값을 쓰면 된다
예를 들어 LaunchedEffect(userId)에서 userId가 바뀌면 새 로딩을 시작하는 것이 맞다면, 그건 stale value 문제가 아니라 정상적인 restart에 가깝습니다.
흔한 오해
1) remember처럼 값을 캐시하는 용도다
아닙니다.
remember는 계산 결과를 재사용하는 쪽에 가깝고, rememberUpdatedState는 effect 안에서 최신 참조를 안전하게 읽는 쪽에 가깝습니다.
2) callback이 있으면 항상 써야 한다
아닙니다.
callback이 effect 밖에서 즉시 실행되거나, callback 변경 시 effect 재시작이 자연스러우면 굳이 필요 없습니다.
3) stale lambda를 무조건 숨겨 준다
이 API는 재시작하지 않으려는 effect에서 최신 참조를 읽게 해 주는 도구입니다.
설계 자체가 잘못되었는데 무조건 이걸로 덮는다고 해결되지는 않습니다.
빠른 판단 기준
아래 질문으로 고르면 쉽습니다.
- 이 값이 바뀌면 effect를 다시 시작해야 하나?
- 예 → key에 넣는 쪽을 먼저 본다
- 아니오 →
rememberUpdatedState를 검토한다
- effect 안에서 이 값을 나중에 다시 읽나?
- 예 → stale 참조 가능성을 점검한다
- 최신 callback만 바꾸고 실행 중인 작업은 유지하고 싶은가?
- 예 →
rememberUpdatedState가 잘 맞는다
정리
rememberUpdatedState는 Compose에서 가장 유명한 API는 아니지만, effect를 다루다 보면 한 번쯤 꼭 마주치는 도구입니다.
이름보다 중요한 건 역할입니다.
effect를 다시 시작할지 말지와 effect 안에서 최신 값을 읽을지를 분리해 주는 도구라고 이해하면 훨씬 쉬워집니다.
관련해서 같이 보면 좋은 글은 LaunchedEffect와 SideEffect 차이, DisposableEffect 정리, remember와 rememberSaveable 차이입니다.
같이 보면 좋은 글로는 DisposableEffect 정리, remember와 rememberSaveable 차이가 있습니다.
기본 개념은 Android Developers side-effects 문서의 rememberUpdatedState 설명과 연결해서 보면 가장 정확합니다.