UI 상태 저장

이 가이드에서는 UI 상태에 관한 사용자 기대치와 상태 보존에 사용할 수 있는 옵션을 설명합니다.

시스템에서 활동이 폐기되거나 앱이 소멸된 후에 신속하게 활동의 UI 상태를 저장하고 복원하는 것은 우수한 사용자 환경에 필수적입니다. 사용자는 UI 상태가 동일하게 유지되기를 기대하지만 시스템이 활동 및 저장된 상태를 폐기할 수도 있습니다.

사용자 기대치와 시스템 동작 간의 간극을 메우려면 다음 방법을 조합하여 사용하세요.

최적의 솔루션은 UI 데이터의 복잡성, 앱의 사용 사례, 데이터 액세스 속도와 메모리 사용량 간의 균형을 찾는 방법에 따라 달라집니다.

앱이 사용자의 기대치를 충족하고 빠르고 반응성 높은 인터페이스를 제공해야 합니다. 특히 회전과 같은 일반적인 구성 변경 후에는 UI에 데이터를 로드할 때 지연되지 않도록 합니다.

사용자 기대치 및 시스템 동작

사용자는 자신이 실행하는 작업에 따라 활동 상태가 삭제되거나 보존될 것으로 기대합니다. 시스템이 사용자 기대치를 자동으로 충족하는 경우도 있지만 그러지 않는 경우도 있습니다.

사용자가 시작한 UI 상태 닫기

사용자는 활동을 시작할 때 해당 활동을 완전히 닫을 때까지 활동의 일시적인 UI 상태가 그대로 유지될 것으로 기대합니다. 사용자는 다음 동작으로 활동을 완전히 닫을 수 있습니다.

  • 개요(최근 사용) 화면에서 활동 스와이프하여 닫기
  • 설정 화면에서 앱 종료 또는 강제 종료
  • 기기 재부팅
  • Activity.finish()로 지원되는 일종의 '마무리' 작업 진행

활동을 완전히 닫은 사용자는 영구적으로 활동에서 벗어났다고 가정하며 활동을 다시 열 경우 활동이 완전히 새로 시작될 것으로 예상합니다. 이러한 닫기 시나리오의 기본적인 시스템 동작은 사용자 기대치와 일치합니다. 활동 인스턴스는 내부에 저장된 상태, 그리고 활동과 관련하여 저장된 인스턴스 상태 기록과 함께 메모리에서 폐기되고 삭제됩니다.

완전한 닫기에 관한 이 규칙에는 몇 가지 예외가 있습니다. 예를 들어 사용자는 브라우저에서 뒤로 버튼을 사용하여 브라우저를 종료하기 전에 보고 있었던 정확한 웹페이지로 되돌아갈 것으로 기대할 수 있습니다.

시스템에서 시작된 UI 상태 닫기

사용자는 회전 또는 멀티 윈도우 모드로의 전환과 같은 구성 변경이 발생하더라도 활동의 UI 상태가 동일하게 유지될 것으로 기대합니다. 그러나 기본적으로 시스템은 이러한 구성 변경이 발생할 때 활동을 폐기하고 활동 인스턴스에 저장된 UI 상태를 완전히 삭제합니다. 기기 설정에 관해 자세히 알아보려면 구성 참조 페이지를 참고하세요. 구성 변경의 기본 동작을 재정의하는 것은 권장되지는 않지만 가능합니다. 자세한 내용은 구성 변경 직접 처리를 참고하세요.

또한 사용자는 일시적으로 다른 앱으로 전환한 후 나중에 다시 사용하던 앱으로 돌아오면 활동의 UI 상태가 동일하게 유지될 것으로 기대합니다. 예를 들어 사용자가 검색 활동에서 검색을 실행한 후 홈 버튼을 누르거나 전화를 받습니다. 그런 다음 검색 활동으로 돌아오면 사용자는 검색 키워드 및 결과가 이전과 똑같이 그대로 유지되기를 기대합니다.

이 시나리오에서 앱은 시스템이 앱 프로세스를 메모리에 유지하기 위해 최선을 다하는 동안 백그라운드에 배치됩니다. 하지만 사용자가 다른 앱과 상호작용하는 동안 시스템은 애플리케이션 프로세스를 폐기할 수 있습니다. 이러한 경우 활동 인스턴스는 내부에 저장된 상태와 함께 폐기됩니다. 사용자가 앱을 다시 실행하면 활동은 사용자의 예상과는 달리 깨끗한 상태입니다. 프로세스 종료에 관해 자세히 알아보려면 프로세스 및 애플리케이션 수명 주기를 참고하세요.

UI 상태를 유지하기 위한 옵션

UI 상태에 관한 사용자 기대치가 기본 시스템 동작과 일치하지 않으면 사용자의 UI 상태를 저장하고 복원하여 시스템에 의한 폐기를 사용자가 인식할 수 있도록 해야 합니다.

UI 상태를 유지하기 위한 각각의 옵션은 사용자 환경에 영향을 주는 다음과 같은 측정기준에 따라 다릅니다.

ViewModel 저장된 인스턴스 상태 영구 스토리지
저장소 위치 메모리 메모리 디스크 또는 네트워크
구성 변경 시에도 유지
시스템에서 시작된 프로세스 종료 시에도 유지 아니요
사용자의 완전한 활동 닫기/onFinish() 시에도 유지 아니요 아니요
데이터 제한 복잡한 객체도 괜찮지만 사용 가능한 메모리에 의해 공간이 제한됨 원시(primitive) 유형 및 문자열과 같은 단순하고 작은 객체만 해당 디스크 공간 또는 비용, 네트워크 리소스에서 검색하는 시간에 의해서만 제한됨
읽기/쓰기 시간 빠름(메모리 액세스만) 느림(직렬화/역직렬화 필요) 느림(디스크 액세스 또는 네트워크 트랜잭션 필요)

ViewModel을 사용하여 구성 변경사항 처리

ViewModel은 사용자가 애플리케이션을 적극적으로 사용하는 동안 UI 관련 데이터를 저장 및 관리하는 데 이상적입니다. ViewModel을 사용하면 UI 데이터에 빠르게 액세스할 수 있으며 회전, 창 크기 조절 및 일반적으로 발생하는 기타 구성 변경 시 네트워크 또는 디스크에서 데이터를 다시 가져오는 것을 피할 수 있습니다. ViewModel 구현 방법을 알아보려면 ViewModel 가이드를 참고하세요.

ViewModel은 메모리에 데이터를 보관하므로 디스크 또는 네트워크에서 데이터를 검색할 때보다 비용이 낮습니다. ViewModel은 활동(또는 다른 일부 수명 주기 소유자)과 관련되어 있습니다. 구성 변경 중에는 메모리에 남아 있으며 시스템이 구성 변경으로 인한 새 활동 인스턴스와 ViewModel을 자동으로 연결합니다.

사용자가 활동 또는 프래그먼트를 종료할 때 또는 개발자가 finish()를 호출할 때 ViewModel은 시스템에 의해 자동으로 폐기됩니다. 이는 사용자가 이러한 시나리오에서 기대한 대로 상태가 삭제됨을 의미합니다.

저장된 인스턴스 상태와 달리 ViewModel은 시스템에서 시작된 프로세스 종료 중에 폐기됩니다. ViewModel에서 시스템이 시작한 프로세스가 종료된 후 데이터를 다시 로드하려면 SavedStateHandle API를 사용하세요. 또는 데이터가 UI와 관련되어 있고 ViewModel에 유지할 필요가 없는 경우에는 뷰 시스템의 onSaveInstanceState() 또는 Jetpack Compose의 rememberSaveable을 사용하세요. 데이터가 애플리케이션 데이터인 경우에는 디스크에 유지하는 것이 더 나을 수 있습니다.

구성 변경 시 UI 상태를 저장하기 위해 준비한 '메모리 내' 솔루션이 이미 있다면 ViewModel을 사용하지 않아도 됩니다.

저장된 인스턴스 상태를 백업으로 사용하여 시스템에서 시작된 프로세스 종료 처리

뷰 시스템의 onSaveInstanceState() 콜백, Jetpack Compose의 rememberSaveable, ViewModel의 SavedStateHandle은 시스템이 활동 또는 프래그먼트와 같은 UI 컨트롤러를 폐기하고 나중에 다시 생성할 때 컨트롤러의 상태를 다시 로드하는 데 필요한 데이터를 저장합니다. onSaveInstanceState를 사용하여 저장된 인스턴스 상태를 구현하는 방법을 알아보려면 활동 수명 주기 가이드에서 활동 상태 저장 및 복원을 참고하세요.

저장된 인스턴스 상태 번들은 구성 변경 및 프로세스 종료 시에도 유지되지만, 여러 API가 데이터를 직렬화하기 때문에 저장용량 및 속도의 제한이 있습니다. 직렬화될 객체가 복잡하면 직렬화 시 많은 메모리가 소비될 수 있습니다. 직렬화 프로세스는 구성 변경 시 기본 스레드에서 발생하기 때문에 직렬화가 장기적으로 실행되면 프레임 하락 및 시각적인 끊김 현상이 발생할 수 있습니다.

비트맵과 같은 대량의 데이터, 또는 길이가 긴 직렬화나 역직렬화가 필요한 복잡한 데이터 구조를 저장하는 데 저장된 인스턴스 상태를 사용해서는 안 됩니다. 대신 원시 유형 및 String 같은 단순하고 작은 객체만 저장해야 합니다. 따라서 다른 지속성 메커니즘이 실패할 때 UI를 이전 상태로 복원하는 데 필요한 데이터를 다시 생성할 수 있도록 저장된 인스턴스 상태를 사용하여 ID와 같이 필수적인 최소한의 데이터를 저장해야 합니다. 대부분의 앱은 저장된 인스턴스 상태를 구현하여 시스템에서 시작된 프로세스 종료를 처리해야 합니다.

앱의 사용 사례에 따라 저장된 인스턴스 상태를 전혀 사용할 필요가 없을 수도 있습니다. 예를 들어 브라우저에서 사용자가 브라우저를 종료하기 전에 보고 있었던 정확한 웹페이지로 되돌아갈 수 있습니다. 활동이 이런 식으로 작동하면 저장된 인스턴스 상태를 사용하지 않고, 모든 항목을 로컬에 유지할 수 있습니다.

또한 인텐트에서 활동을 열면 구성이 변경되거나 시스템에서 활동을 복원하는 경우 모두 추가 항목 번들이 활동으로 전달됩니다. 활동이 시작될 때 검색어와 같은 UI 상태 데이터가 인텐트 추가 항목으로 전달되면 저장된 인스턴스 상태 번들 대신 추가 번들을 사용할 수 있습니다. 인텐트 추가 항목에 관해 자세히 알아보려면 인텐트 및 인텐트 필터를 참고하세요.

이러한 시나리오 중 하나에서 구성을 변경하는 동안 데이터베이스에서 데이터를 다시 로드하느라 주기를 낭비하지 않으려면 ViewModel을 계속 사용해야 합니다.

보존할 UI 데이터가 단순하고 가벼우면 상태 데이터를 보존하는 데 저장된 인스턴스 상태 API만 단독으로 사용할 수 있습니다.

SavedStateRegistry를 사용하여 저장된 상태에 연결

Fragment 1.1.0 또는 전이 종속 항목인 Activity 1.0.0부터는 Activity 또는 Fragment 같은 UI 컨트롤러가 SavedStateRegistryOwner를 구현하고 컨트롤러에 결합되는 SavedStateRegistry를 제공합니다. SavedStateRegistry를 사용하면 UI 컨트롤러의 저장된 상태에 구성요소가 연결되어 소비하거나 기여할 수 있습니다. 예를 들어 ViewModel의 저장된 상태 모듈SavedStateRegistry를 사용하여 SavedStateHandle을 만들어 ViewModel 객체에 제공합니다. getSavedStateRegistry()를 호출하여 UI 컨트롤러 내에서 SavedStateRegistry를 검색할 수 있습니다.

저장된 상태에 기여하는 구성요소는 saveState()라는 단일 메서드를 정의하는 SavedStateRegistry.SavedStateProvider를 구현해야 합니다. saveState() 메서드를 통해 구성요소가 자체에서 저장되어야 하는 상태가 포함된 Bundle을 반환할 수 있습니다. SavedStateRegistry는 UI 컨트롤러 수명 주기의 상태 저장 단계에서 이 메서드를 호출합니다.

Kotlin

class SearchManager : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val QUERY = "query"
    }

    private val query: String? = null

    ...

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String QUERY = "query";
    private String query = null;
    ...

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }
}

SavedStateProvider를 등록하려면 SavedStateRegistry에서 registerSavedStateProvider()를 호출하여 제공자의 데이터뿐만 아니라 제공자 자체와도 연결할 키를 전달합니다. 이전에 저장된 제공자 데이터는 SavedStateRegistry에서 consumeRestoredStateForKey()를 호출한 후 제공자의 데이터와 연결된 키를 전달하는 방법을 통해 저장된 상태에서 검색할 수 있습니다.

Activity 또는 Fragment 내에서 super.onCreate()를 호출한 후에 onCreate()SavedStateProvider를 등록할 수 있습니다. 또는 SavedStateRegistryOwner에서 LifecycleObserver를 설정하여 LifecycleOwner를 구현하고 ON_CREATE 이벤트 발생 시 SavedStateProvider를 등록할 수 있습니다. LifecycleObserver를 사용하면 SavedStateRegistryOwner 자체에서 이전에 저장된 상태의 등록 및 검색을 분리할 수 있습니다.

Kotlin

class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val PROVIDER = "search_manager"
        private const val QUERY = "query"
    }

    private val query: String? = null

    init {
        // Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
        registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_CREATE) {
                val registry = registryOwner.savedStateRegistry

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this)

                // Get the previously saved state and restore it
                val state = registry.consumeRestoredStateForKey(PROVIDER)

                // Apply the previously saved state
                query = state?.getString(QUERY)
            }
        }
    }

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }

    ...
}

class SearchFragment : Fragment() {
    private var searchManager = SearchManager(this)
    ...
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String PROVIDER = "search_manager";
    private static String QUERY = "query";
    private String query = null;

    public SearchManager(SavedStateRegistryOwner registryOwner) {
        registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
            if (event == Lifecycle.Event.ON_CREATE) {
                SavedStateRegistry registry = registryOwner.getSavedStateRegistry();

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this);

                // Get the previously saved state and restore it
                Bundle state = registry.consumeRestoredStateForKey(PROVIDER);

                // Apply the previously saved state
                if (state != null) {
                    query = state.getString(QUERY);
                }
            }
        });
    }

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }

    ...
}

class SearchFragment extends Fragment {
    private SearchManager searchManager = new SearchManager(this);
    ...
}

로컬 지속성을 사용하여 복잡하거나 큰 데이터의 프로세스 종료 처리

데이터베이스 또는 공유 환경설정과 같은 영구 로컬 저장소는 애플리케이션이 사용자의 기기에 설치되어 있는 동안(사용자가 앱의 데이터를 삭제하지 않는 한) 유지됩니다. 이러한 로컬 저장소는 시스템에서 시작된 활동 및 애플리케이션의 프로세스 종료 시에도 유지되지만 검색하는 데 비용이 많이 들 수 있습니다. 데이터를 로컬 저장소에서 메모리로 읽어와야 하기 때문입니다. 활동을 열고 닫을 때 손실하지 않으려는 모든 데이터를 저장하기 위해 대개 영구 로컬 저장소는 이미 애플리케이션 아키텍처의 일부일 수 있습니다.

ViewModel과 저장된 인스턴스 상태 모두 장기 저장소 솔루션이 아니므로 데이터베이스와 같은 로컬 저장소를 대체하지 못합니다. 일시적인 UI 상태를 임시 저장하는 데에만 이러한 메커니즘을 사용하고 다른 앱 데이터에는 영구 스토리지를 사용해야 합니다. 로컬 스토리지를 활용하여 앱 모델 데이터를 장기적으로(예: 기기 재시작 전체에 걸쳐) 유지하는 방법에 관한 자세한 내용은 앱 아키텍처 가이드를 참고하세요.

UI 상태 관리: 분할 및 정복

다양한 유형의 지속성 메커니즘으로 작업을 분할하여 UI 상태를 효율적으로 저장 및 복원할 수 있습니다. 대부분의 경우 이러한 각 메커니즘은 데이터 복잡도, 액세스 속도 및 전체 기간의 균형에 따라 활동에 사용되는 다양한 유형의 데이터를 저장해야 합니다.

  • 로컬 지속성: 활동을 열고 닫을 때 손실하지 않으려는 모든 애플리케이션 데이터를 저장합니다.
    • 예: 오디오 파일 및 메타데이터가 포함된 노래 객체 컬렉션
  • ViewModel: 관련 UI 컨트롤러를 표시하는 데 필요한 모든 데이터(화면 UI 상태)를 메모리에 저장합니다.
    • 예: 최근 검색한 노래 객체 및 최근 검색어
  • 저장된 인스턴스 상태: 시스템에서 UI를 중지한 후 다시 생성할 때 UI 상태를 다시 로드하는 데 필요한 소량의 데이터를 저장합니다. 복잡한 객체는 여기에 저장하지 않고 로컬 스토리지에 보존하고 저장된 인스턴스 상태 API에 이러한 객체의 고유 ID를 저장합니다.
    • 예: 최근 검색어 저장

예를 들어 노래 라이브러리를 검색할 수 있는 활동을 생각해 보세요. 다양한 이벤트가 다음과 같이 처리됩니다.

사용자가 노래를 추가하면 ViewModel은 즉시 이 데이터가 로컬에 유지되도록 위임합니다. 새로 추가된 이 노래가 UI에 표시되어야 하는 경우 노래가 추가되었음을 반영하도록 ViewModel 객체의 데이터도 업데이트해야 합니다. 이때 기본 스레드에서 모든 데이터베이스 삽입 작업을 진행해야 합니다.

사용자가 노래를 검색할 때 데이터베이스에서 로드되어 UI에 표시될 노래 데이터는 아무리 복잡하더라도 즉시 화면 UI 상태의 일환으로 ViewModel 객체에 저장되어야 합니다.

활동이 백그라운드로 전환되고 시스템이 저장된 인스턴스 상태 API를 호출하면 검색어는 프로세스가 다시 생성될 경우에 대비하여 저장된 인스턴스 상태에 저장되어야 합니다. 이 정보는 여기에 저장된 애플리케이션 데이터를 로드하는 데 필요하므로 검색어를 ViewModel SavedStateHandle에 저장합니다. 이는 데이터를 로드하고 UI를 현재 상태로 되돌리는 데 필요한 모든 정보입니다.

복잡한 상태 복원: 조각 다시 맞추기

사용자가 활동으로 돌아갈 때 활동이 다시 생성될 수 있는 시나리오에는 두 가지가 있습니다.

  • 활동이 시스템에 의해 중지된 후 다시 생성됩니다. 시스템이 쿼리를 저장된 인스턴스 상태 번들에 저장했으므로 SavedStateHandle을 사용하지 않을 경우 UI가 이 쿼리를 ViewModel에 전달해야 합니다. ViewModel은 캐시된 검색결과가 없음을 확인한 후 주어진 검색어를 사용하여 검색결과 로드를 위임합니다.
  • 구성 변경 후 활동이 생성됩니다. ViewModel 인스턴스가 폐기되지 않았으므로 ViewModel에는 메모리에 캐시된 모든 정보가 있으며 데이터베이스를 다시 쿼리할 필요가 없습니다.

추가 리소스

UI 상태 저장에 관해 자세히 알아보려면 다음 리소스를 참고하세요.

블로그