Jetpack Compose에서 컴포저블 함수는 remember 함수를 사용하여 상태를 보유하는 경우가 많습니다. 기억된 값은 상태 및 Jetpack Compose에 설명된 대로 리컴포지션에서 재사용할 수 있습니다.
remember는 리컴포지션 전반에서 값을 유지하는 도구로 사용되지만 상태는 컴포지션의 수명을 넘어 유지되어야 하는 경우가 많습니다. 이 페이지에서는 remember, retain, rememberSaveable, rememberSerializable API의 차이점, 어떤 API를 선택해야 하는지, Compose에서 기억된 값과 유지된 값을 관리하는 권장사항을 설명합니다.
올바른 수명 선택
Compose에는 컴포지션 전반과 그 이상에서 상태를 유지하는 데 사용할 수 있는 여러 함수가 있습니다. remember, retain, rememberSaveable, rememberSerializable가 그 예입니다. 이러한 함수는 수명과 의미가 다르며 각각 특정 종류의 상태를 저장하는 데 적합합니다. 차이점은 다음 표에 설명되어 있습니다.
|
|
|
|
|---|---|---|---|
값이 리컴포지션 후에도 유지되나요? |
✅ |
✅ |
✅ |
값이 활동 재생성을 통해 유지되나요? |
❌ |
✅ 동일한 ( |
✅ 동등한 ( |
값이 프로세스 종료 후에도 유지되나요? |
❌ |
❌ |
✅ |
지원되는 데이터 유형 |
전체 |
활동이 소멸될 때 누수되는 객체를 참조해서는 안 됩니다. |
직렬화 가능해야 합니다(맞춤 |
사용 사례 |
|
|
|
remember
remember은 Compose에서 상태를 저장하는 가장 일반적인 방법입니다. remember가 처음 호출되면 지정된 계산이 실행되고 remember됩니다. 즉, 컴포저블이 나중에 재사용할 수 있도록 Compose에 저장됩니다. 컴포저블이 리컴포즈되면 코드를 다시 실행하지만 remember 호출은 계산을 다시 실행하는 대신 이전 컴포지션에서 값을 반환합니다.
컴포저블 함수의 각 인스턴스에는 위치 메모이제이션이라고 하는 자체 기억된 값 집합이 있습니다. 리컴포지션 전반에서 사용하기 위해 기억된 값이 메모이제이션되면 컴포지션 계층 구조의 위치에 연결됩니다. 컴포저블이 여러 위치에서 사용되는 경우 컴포지션 계층 구조의 각 인스턴스에는 자체 기억된 값 집합이 있습니다.
기억된 값이 더 이상 사용되지 않으면 잊혀지고 레코드가 삭제됩니다. 기억된 값은 컴포지션 계층 구조에서 삭제되거나 (key 컴포저블이나 MovableContent를 사용하지 않고 다른 위치로 이동하기 위해 값이 삭제되었다가 다시 추가되는 경우 포함) 다른 key 매개변수로 호출될 때 삭제됩니다.
사용 가능한 선택사항 중에서 remember는 수명이 가장 짧고 이 페이지에 설명된 네 가지 메모이제이션 함수 중에서 가장 먼저 값을 잊습니다.
따라서 다음과 같은 경우에 가장 적합합니다.
- 스크롤 위치 또는 애니메이션 상태와 같은 내부 상태 객체 만들기
- 리컴포지션마다 비용이 많이 드는 객체 재생성 방지
하지만 다음은 피해야 합니다.
remember를 사용하여 사용자 입력을 저장합니다. 기억된 객체는 활동 구성 변경 및 시스템에서 시작된 프로세스 종료 시 잊혀지기 때문입니다.
rememberSaveable 및 rememberSerializable
rememberSaveable 및 rememberSerializable는 remember를 기반으로 빌드됩니다. 이 가이드에서 설명하는 메모이제이션 함수 중 수명이 가장 깁니다.
리컴포지션 간에 객체를 위치별로 메모이제이션하는 것 외에도 구성 변경 및 프로세스 종료 (시스템이 백그라운드에 있는 앱의 프로세스를 종료하는 경우, 일반적으로 포그라운드 앱의 메모리를 확보하거나 사용자가 앱이 실행되는 동안 앱의 권한을 취소하는 경우)를 비롯한 활동 재생성 간에 복원할 수 있도록 값을 저장할 수도 있습니다.
rememberSerializable는 rememberSaveable와 동일한 방식으로 작동하지만 kotlinx.serialization 라이브러리로 직렬화할 수 있는 복잡한 유형의 지속성을 자동으로 지원합니다. 유형이 @Serializable로 표시되거나 표시될 수 있는 경우 rememberSerializable를 선택하고, 그 외의 모든 경우에는 rememberSaveable를 선택합니다.
따라서 rememberSaveable와 rememberSerializable는 텍스트 필드 입력, 스크롤 위치, 전환 버튼 상태 등 사용자 입력과 관련된 상태를 저장하는 데 적합합니다. 사용자가 자신의 위치를 잃지 않도록 이 상태를 저장해야 합니다. 일반적으로 앱이 데이터베이스와 같은 다른 영구 데이터 소스에서 검색할 수 없는 상태를 메모이제이션하려면 rememberSaveable 또는 rememberSerializable를 사용해야 합니다.
rememberSaveable과 rememberSerializable은 메모이제이션된 값을 Bundle로 직렬화하여 저장합니다. 이로 인해 두 가지 결과가 발생합니다.
- 메모이제이션하는 값은 기본 유형 (
Int,Long,Float,Double포함),String또는 이러한 유형의 배열 중 하나 이상으로 표현할 수 있어야 합니다. - 저장된 값이 복원되면 (
==)와 동일하지만 컴포지션에서 이전에 사용한 것과 동일한 참조 (===)는 아닌 새 인스턴스가 됩니다.
kotlinx.serialization를 사용하지 않고 더 복잡한 데이터 유형을 저장하려면 맞춤 Saver를 구현하여 객체를 지원되는 데이터 유형으로 직렬화 및 역직렬화하면 됩니다. Compose는 State, List, Map, Set 등의 일반적인 데이터 유형을 기본적으로 이해하며 이러한 유형을 지원되는 유형으로 자동으로 변환합니다. 다음은 Size 클래스의 Saver 예시입니다. listSaver를 사용하여 Size의 모든 속성을 목록에 패킹하여 구현됩니다.
data class Size(val x: Int, val y: Int) { object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver( save = { listOf(it.x, it.y) }, restore = { Size(it[0], it[1]) } ) } @Composable fun rememberSize(x: Int, y: Int) { rememberSaveable(x, y, saver = Size.Saver) { Size(x, y) } }
retain
retain API는 값을 메모이제이션하는 기간 측면에서 remember과 rememberSaveable/rememberSerializable 사이에 있습니다. 유지된 값은 기억된 값과 수명 주기가 다르기 때문에 이름이 다릅니다.
값이 유지되면 위치별로 메모이제이션되고 앱의 수명에 연결된 별도의 수명이 있는 보조 데이터 구조에 저장됩니다. 유지된 값은 직렬화되지 않고 구성 변경을 견딜 수 있지만 프로세스 중단은 견딜 수 없습니다. 컴포지션 계층 구조가 다시 생성된 후 값이 사용되지 않으면 유지된 값이 사용 중지됩니다 (retain에서 잊혀지는 것과 동일).
이 rememberSaveable보다 짧은 수명 주기의 대가로 retain은 람다 표현식, 흐름, 비트맵과 같은 대형 객체와 같이 직렬화할 수 없는 값을 유지할 수 있습니다. 예를 들어 retain를 사용하여 미디어 플레이어 (예: ExoPlayer)를 관리하여 구성 변경 중에 미디어 재생이 중단되지 않도록 할 수 있습니다.
@Composable fun MediaPlayer() { // Use the application context to avoid a memory leak val applicationContext = LocalContext.current.applicationContext val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() } // ... }
retain 및 ViewModel 비교
핵심적으로 retain와 ViewModel는 구성 변경 전반에 걸쳐 객체 인스턴스를 유지하는 가장 일반적으로 사용되는 기능에서 유사한 기능을 제공합니다. retain 또는 ViewModel을 선택하는 것은 유지하려는 값의 유형, 범위 설정 방법, 추가 기능이 필요한지에 따라 달라집니다.
ViewModel는 일반적으로 앱의 UI와 데이터 레이어 간의 통신을 캡슐화하는 객체입니다. 이를 통해 컴포저블 함수에서 로직을 이동할 수 있으므로 테스트 가능성이 향상됩니다. ViewModel는 ViewModelStore 내에서 싱글톤으로 관리되며 유지된 값과는 수명이 다릅니다. ViewModel는 ViewModelStore가 소멸될 때까지 활성 상태로 유지되지만, 콘텐츠가 컴포지션에서 영구적으로 삭제되면 (예를 들어 구성 변경의 경우 UI 계층 구조가 다시 생성되고 컴포지션이 다시 생성된 후 유지된 값이 사용되지 않으면 유지된 값이 삭제됨) 유지된 값은 삭제됩니다.
ViewModel에는 Dagger 및 Hilt를 사용한 종속 항목 삽입을 위한 기본 통합, SavedState와의 통합, 백그라운드 작업을 실행하기 위한 내장 코루틴 지원도 포함되어 있습니다. 따라서 ViewModel는 백그라운드 작업과 네트워크 요청을 실행하고, 프로젝트의 다른 데이터 소스와 상호작용하며, 선택적으로 ViewModel의 구성 변경 전반에 걸쳐 유지되고 프로세스 종료에서 살아남아야 하는 미션 크리티컬 UI 상태를 캡처하고 유지하는 데 이상적인 위치입니다.
retain는 특정 컴포저블 인스턴스로 범위가 지정되고 형제 컴포저블 간에 재사용하거나 공유할 필요가 없는 객체에 가장 적합합니다. ViewModel은 UI 상태를 저장하고 백그라운드 작업을 실행하기에 적합한 반면, retain은 캐시, 노출 추적 및 분석, AndroidView 종속 항목, Android OS와 상호작용하거나 결제 처리기 또는 광고와 같은 서드 파티 라이브러리를 관리하는 기타 객체와 같은 UI 플러밍 객체를 저장하는 데 적합합니다.
최신 Android 앱 아키텍처 권장사항 외에 맞춤 앱 아키텍처 패턴을 설계하는 고급 사용자의 경우 retain를 사용하여 사내 'ViewModel와 유사한' API를 빌드할 수도 있습니다. 코루틴과 저장된 상태 지원은 기본적으로 제공되지 않지만 retain는 이러한 기능이 상단에 빌드된 ViewModel 유사 항목의 수명 주기를 위한 빌드 블록으로 사용할 수 있습니다. 이러한 구성요소를 설계하는 방법에 관한 세부사항은 이 가이드에서 다루지 않습니다.
|
|
|
|---|---|---|
범위 지정 |
공유 값이 없습니다. 각 값은 구성 계층 구조의 특정 지점에 유지되고 연결됩니다. 다른 위치에서 동일한 유형을 유지하면 항상 새 인스턴스에서 작동합니다. |
|
파괴 |
컴포지션 계층 구조를 영구적으로 벗어나는 경우 |
|
추가 기능 |
객체가 컴포지션 계층 구조에 있는지 여부에 따라 콜백을 수신할 수 있습니다. |
기본 |
소유자 |
|
|
사용 사례 |
|
|
retain 및 rememberSaveable 또는 rememberSerializable 결합
객체에 retained와 rememberSaveable 또는 rememberSerializable의 하이브리드 수명이 필요한 경우도 있습니다. 이는 객체가 ViewModel여야 함을 나타낼 수 있습니다. ViewModel는 ViewModel의 저장된 상태 모듈 가이드에 설명된 대로 저장된 상태를 지원할 수 있습니다.
retain와 rememberSaveable 또는 rememberSerializable를 동시에 사용할 수 있습니다. 두 수명 주기를 올바르게 결합하면 복잡성이 크게 증가합니다.
이 패턴은 고급 맞춤 아키텍처 패턴의 일부로, 다음 조건이 모두 충족되는 경우에만 사용하는 것이 좋습니다.
- 유지하거나 저장해야 하는 값의 혼합으로 구성된 객체를 정의하고 있습니다 (예: 사용자 입력을 추적하는 객체와 디스크에 쓸 수 없는 인메모리 캐시).
- 상태가 컴포저블로 범위가 지정되어 있으며
ViewModel의 싱글톤 범위 지정이나 수명에 적합하지 않습니다.
이러한 모든 경우에 클래스를 세 부분으로 분할하는 것이 좋습니다. 저장된 데이터, 유지된 데이터, 자체 상태가 없고 유지된 객체와 저장된 객체에 위임하여 상태를 적절하게 업데이트하는 '미디에이터' 객체입니다. 이 패턴은 다음과 같은 모양을 갖습니다.
@Composable fun rememberAndRetain(): CombinedRememberRetained { val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) { ExtractedSaveData() } val retainData = retain { ExtractedRetainData() } return remember(saveData, retainData) { CombinedRememberRetained(saveData, retainData) } } @Serializable data class ExtractedSaveData( // All values that should persist process death should be managed by this class. var savedData: AnotherSerializableType = defaultValue() ) class ExtractedRetainData { // All values that should be retained should appear in this class. // It's possible to manage a CoroutineScope using RetainObserver. // See the full sample for details. var retainedData = Any() } class CombinedRememberRetained( private val saveData: ExtractedSaveData, private val retainData: ExtractedRetainData, ) { fun doAction() { // Manipulate the retained and saved state as needed. } }
수명에 따라 상태를 분리하면 책임과 저장소의 분리가 매우 명시적으로 이루어집니다. 저장 데이터는 데이터 유지로 조작할 수 없습니다. 이는 savedInstanceState 번들이 이미 캡처되어 업데이트할 수 없는 경우 저장 데이터 업데이트가 시도되는 시나리오를 방지하기 위한 것입니다. 또한 Compose를 호출하거나 활동 재생성을 시뮬레이션하지 않고 생성자를 테스트하여 재생성 시나리오를 테스트할 수 있습니다.
이 패턴이 구현되는 방식의 전체 예는 전체 샘플 (RetainAndSaveSample.kt)을 참고하세요.
위치 메모이제이션 및 적응형 레이아웃
Android 애플리케이션은 휴대전화, 폴더블, 태블릿, 데스크톱을 비롯한 다양한 폼 팩터를 지원할 수 있습니다. 애플리케이션은 적응형 레이아웃을 사용하여 이러한 폼 팩터 간에 전환해야 하는 경우가 많습니다. 예를 들어 태블릿에서 실행되는 앱은 2열 목록 세부정보 뷰를 표시할 수 있지만 작은 휴대전화 화면에 표시될 때는 목록과 세부정보 페이지 간에 탐색할 수 있습니다.
기억되고 유지된 값은 위치별로 메모이제이션되므로 컴포지션 계층 구조의 동일한 지점에 표시되는 경우에만 재사용됩니다. 레이아웃이 다양한 폼 팩터에 적응하면 컴포지션 계층 구조가 변경되어 값이 잊혀질 수 있습니다.
ListDetailPaneScaffold 및 NavDisplay(Jetpack 탐색 3에서)과 같은 기본 제공 구성요소의 경우 문제가 없으며 레이아웃 변경 전반에 걸쳐 상태가 유지됩니다. 폼 팩터에 적응하는 맞춤 구성요소의 경우 다음 중 하나를 실행하여 레이아웃 변경이 상태에 영향을 미치지 않도록 합니다.
- 스테이트풀 컴포저블은 항상 컴포지션 계층 구조의 동일한 위치에서 호출해야 합니다. 컴포지션 계층 구조에서 객체를 재배치하는 대신 레이아웃 로직을 변경하여 적응형 레이아웃을 구현합니다.
MovableContent를 사용하여 스테이트풀(Stateful) 컴포저블을 원활하게 재배치합니다.MovableContent인스턴스는 기억되고 유지된 값을 이전 위치에서 새 위치로 이동할 수 있습니다.
팩토리 함수 기억
Compose UI는 구성 가능한 함수로 구성되지만 컴포지션의 생성 및 구성에는 많은 객체가 사용됩니다. 이러한 객체의 가장 일반적인 예는 LazyListState를 허용하는 LazyList와 같이 자체 상태를 정의하는 복잡한 컴포저블 객체입니다.
Compose 중심 객체를 정의할 때는 수명과 키 입력을 모두 포함하여 원하는 기억 동작을 정의하는 remember 함수를 만드는 것이 좋습니다. 이를 통해 상태 소비자는 예상대로 유지되고 무효화되는 컴포지션 계층 구조에서 인스턴스를 확실하게 만들 수 있습니다. 컴포저블 팩토리 함수를 정의할 때는 다음 가이드라인을 따르세요.
- 함수 이름 앞에
remember를 붙입니다. 선택적으로 함수 구현이retained객체에 의존하고 API가remember의 다른 변형에 의존하도록 발전하지 않는 경우 대신retain접두사를 사용하세요. - 상태 지속성이 선택되고 올바른
Saver구현을 작성할 수 있는 경우rememberSaveable또는rememberSerializable를 사용합니다. - 사용과 관련이 없을 수 있는
CompositionLocal에 따라 부작용을 방지하거나 값을 초기화합니다. 상태가 생성된 위치가 소비되는 위치가 아닐 수도 있습니다.
@Composable fun rememberImageState( imageUri: String, initialZoom: Float = 1f, initialPanX: Int = 0, initialPanY: Int = 0 ): ImageState { return rememberSaveable(imageUri, saver = ImageState.Saver) { ImageState( imageUri, initialZoom, initialPanX, initialPanY ) } } data class ImageState( val imageUri: String, val zoom: Float, val panX: Int, val panY: Int ) { object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver( save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) }, restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) } ) }