|

SavedStateHandle은 언제 써야 할까: process death와 UI 상태 판단 기준

SavedStateHandle은 언제 써야 할까를 설명하는 안드로이드 대표 이미지
process death와 최소 복원 상태 기준으로 SavedStateHandle의 적정 사용 범위를 설명한다

SavedStateHandle은 자주 등장하지만, 막상 언제 필요한지는 헷갈리기 쉽습니다. 이 글에서는 process death 뒤에 다시 필요한 최소 상태라는 기준으로 SavedStateHandle을 어디까지 써야 하는지 실무적으로 정리해보겠습니다.

결론부터 말하면 SavedStateHandle은 화면을 다시 세우는 데 필요한 작은 복원 키에 가장 잘 맞습니다. 반대로 결과 리스트 전체나 영속적인 business state까지 맡기기 시작하면 구조가 어색해집니다.


configuration change와 process death를 먼저 나눠야 한다

많은 혼란은 여기서 시작합니다. 화면 회전처럼 configuration change가 일어나면 Activity나 Fragment는 다시 만들어질 수 있지만, 같은 화면 범위의 ViewModel은 계속 살아남을 수 있습니다. 그래서 검색 결과, 로딩 상태, 선택된 필터처럼 화면 수준 상태는 우선 ViewModel이 맡는 편이 자연스럽습니다.

하지만 process death는 다릅니다. 앱이 백그라운드에 있는 동안 시스템이 프로세스를 정리했다가, 사용자가 다시 돌아와 화면을 복원해야 하는 상황입니다. 이때는 ViewModel 인스턴스 자체가 사라졌다고 보는 편이 맞습니다. 즉, ViewModel은 configuration change 대응에는 강하지만 process death 복원까지 혼자 해결하는 도구는 아닙니다.


SavedStateHandle은 언제 써야 할까: 최소 복원 상태에 맞을 때

SavedStateHandle 후보를 고를 때는 크기보다 역할을 먼저 보는 편이 좋습니다. 아래 값들은 process death 뒤에도 다시 필요할 가능성이 높고, 화면을 어떤 조건으로 다시 세워야 하는지를 결정합니다.

  • 상세 화면을 다시 열기 위한 itemId
  • 검색 화면의 마지막 query
  • 현재 선택된 탭 번호
  • 정렬 기준이나 필터 값
  • 복원 시 꼭 필요한 작은 플래그

이 값들의 공통점은 하나입니다. 이 값만 있으면 화면의 핵심 의미를 다시 복원할 수 있다는 점입니다.


navigation args는 SavedStateHandle과 특히 잘 맞는다

상세 화면으로 이동할 때 정말 필요한 것은 대개 상품 객체 전체가 아니라 itemId 같은 식별자입니다. process death 뒤에도 itemId만 있으면 Repository에서 데이터를 다시 읽어올 수 있기 때문에, navigation args와 SavedStateHandle 조합은 실무에서 꽤 자연스럽습니다.

class ProductDetailViewModel(
    private val savedStateHandle: SavedStateHandle,
    private val repository: ProductRepository
) : ViewModel() {

    private val itemId: String = checkNotNull(savedStateHandle["itemId"])

    val uiState: StateFlow<ProductDetailUiState> =
        repository.productFlow(itemId)
            .map { product -> ProductDetailUiState(product = product) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = ProductDetailUiState(isLoading = true)
            )
}

여기서 중요한 것은 저장하는 단위입니다. 저장하는 것은 itemId이고, 다시 불러오는 것은 실제 상품 데이터입니다. 복원 키만 저장하고 본문 데이터는 다시 로드하는 패턴이 SavedStateHandle과 가장 잘 맞습니다.


검색 화면에서는 query와 filter까지가 보통 1차 후보다

검색 화면 상태는 보통 query, 정렬/필터 값, 로딩 여부, 결과 목록, 에러 메시지로 나뉩니다. 이 중 SavedStateHandle 후보는 대개 query와 필터 값 정도입니다. 이 값들이 화면을 다시 어떤 조건으로 그릴지 결정하기 때문입니다.

class SearchViewModel(
    private val savedStateHandle: SavedStateHandle,
    private val repository: SearchRepository
) : ViewModel() {

    companion object {
        private const val KEY_QUERY = "query"
        private const val KEY_SORT = "sort"
    }

    private val queryFlow = MutableStateFlow(savedStateHandle[KEY_QUERY] ?: "")
    private val sortFlow = MutableStateFlow(savedStateHandle[KEY_SORT] ?: "recent")

    fun onQueryChanged(newQuery: String) {
        savedStateHandle[KEY_QUERY] = newQuery
        queryFlow.value = newQuery
    }

    fun onSortChanged(newSort: String) {
        savedStateHandle[KEY_SORT] = newSort
        sortFlow.value = newSort
    }
}

반면 로딩 여부, 일시적인 에러 메시지, 결과 목록 전체는 저장 우선순위가 낮습니다. process death 뒤에 검색 조건만 복원하고 데이터를 다시 요청해도 충분한 경우가 많기 때문입니다.

여기서 자주 하는 실수는 사용자가 한 글자 입력할 때마다 무조건 SavedStateHandle에 넣는 패턴을 습관처럼 복붙하는 것입니다. 입력 중간값이 정말 process death 뒤에도 꼭 유지돼야 하는지 먼저 생각해야 합니다.


UI state와 business state를 같이 보면 SavedStateHandle을 남용하게 된다

SavedStateHandle을 과하게 쓰는 코드는 대개 UI state와 business state를 한 덩어리로 보기 시작할 때 나옵니다. 간단히 나누면 UI state는 지금 화면이 무엇을 보여줘야 하는가이고, business state는 화면을 넘어도 의미가 남는 도메인 상태인가에 가깝습니다.

  • SavedStateHandle 후보: 선택된 탭, 검색어, itemId, 정렬 값처럼 화면 복원의 출발점이 되는 값
  • 저장 계층 후보: 결제 진행 상태, 계약서 초안, 서버와 맞춰야 하는 장바구니처럼 앱을 완전히 종료해도 남아야 하는 값

예를 들어 현재 선택된 탭이나 상세 화면의 itemId는 화면을 다시 세우는 데 필요한 UI 관련 상태입니다. 이런 값은 SavedStateHandle 후보가 될 수 있습니다. 반대로 결제 진행 상태나 작성 중인 대형 초안은 business state에 더 가깝기 때문에 Room, DataStore, 서버 저장 같은 더 강한 저장 계층을 먼저 생각해야 합니다.


SavedStateHandle 남용이 시작되는 신호

  1. 화면 상태 객체 전체를 그대로 SavedStateHandle에 넣고 있다
  2. 네트워크 응답 모델을 통째로 저장하고 있다
  3. process death 복원보다 단순 화면 재생성 대응을 위해서만 쓰고 있다
  4. 앱을 완전히 껐다 켜도 남아야 할 데이터를 SavedStateHandle에 기대고 있다
  5. 키가 너무 많아져서 ViewModel이 작은 캐시 저장소처럼 변하고 있다

SavedStateHandle은 필요한 값 몇 개를 잡아주는 도구일 때 가장 깔끔합니다. 저장소처럼 쓰기 시작하면 책임이 흐려집니다.


실무에서는 이 네 질문으로 거의 정리된다

  1. 이 값은 configuration change만 버티면 되는가
  2. process death 뒤에도 다시 필요할까
  3. 이 값만 있으면 화면을 다시 구성할 수 있을까
  4. 이 값은 화면용 상태인가, 영속성이 필요한 business state인가

답이 분명해지면 선택도 쉬워집니다. 화면 회전만 버티면 되면 ViewModel 또는 View state가 우선이고, process death 뒤에도 다시 필요하면 SavedStateHandle 후보가 됩니다. 값 하나로 다시 불러올 수 있으면 SavedStateHandle에 저장하고 데이터는 재로드하면 됩니다. 앱 재실행 후에도 반드시 남아야 하면 로컬 저장소나 서버 저장이 우선입니다.


마무리

SavedStateHandle은 모든 상태에 붙이는 안전장치가 아닙니다. 정말 필요한 것은 process death 뒤에도 화면을 다시 세울 최소 상태인지 먼저 따지는 판단 기준입니다.

상세 화면의 itemId, 검색 조건, 선택된 탭처럼 작지만 의미 있는 복원 키에는 SavedStateHandle이 잘 맞습니다. 반대로 결과 데이터 전체나 영구 보관이 필요한 business state까지 맡기기 시작하면 구조가 무거워집니다.

상태 자체를 어디에 둘지부터 다시 정리하고 싶다면 화면 상태는 어디에 두는 게 맞을까를 먼저 함께 읽어보면 좋습니다. ViewModel 역할을 먼저 잡고 싶다면 ViewModel은 왜 필요할까, 생명주기 배경을 다시 정리하고 싶다면 생명주기를 알아야 설계가 쉬워지는 이유도 같이 추천합니다.

공식 기준은 Android Developers의 ViewModel 문서, Saving UI States 문서, Saved State module for ViewModel 문서를 함께 보면 더 분명해집니다.

함께보면 좋은 글