
remember와 rememberSaveable 차이는 API 두 개를 비교하는 문제가 아니라, 상태가 언제 사라지는지를 구분하는 문제에 가깝습니다. Jetpack Compose를 처음 배울 때 이 경계가 흐리면 회전 한 번에 값이 날아가거나, 반대로 작은 UI 상태와 화면 로직을 한 composable 안에 과하게 붙잡게 됩니다.
이 글에서는 recomposition, configuration change, process recreation을 나눠서 무엇이 남고 무엇이 사라지는지부터 정리하겠습니다. 그다음에 remember가 충분한 경우와 rememberSaveable이 필요한 경우를 실제 예시로 연결해보겠습니다.
remember와 rememberSaveable 차이 먼저 한 줄로 정리
짧게 말하면 이렇습니다. 같은 composition 안에서 다시 그려질 때만 유지하면 되면 remember, 화면 회전이나 activity recreation 뒤에도 복원되면 좋은 작은 UI 상태면 rememberSaveable이 더 잘 맞습니다.
반대로 화면 전체 의미를 설명하는 상태나 비즈니스 로직까지 커졌다면 rememberSaveable을 더 붙일지 고민하기보다 state hoisting이나 ViewModel로 책임을 옮길 시점인지 먼저 보는 편이 좋습니다.
핵심은 상태의 중요도가 아니라 상태가 살아남아야 하는 경계입니다.
remember와 rememberSaveable 차이를 이해하려면 먼저 recomposition을 봐야 한다
Compose는 뷰 객체를 직접 만지는 방식보다, 상태를 바탕으로 같은 composable을 다시 읽게 만드는 방식에 가깝습니다. 그래서 recomposition은 화면이 전부 사라졌다가 다시 생기는 이벤트가 아니라, 현재 composition 안에서 필요한 부분을 다시 계산하는 이벤트에 가깝습니다.
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Column {
Text(text = "count = $count")
Button(onClick = { count++ }) {
Text("증가")
}
}
}버튼을 눌러 count가 바뀌면 recomposition이 일어나지만, 값이 0으로 돌아가지는 않습니다. remember가 현재 Composition 안에서 그 값을 붙잡고 있기 때문입니다. 즉 recomposition은 상태를 날리는 이벤트가 아닙니다.
Thinking in Compose 문서도 Compose 위젯은 비교적 stateless하고, 상태가 바뀌면 같은 composable을 새 인자로 다시 호출해 UI를 갱신한다고 설명합니다. 이 감각을 먼저 잡아야 remember를 화면 전체 저장소처럼 오해하지 않게 됩니다.
remember는 왜 configuration change에서 버티지 못할까
문제는 화면 회전처럼 activity 자체가 다시 만들어질 때입니다. 이 시점에는 composable이 속했던 기존 Composition도 함께 사라집니다. remember는 그 Composition 안에 저장된 값이기 때문에, 새 activity에서 다시 시작하면 초기값부터 다시 계산됩니다.
@Composable
fun NicknameEditor() {
var nickname by remember { mutableStateOf("") }
OutlinedTextField(
value = nickname,
onValueChange = { nickname = it },
label = { Text("닉네임") }
)
}입력 중에는 잘 동작하지만, 사용자가 닉네임을 적은 뒤 화면을 회전하면 값이 초기화될 수 있습니다. remember가 고장 난 것이 아니라 원래 책임 범위가 거기까지이기 때문입니다. 공식 state 문서도 remember는 recomposition 동안 상태를 유지하지만 configuration changes across로는 유지되지 않는다고 분명히 설명합니다.
rememberSaveable은 무엇을 하나 더 해주는가
rememberSaveable은 저장 가능한 값을 Bundle 기반으로 보존해서 activity recreation과 system-initiated process recreation 뒤에도 복원을 시도할 수 있게 해줍니다. 저장 가능한 타입이 아니면 custom Saver를 붙이는 방식도 가능합니다.
@Composable
fun NicknameEditor() {
var nickname by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = nickname,
onValueChange = { nickname = it },
label = { Text("닉네임") }
)
}이제 사용자가 화면을 회전해도 입력 중이던 닉네임을 계속 보고 싶다는 기대를 맞추기 쉬워집니다. remember는 현재 composition 안의 기억이고, rememberSaveable은 다시 만들어진 화면으로 건너갈 수 있게 저장해 두는 기억입니다.
recomposition, configuration change, process recreation은 같은 일이 아니다
1) recomposition
- 상태가 바뀌어서 composable 일부를 다시 읽는다
- 같은 composition 안에서 일어난다
- 이 범위라면 remember로도 충분하다
예를 들어 비밀번호 보기 토글이나 드래그 중간값처럼, 사용 중 잠깐만 의미가 있는 상태는 여기서 끝나는 경우가 많습니다.
2) configuration change
- 회전, 다크 모드, 언어 변경 등으로 activity가 다시 만들어질 수 있다
- 기존 composition이 사라지고 새 composition이 시작된다
- 이 경계를 넘겨야 하는 작은 UI state면 rememberSaveable이 유리하다
사용자가 작성 중인 메모 초안, 현재 열린 탭, 펼쳐 둔 카드 상태처럼 방금 하던 맥락을 이어 주는 값이 여기에 자주 들어갑니다.
3) process recreation
- 앱이 백그라운드에 있을 때 시스템이 프로세스를 정리하고 나중에 다시 복원할 수 있다
- 여기서는 복원 가능한 최소 상태만 남기는 발상이 중요하다
- rememberSaveable은 savable한 작은 UI state에 적합하지만 화면 전체 데이터 저장소는 아니다
예를 들어 검색 결과 목록 전체를 통째로 붙잡기보다 마지막 query나 선택된 탭 정도만 복원하고 실제 데이터는 다시 불러오는 편이 더 자연스럽습니다.
실무에서 remember면 충분한 경우
모든 상태를 저장하려고 들면 오히려 UI가 무거워집니다. 아래 경우에는 remember로 끝내는 것이 보통 더 자연스럽습니다.
- 잠깐 보여 주는 애니메이션 진행 상태
- 한 composition 안에서만 의미 있는 드래그 중간값
- 회전 뒤까지 이어질 필요가 없는 일시적 토글 상태
- 외부 state로 이미 올라가 있어 내부에서는 캐시 정도만 필요한 값
@Composable
fun ExpandHint() {
var pressed by remember { mutableStateOf(false) }
Text(
text = if (pressed) "손을 떼면 닫힙니다" else "길게 눌러보세요",
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = {
pressed = true
tryAwaitRelease()
pressed = false
}
)
}
)
}이 pressed는 사용 중 잠깐만 의미가 있습니다. 회전 뒤에도 복원해야 할 이유가 거의 없습니다. 이런 상태에 rememberSaveable을 붙이면 가능은 해도, 의도에 비해 과합니다.
실무에서 rememberSaveable이 빛나는 경우
반대로 사용자가 방금 하던 맥락을 잃으면 어색한 상태는 rememberSaveable 후보가 됩니다.
- TextField 입력값
- 현재 선택된 탭 인덱스
- 펼침/접힘 여부
- 필터 패널 열림 여부
- stepper 현재 단계
- 상세 설명 노출 여부
@Composable
fun FaqCard(question: String, answer: String) {
var expanded by rememberSaveable { mutableStateOf(false) }
Column {
Text(text = question)
Button(onClick = { expanded = !expanded }) {
Text(if (expanded) "접기" else "펼치기")
}
if (expanded) {
Text(text = answer)
}
}
}이 상태는 작고, Boolean 하나로 표현되고, 회전 뒤에도 사용자가 그대로 유지되길 기대하기 쉽습니다. 이럴 때 rememberSaveable은 아주 좋은 선택입니다.
rememberSaveable을 남용하면 왜 어색해질까
여기서 흔한 실수가 나옵니다. 날아가면 안 되는 건 전부 rememberSaveable로 저장하면 되겠다고 생각하는 것입니다. 하지만 화면이 커질수록 이 접근은 곧 막힙니다.
data class SearchUiState(
val query: String = "",
val selectedTab: Int = 0,
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val errorMessage: String? = null
)이 상태 전체를 composable 안에서 rememberSaveable 하나로 오래 버티게 만들고 싶어질 수 있습니다. 그런데 여기에는 UI element state도 있고, 서버에서 다시 받아오면 되는 데이터도 있고, 화면 전체 흐름을 설명하는 값도 섞여 있습니다.
공식 문서도 상태가 커지거나 로직이 많아지면 state holder로 책임을 분리하라고 안내합니다. rememberSaveable은 화면 구조를 대신 설계해 주는 도구가 아니라, 작고 복원 가능한 UI 상태를 챙겨 주는 도구에 가깝습니다.
언제 ViewModel까지 생각해야 할까
아래처럼 화면 전체 의미를 설명하기 시작하면 remember와 rememberSaveable만으로 풀기보다 ViewModel을 함께 떠올리는 편이 좋습니다.
- 로딩, 성공, 실패, 빈 상태를 함께 다뤄야 할 때
- 서버 호출 결과와 사용자 액션이 계속 엮일 때
- 여러 composable이 같은 상태를 함께 봐야 할 때
- 화면이 다시 만들어져도 같은 UI state를 이어 가야 할 때
예를 들어 검색 화면은 보통 이렇게 나누면 자연스럽습니다. query 입력값은 상황에 따라 rememberSaveable 또는 ViewModel, 현재 선택 탭은 rememberSaveable, 검색 결과 목록과 로딩/에러 상태는 ViewModel이 더 잘 맞습니다.
특히 query는 자주 헷갈립니다. 단순 입력 임시값이면 rememberSaveable로 충분할 수 있지만, query가 곧 화면 상태를 대표하고 debounce 검색이나 필터 로직과 강하게 연결된다면 처음부터 ViewModel에 두는 쪽이 더 자연스러울 수 있습니다. 즉 같은 TextField라도 역할에 따라 판단이 달라집니다.
빠르게 판단하는 체크리스트
- 이 값은 recomposition만 넘기면 되는가
- 화면 회전 뒤에도 사용자가 그대로 보길 기대하는가
- process recreation 뒤에도 복원할 가치가 있는 최소 상태인가
- 이 값이 화면 전체 로직을 설명할 정도로 커졌는가
판단을 붙여 보면 이렇습니다. 1번만 yes면 remember, 2번까지 yes면 rememberSaveable 후보, 3번도 yes지만 값이 크거나 구조가 복잡하면 최소 상태만 save하고 나머지는 재구성, 4번이 yes면 state hoisting 또는 ViewModel입니다.
remember와 rememberSaveable 차이를 한 번에 잡는 예시
예시 A: 비밀번호 보기 토글
앱을 쓰는 동안만 잠깐 의미 있고 회전 뒤까지 꼭 유지되지 않아도 UX 손실이 작다면 remember로 충분한 경우가 많습니다.
예시 B: 회원가입 입력 폼
사용자가 몇 칸 작성하던 중 회전하면 불편함이 크고, 각 입력값은 작고 저장 가능하다면 rememberSaveable이 잘 맞습니다.
예시 C: 검색 결과 화면
query와 선택 탭은 저장 가치가 있을 수 있지만, 결과 목록과 로딩/에러는 화면 로직 전체와 연결됩니다. 그래서 작은 값은 rememberSaveable, 화면 state는 ViewModel이 더 자연스럽습니다.
이렇게 보면 두 API의 차이는 기능 차이보다 상태의 생존 범위를 어디까지 보느냐의 차이라는 점이 선명해집니다.
마무리
remember와 rememberSaveable을 구분할 때 가장 중요한 것은 API 이름을 외우는 것이 아닙니다. 상태가 언제 사라지는지를 먼저 머릿속에서 구분하는 것입니다.
정리하면 remember는 recomposition을 넘기기에 좋고, rememberSaveable은 activity recreation이나 system-initiated process recreation 뒤에도 남아 있으면 좋은 작은 UI 상태에 잘 맞습니다. 반대로 화면 전체 의미를 설명하는 상태까지 composable 안에 붙잡기 시작했다면, rememberSaveable을 더 붙일지 고민하기보다 state hoisting이나 ViewModel로 책임을 옮길 타이밍인지 먼저 보는 편이 좋습니다.
함께 읽으면 좋은 글로는 Jetpack Compose와 XML 차이: 안드로이드 UI를 배울 때 무엇부터 잡아야 할까, 화면 상태는 어디에 두는 게 맞을까, ViewModel은 왜 필요할까가 있습니다. 공식 기준은 State and Jetpack Compose, Save UI state in Compose, Thinking in Compose 문서를 함께 보면 더 명확합니다.