diff --git a/docs/jihyun/week8/Q11_State.md b/docs/jihyun/week8/Q11_State.md new file mode 100644 index 0000000..4325a21 --- /dev/null +++ b/docs/jihyun/week8/Q11_State.md @@ -0,0 +1,50 @@ +# 상태(state)란 무엇이며 이를 관리하는 데 사용되는 API는 무엇인가요? + +Jetpack Compose에서 State는 앱 시나리오에서 흔히 변경될 수 있는 값이자, UI에서 동적으로 반영되는 데이터를 나타냄 + +흔히 사용되는 상태: + +- 네트워크 오류에 대한 Snackbar 메시지 노출 +- 사용자 입력 또는 상호작용으로 발생하는 애니메이션을 표시하기 위한 용도 + +상태는 UI 업데이트를 직접 트리거 + +상태가 변경되면 변경된 상태를 UI에 반영하기 위해 다시 렌더링 + +## State와 Composition + +UI 업데이트: 컴포저블이 변경된 매개변수를 통해 호출될 때만 발생 + +이러한 동작은 composition 생명주기와 밀접하게 관련됨 + +- 초기 Composition: 컴포저블을 실행하여 UI 트리가 처음 생성되고 렌더링되는 프로세스 +- Recomposition: 상태 변경 시 트리거되며, recomposition은 관련 컴포저블을 업데이트하여 새로운 상태를 반영 + +Compose Runtime: 상태 변경 사항을 자동으로 추적 + +→ 안드로이드 View 시스템에서 UI를 호출하기 위해 사용하는 View.invalidate() 메서드와 유사한 동작을 개발자 대신하여 UI를 업데이트 + +Recomposition: 업데이트된 상태를 반영해야 하는 컴포저블 함수에 대해서만 트리거 + +## Compose에서 상태 관리하기 + +Jetpack Compose는 상태를 효과적으로 관리하기 위해 여러 API들을 제공 + +- remember + - 초기 컴포지션이 발생했을 때 메모리에 객체를 저장하고 recomposition이 발생하면 기존 메모리에 저장된 값을 꺼내옴 + - API의 이름 그대로 상태 값을 메모리에 ‘기억한다’고 이해하면 됨 +- rememberSaveable + - recomposition 뿐만 아니라, 화면 회전과 같은 구성 변경 시에도 상태를 유지 + - Bundle에 저장할 수 있는 유형 또는 그 외 유형에 대해서는 커스텀 saver 객체와 함께 작동할 수 있음 +- mutableStateOf + - 가변적인 상태를 의미 + - 상태 값이 변경될 때 recomposition을 트리거하는 관찰 가능한 상태 객체를 생성 + +### 왜 remember와 mutableStateOf가 함께 사용되나요? + +- remember API는 객체를 메모리에 저장 +- mutableStateOf는 상태값이 변할 때 recomposition을 트리거하기 위한 관찰 가능한 객체를 생성하기 위한 API + +만약 mutableStateOf만 사용하면 상태값이 바뀔 때마다 recomposition을 트리거 → 문제는 해당 상태값 자체가 recomposition이 발생할 때마다 메모리에 저장되어있지 않기 때문에 초기화되고, 기대했던 동작과는 다른 결과가 나오게 됨 + +따라서 상태 또한 메모리에 저장을 해야 함 \ No newline at end of file diff --git a/docs/jihyun/week8/Q12_State_Hoisting.md b/docs/jihyun/week8/Q12_State_Hoisting.md new file mode 100644 index 0000000..d2f7166 --- /dev/null +++ b/docs/jihyun/week8/Q12_State_Hoisting.md @@ -0,0 +1,34 @@ +# 상태 호이스팅(state hoisting)으로 어떤 이점을 얻을 수 있나요? + +상태 호이스팅(State hoisting)은 상태를 상위 수준의 컴포저블 함수로 끌어올리는 것을 의미 + +따라서, 상태 값과 상태 값을 업데이트하는 람다 함수를 컴포저블 매개변수로 전달하고 해당 값은 현재 컴포저블이 아닌 다른 호출자 쪽에서 관리하도록 하는 것 + +상태 호이스팅은 근본적으로 단방향 데이터 흐름 원칙을 따르므로 UI를 더 쉽게 관리하고 확장 가능하게 만듦 + +### 상태 호이스팅의 특성 + +- State: 부모 컴포저블에서 관리 + - 상태 호이스팅을 적용하려는 컴포저블은 상태 자체를 가져서는 안 되고, 매개변수로 상태를 받아서도 안 됨 +- Events 또는 triggers + - 자식에서 값을 바꾸고, 해당 값을 부모로 다시 전달받는 형태로 상태를 업데이트 + - 보편적으로 람다 함수를 매개변수로 넘기고, 해당 컴포저블을 호출하는 쪽에서 업데이트된 값을 콜백으로 받아 상태를 업데이트시키는 형태로 동작 +- 업데이트된 상태는 매개변수로 자식에게 다시 전달되어 단방향 데이터 흐름을 생성 + +### 상태 호이스팅의 장점 + +- 더 나은 재사용성 + - 상태 호이스팅을 적용하면 컴포저블을 상태 없는 형태(stateless) 및 재사용 가능하게 만들 수 있음 + - 상태 및 이벤트 콜백을 전달 → 동일한 컴포저블을 특정 구현에 얽매이지 않고 다른 화면이나 컨텍스트에서 사용할 수 있음 +- 단순화된 테스트 + - 상태를 저장하지 않는 컴포저블은 매개변수로 전달된 상태 값에 전적으로 의존 → 테스트하기가 더 쉬움 + - 이로 인해 예측 가능하고 명확한 테스트 시나리오 가능 +- 더 나은 관심사 분리 + - 상태 관리 로직을 부모 컴포저블 또는 ViewModel로 옮김으로써, UI 컴포넌트가 인터페이스 렌더링에만 집중하도록 함 + - 이러한 역할 분리는 비즈니스 로직과 UI 코드를 구별하여 유지 관리성을 향상 +- 단방향 데이터 흐름 지원 + - 상태 호이스팅은 Jetpack Compose의 단방향 데이터 흐름 아키텍처와 일치하여 상태가 단 하나의 공급원에서 흐르도록 보장 + - 여러 소스가 동일한 상태를 관리하려고 할 때 발생하는 예상치 못한 동작의 발생 가능성을 줄임 +- 향상된 상태 관리 + - 상태 호이스팅을 사용하면 ViewModel 또는 부모 컴포저블과 같은 상위 수준 컨테이너에서 상태를 중앙 집중화할 수 있음 + - 이를 통해 복잡한 UI 흐름을 관리하고 인스턴스 상태 저장 또는 상태 복원 관리와 같은 작업을 더 쉽게 처리할 수 있음 \ No newline at end of file diff --git a/docs/jihyun/week8/Q13_remember_rememberSaveable.md b/docs/jihyun/week8/Q13_remember_rememberSaveable.md new file mode 100644 index 0000000..5afc514 --- /dev/null +++ b/docs/jihyun/week8/Q13_remember_rememberSaveable.md @@ -0,0 +1,184 @@ +# remember와 rememberSaveable의 차이점은 무엇인가요? + +상태 관리는 UI가 데이터 변경에 따라 동적으로 업데이트되도록 하는 핵심적인 개념 + +remember와 rememberSaveable은 모두 recomposition로부터 상태를 유지하도록 하는 API지만 서로 다른 목적을 가지고 있음 + +## remember + +- 목적 + - remember API는 메모리에 값을 저장하고 recomposition이 발생했을 경우 메모리에 저장된 값을 꺼내와 상태를 유지 + - 그러나 화면 회전이나 프로세스 재시작과 같은 구성 변경 중에는 상태를 유지하지 않음 +- 유즈 케이스 + - 상태가 구성 변경 후에도 유지될 필요가 없을 때 remember을 사용 + - e.g., 화면이 회전되거나 사용자가 언어 설정 등을 바꾸었을 경우, 정보가 날아가도 상관없는 상태의 경우 remember가 적합 + +## rememberSaveable + +- 목적 + - rememberSaveable API는 구성 변경 시에도 상태를 유지하여 remember보다 더 넓은 범위에서 상태를 저장하고 복원 + - 안드로이드 SDK의 Bundle에 저장할 수 있는 값에 한하여 자동으로 저장하고 복원 +- 유즈 케이스 + - 유저 인풋 입력이나 내비게이션 상태와 같이 구성 변경 후에도 유지되어야 하는 상태에 대해서는 rememberSaveable을 사용해야 함 + +### 무조건 rememberSaveable을 사용하는 것이 더 좋은가? + +rememberSaveable은 내부적으로 remember보다 훨씬 다양한 내부 처리 및 오버헤드가 발생 + +무턱대고 모든 상황에 rememberSaveable을 사용하는 것은 오히려 앱 성능을 떨어뜨리고 사용자 경험을 방해할 수도 있음 + +## 주요 차이점 + +| 기능 | remember | rememberSaveable | +| --- | --- | --- | +| 지속성 | 션재 컴포지션 생명주기 동안에만 상태 유지 | 컴포지션 및 구성 변경 시 상태 유지 | +| 저장 위치 | 메모리에 값 저장 | 메모리에 값을 저장하고 Bundle에 저장 | +| 커스텀 Saver 지원 | 해당 없음 | 복잡한 객체에 대한 커스텀 saver 지원 | + +## 사용 시기 + +- 애니메이션이나 임시적인 UI 상태와 같이 현재 컴포지션을 넘어서 유지될 필요가 없는 일시적인 상태에는 remember를 사용 +- 사용자 입력, 선택 상태 또는 양식 데이터와 같이 구성 변경 시에도 유지되어 더 나은 사용자 경험을 제공할 수 있는 상태에서는 rememberSaveable을 사용 + +## remember 및 rememberSaveable 내부 구조 + +### remember 내부 구조 + +```kotlin +@Composable +inline fun remember(crossinline calculation: @DisallowComposableCalls () -> T): T = + currentComposer.cache(false, calculation) +``` + +remember는 내부적으로 Composer 인스턴스에서 cache 함수를 호출 + +cache 함수 구현 방식은 다음과 같음 + +```kotlin +@ComposeCompilerApi +inline fun Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T { + @Suppress("UNCHECKED_CAST") + return rememberedValue().let { + if (invalid || it === Composer.Empty) { + val value = block() + updateRememberedValue(value) + value + } else it + } as T +} +``` + +Compose 컴파일러 플러그인 API와 상호 작용하여 컴포지션 데이터에 값을 캐시 + +구체적으로, 값이 유효하지 않거나 초기화되지 않았는지(Composer.Empty) 확인 + +- 초기화되지 않았다면, 제공된 블록 람다 함수를 사용하여 값을 계산하고 컴포지션 데이터에 저장한 다음 반환 +- 그렇지 않으면 이전에 기억된 값을 단순히 복원하여 반환 + +⇒ 중복적인 계산을 피하면서 recomposition이 발생해도 값이 효율적으로 유지되도록 보장 + +### rememberSaveable 내부 구조 + +```kotlin +@Composable +public fun rememberSaveable(vararg inputs: Any?, init: () -> T): T { + // TODO(mgalhardo): We're planning to support both `autoSaver` and `serializer` in this base + // variant, where neither is explicitly passed. To avoid potential method signature conflicts, + // we're not using default parameters for `saver`. + // This introduces a direct dependency between Compose Runtime and KTX Serialization, which is + // currently under discussion at go/ktx-serialization-in-savedstate. + @Suppress("DEPRECATION") + return rememberSaveable(*inputs, saver = autoSaver(), key = null, init = init) +} +``` + +```kotlin +@Composable +public fun rememberSaveable( + vararg inputs: Any?, + saver: Saver = autoSaver(), + key: String? = null, + init: () -> T, +): T { + val compositeKey = currentCompositeKeyHashCode + // key is the one provided by the user or the one generated by the compose runtime + val finalKey = + if (!key.isNullOrEmpty()) { + key + } else { + compositeKey.toString(MaxSupportedRadix) + } + @Suppress("UNCHECKED_CAST") (saver as Saver) + + val registry = LocalSaveableStateRegistry.current + + val holder = remember { + // value is restored using the registry or created via [init] lambda + val restored = registry?.consumeRestored(finalKey)?.let { saver.restore(it) } + val finalValue = restored ?: init() + SaveableHolder(saver, registry, finalKey, finalValue, inputs) + } + + val value = holder.getValueIfInputsDidntChange(inputs) ?: init() + SideEffect { holder.update(saver, registry, finalKey, value, inputs) } + + return value +} +``` + +rememberSaveable 함수는 구성 변경 및 프로세스 종료 시에도 상태를 저장하고 복원하는 기능을 추가적으로 지원 → remember 보다 더 넓은 스코프에서의 데이터 복원을 가능하게 함 + +- 키 생성(Key Generation) + - key 매개변수를 통해 사용자가 커스텀 키를 제공할 수 있음 + - 제공되지 않으면 현재 컴포지션 해시를 사용하여 복합 키가 자동으로 생성됨 + + ```kotlin + val compositeKey = currentCompositeKeyHashCode + // key is the one provided by the user or the one generated by the compose runtime + val finalKey = + if (!key.isNullOrEmpty()) { + key + } else { + compositeKey.toString(MaxSupportedRadix) + } + ``` + +- 상태 복원(State Resotration) + - LocalSaveableStateRegistry는 주어진 키에 대해 저장된 값을 검색하는 데 사용 + - 저장된 값이 존재하면 제공된 Saver를 사용하여 복원됨 + + ```kotlin + val registry = LocalSaveableStateRegistry.current + val restored = registry?.consumeRestored(finalKey)?.let { saver.restore(it) } + ``` + +- 기본값 초기화(Default Vlaue Initialization) + - 복원된 값이 없으면 init 람다를 사용하여 기본값이 초기화됨 + + ```kotlin + val finalValue = restored ?: init() + ``` + +- Saveable Holder + - 상태, saver, 레지스트리 및 입력을 관리하기 위해 SaveableHolder가 생성됨 + + ```kotlin + SaveableHolder(saver, registry, finalKey, finalValue, inputs) + ``` + +- 입력 변경 처리(Input Change Handling) + - rememberSaveable에 대한 입력이 변경되면 상태가 무효화되고 값이 다시 초기화됨 + + ```kotlin + val value = holder.getValueIfInputsDidntChange(inputs) ?: init() + ``` + +- 사이드 이펙트(Side Effects) + - SideEffect는 recomposition 중에 업데이트된 상태가 레지스트리에 저장되도록 보장 + + ```kotlin + SideEffect { holder.update(saver, registry, finalKey, value, inputs) } + ``` + + +⇒ remember와 달리 LocalSaveableStateRegistry를 사용하여 화면 회전과 같은 구성 변경 시에도 상태를 보존함으로써 상태의 복원 범위를 넓힘. 또한 saver 매개변수를 통해 개발자는 커스텀 직렬화 및 역직렬화 로직을 정의할 수 있어 복잡한 객체를 원활하게 처리하는 것도 가능 \ No newline at end of file