|

rememberSaveable과 SavedStateHandle은 언제 나눠야 할까

rememberSaveable과 SavedStateHandle은 언제 나눠야 할까
rememberSaveable과 SavedStateHandle은 모두 상태 복원에 관련 있지만 책임과 위치가 다르다.

rememberSaveable SavedStateHandle 차이는 Compose 상태 보존을 공부할 때 자주 막히는 지점입니다. 둘 다 상태를 살리는 도구처럼 보이지만 책임이 다릅니다.

기준은 Composable 내부 UI 상태는 rememberSaveable, ViewModel이 복원해야 할 최소 키는 SavedStateHandle로 나누어 보는 것입니다.


SavedStateHandle과 rememberSaveable 책임 분리

Compose 상태 책임 분리 카드
rememberSaveable과 SavedStateHandle은 모두 상태 복원에 관련 있지만 책임과 위치가 다르다.

rememberSaveable은 작은 UI 상태에 가깝다

Android Developers의 Compose state saving 문서는 rememberSaveable이 Bundle에 저장 가능한 값을 보존하는 데 쓰인다고 설명합니다.

var query by rememberSaveable { mutableStateOf("") }

검색어, 선택된 탭, 펼침 여부처럼 화면 안에서만 의미가 있는 작은 UI 상태에 잘 맞습니다.

SavedStateHandle은 ViewModel의 최소 복원 값이다

SavedStateHandle 문서는 ViewModel이 saved state에 접근할 수 있게 해주는 API로 설명합니다.

class DetailViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    private val itemId: String = checkNotNull(savedStateHandle["itemId"])
}

전체 객체를 저장하려고 하지 않는다

상세 객체 전체를 저장하기보다 다시 가져올 수 있는 ID를 남기는 편이 안전합니다. 큰 객체는 직렬화, 오래된 데이터, 메모리 문제를 만들 수 있습니다.

정리

rememberSaveable과 SavedStateHandle은 경쟁 관계가 아닙니다. 하나는 composable 내부의 작은 UI 상태, 다른 하나는 ViewModel이 복원해야 할 최소 키에 가깝습니다.


Compose 상태 저장 판단 카드
이번 글에서 실제로 판단해야 할 기준을 카드로 정리했다.

SavedStateHandle과 rememberSaveable을 구분하는 가장 쉬운 질문

둘 중 무엇을 쓸지 헷갈릴 때는 “이 값의 주인이 누구인가”를 먼저 묻는 편이 좋습니다. TextField의 현재 입력값처럼 화면 내부 표현에 가까우면 rememberSaveable이 자연스럽습니다. 반면 화면을 다시 만들 때 필요한 id, filter, selectedTab 같은 값이 ViewModel 로직의 입력이면 SavedStateHandle이 더 잘 맞습니다.

  • 스크롤 위치, 임시 입력값, 열림/닫힘 상태: rememberSaveable 후보
  • 상세 화면의 itemId, 검색 조건, 탭 id: SavedStateHandle 후보
  • 네트워크 응답 전체, 큰 리스트, 이미지 데이터: 둘 다 저장 대상으로 부적합
  • 도메인 상태의 원본: Repository, DB, 서버를 우선 고려

process death를 기준으로 보면 더 분명해진다

화면 회전만 생각하면 remember나 ViewModel만으로도 충분해 보일 수 있습니다. 하지만 앱 프로세스가 죽었다가 복원되는 상황을 생각하면 기준이 달라집니다.

rememberSaveable은 가능한 값을 Bundle에 저장해 Composable 상태를 복원합니다. SavedStateHandle은 ViewModel이 새로 만들어질 때 필요한 작은 상태를 제공해서, 다시 데이터를 로드하거나 화면 상태를 재구성할 수 있게 돕습니다.

잘못 나누면 생기는 문제

  • rememberSaveable에 큰 객체를 넣으면 Bundle 크기와 직렬화 문제가 생길 수 있다
  • SavedStateHandle에 UI 장식 상태까지 넣으면 ViewModel이 화면 세부 표현에 묶인다
  • ViewModel에만 값을 두면 process death 이후 필요한 입력값이 사라질 수 있다
  • 서버에서 다시 받을 수 있는 데이터를 저장하려 하면 복원 책임이 과해진다

Compose 화면 예시로 보는 분리

@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
    var queryDraft by rememberSaveable { mutableStateOf("") }
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    SearchTextField(
        value = queryDraft,
        onValueChange = { queryDraft = it },
        onSearch = { viewModel.search(queryDraft) }
    )

    SearchResultList(uiState.items)
}

class SearchViewModel(
    private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private var lastQuery: String
        get() = savedStateHandle["lastQuery"] ?: ""
        set(value) { savedStateHandle["lastQuery"] = value }

    fun search(query: String) {
        lastQuery = query
        // repository.search(query)
    }
}

이 예시에서 입력 중인 임시 문자열은 화면 표현에 가깝습니다. 반면 마지막 검색어는 ViewModel이 다시 검색을 재개할 때 필요한 입력값이므로 SavedStateHandle에 남길 수 있습니다.

실무 체크리스트

  • 값이 화면 내부 표현인가, 화면 로직의 입력인가
  • Bundle에 넣어도 될 만큼 작고 단순한 값인가
  • 프로세스가 죽은 뒤에도 반드시 복원되어야 하는가
  • 복원보다 재조회가 더 안전한 데이터는 아닌가
  • Navigation argument와 중복 저장하고 있지는 않은가

관련해서 같이 보면 좋은 글

Compose Navigation 상태 보존도 함께 보면 이 글의 기준을 더 쉽게 연결할 수 있습니다.

Android SavedStateHandle은 언제 필요할까도 함께 보면 이 글의 기준을 더 쉽게 연결할 수 있습니다.

함께보면 좋은 글