SavePhotoScreen에서_무한_로딩_recomposition_및_오버레이_포커스 - boostcampwm-2024/and04-Nature-Album GitHub Wiki
💥 SavePhotoScreen에서 무한 로딩 recomposition 및 오버레이 포커스/터치 처리 개선
문제 정의
https://github.com/boostcampwm-2024/and04-Nature-Album/pull/128/files
@@ -76,20 +75,6 @@ fun SavePhotoScreen(
}
viewModel.setPhotoLoadingUiSate(UiState.Idle)
val photoLoadingUiState = viewModel.photoLoadingUiState.collectAsStateWithLifecycle()
when (photoLoadingUiState.value) {
UiState.Idle, UiState.Loading -> {
RotatingImageLoading(
drawalbeRes = R.drawable.fish_loading_image,
stringRes = R.string.save_photo_screen_loading
)
}
UiState.Success -> {
// TODO:
}
}
- SavePhotoScreen에서 로딩 상태를 표시하기 위해
RotatingImageLoading
컴포저블을 사용하고자 했다. 처음에 정호님께서 무한 리컴포지션 문제를 발견하여, 이를 이미지 로딩을 삭제하는 방향으로 해결했으나, 이미 만들어둔 로딩 이미지를 유지하면서 로딩 상태를 제어하는 것이 더 좋을 것 같아 문제 분석을 진행했다. 그 후 정호님의 제안으로 내가 뒤를 맡게 되었고, 이 문제를 해결하게 되었다.
문제 요약
- 무한 로딩 문제
https://github.com/user-attachments/assets/03a8ba48-32ad-4791-98e8-d150ae3cc35f
- `SavePhotoScreen`에서 `viewModel.setPhotoLoadingUiSate(UiState.Idle)`가 컴포저블 재구성 시마다 호출되어 `_photoLoadingUiState`가 항상 `Idle` 상태로 유지됨.
- `collectAsStateWithLifecycle()`가 상태 변화를 감지하여 로딩 컴포넌트가 계속 재구성되면서 무한 로딩 발생.
- 오버레이 포커스 문제
- 올바르게 로딩 화면을 처리하고서 발견한 이슈
- 로딩 중
Scaffold
에 포커스가 여전히 잡혀 뒤의 화면이 클릭되는 이슈 발생.
1. 무한 로딩 문제
1-1. 문제 원인 분석
기존 상태 관리 문제
- 초기 코드에서는
photoLoadingUiState
와photoSaveState
라는 두 개의 상태를 관리하고 있었음. photoLoadingUiState
는 로딩 상태를 트리거하기 위한 용도로만 사용되고,photoSaveState
는 저장 상태를 관리하고 있었음.- 로딩과 저장에 관한 상태 관리를 별도로 하면서 상태가 불필요하게 겹치는 문제 발생.
- 상태가 겹치면서
SavePhotoScreen
컴포저블이 매번 재구성되고,_photoLoadingUiState
는 항상Idle
상태로 유지되어 무한 로딩 상태에 빠짐.
흐름 분석
SavePhotoScreen
이 실행될 때viewModel.setPhotoLoadingUiSate(UiState.Idle)
이 실행되어 상태가 초기화됨._photoLoadingUiState
가Idle
로 설정되고,photoLoadingUiState.collectAsStateWithLifecycle()
가 이 변화를 감지하여 재구성을 유도함.- 재구성 시
viewModel.setPhotoLoadingUiSate(UiState.Idle)
가 다시 호출되며, 무한 루프가 발생함.
1-2. 문제 해결
photoSaveState
만 사용)
1. 단일 상태 관리로 변경 (photoSaveState
하나로 로딩 및 저장 완료 상태를 모두 관리하여 불필요한 상태 중복을 제거.Loading
,Success
,Idle
상태를photoSaveState
로 통합하고 로딩 및 저장 완료 후 상태 전환을 관리.
2. 로딩 완료 후에만 Idle 상태로 설정
SavePhotoScreen
에서 로딩 상태를 직접 설정하지 않고, 저장 완료 후에만 ViewModel에서Idle
상태로 설정하도록 변경.- 상태 관리를 ViewModel에서 수행하여 불필요한 재구성을 방지함.
2. 오버레이 포커스 및 터치 이벤트 처리
2-1. 문제 원인
- 로딩 화면(
RotatingImageLoading
)이Scaffold
위에 겹쳐져 표시되었으나, Scaffold의 터치와 입력이 여전히 가능한 상태였다. Scaffold
에 있는 다른 UI 요소와의 상호작용이 방지되지 않아, 사용자가 로딩 중에도 다른 UI 요소를 터치할 수 있는 문제가 있었다.
2-2. 해결 방법: 포커스 및 터치 이벤트 소비 설정
RotatingImageLoading
에pointerInput
과focusRequester
를 추가하여, 포커스와 터치 이벤트를 모두 차단하였다.pointerInput
을 사용하여 모든 터치 이벤트를Box
내부에서 소비하도록 설정하여, 하위Scaffold
로 터치 이벤트가 전달되지 않도록 하였다.
val focusRequester = remember { FocusRequester() }
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f))
.focusRequester(focusRequester)
.focusable()
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
awaitPointerEvent(PointerEventPass.Initial) // 모든 터치 이벤트를 소비하여 Scaffold에 전달되지 않음
}
}
},
contentAlignment = Alignment.Center
) {
// 로딩 이미지 및 텍스트 표시
}
val focusRequester = remember { FocusRequester() }
는 포커스 요청을 관리하는FocusRequester
객체를 생성하고, 컴포지션 동안 유지되도록remember
를 사용해 저장하는 역할focusRequester(focusRequester)
및focusable()
focusRequester
와focusable()
을 사용하여 이Box
가 포커스를 받을 수 있도록 설정- 포커스를 갖게 되면 키보드 입력이나 포커스 관련 이벤트를 다른 UI 요소들이 아닌 이
Box
가 우선적으로 처리
pointerInput(Unit)
pointerInput
을 사용하여 사용자 터치 이벤트를Box
가 가로채도록 설정awaitPointerEventScope
와awaitPointerEvent(PointerEventPass.Initial)
을 사용하여, 모든 터치 이벤트가Box
내에서 소모되도록 만든다.이로 인해 아래쪽에 있는Scaffold
와의 상호작용이 차단된다.
awaitPointerEvent(PointerEventPass.Initial)
로 초기 전달 단계에서 모든 터치 이벤트를 가로채고 소비하여 이후 단계로 이벤트가 전달되지 않도록 막는다.
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Unit
은 변경되지 않는 키 값으로 설정되어 있어, 이LaunchedEffect
블록이 최초 컴포지션 시에만 한 번 실행되도록 보장- 이
LaunchedEffect
블록은focusRequester
가Box
에 포커스를 요청하도록 하여, 로딩 화면이 활성화되면Box
가 포커스를 우선적으로 가지게 하는 용도
결과
위와 같은 수정 사항을 통해 SavePhotoScreen
에서 발생한 무한 로딩 문제와 오버레이 터치 및 포커스 차단 문제를 모두 해결할 수 있었다.
SavePhotoScreen
의 상태 관리 로직을 ViewModel에 위임하여 무한 로딩 문제를 해결하였다.RotatingImageLoading
에 포커스 및 터치 이벤트 소비 기능을 추가하여, 로딩 중에도 Scaffold와의 상호작용을 차단하였다.