[FEAT/#870] 전역 네트워크 에러 처리#873
Conversation
| LaunchedEffect(isOffline) { | ||
| if (isOffline) { | ||
| appState.dialogStateHolder.dismissDialog() | ||
| onShowMessage(Toast("인터넷 연결이 불안정해요.")) | ||
| } else { | ||
| shouldShowNetworkError = false | ||
| } | ||
| } | ||
|
|
||
| LaunchedEffect(currentBackStackEntry) { | ||
| val currentDestination = currentBackStackEntry?.destination ?: return@LaunchedEffect | ||
| shouldShowNetworkError = | ||
| if (currentDestination.hasRoute(Splash::class) || currentDestination.hasRoute(Auth::class)) { | ||
| false | ||
| } else { | ||
| isOffline | ||
| } | ||
| } |
There was a problem hiding this comment.
shouldShowNetworkError는 LaunchedEffect(currentBackStackEntry) 안에서만 true로 설정되고 이 이펙트는 백스택 엔트리가 바뀔 때만 재실행되겠죠? LaunchedEffect(isOffline)는 isOffline == true일 때 다이얼로그를 닫고 토스트를 띄우지만 shouldShowNetworkError = true는 설정하지 않고요.
결과적으로 사용자가 화면 이동 없이 오프라인이 되면currentBackStackEntry가 변하지 않아 오버레이가 절대 뜨지 않고 토스트만 한 번 노출되는걸로 보입니다. 오프라인 중에는 다이얼로그도안뜨고 dialogTrigger도 !isOffline로 게이팅되어서 사용자는 토스트 외에 어떤 에러 표시도 못 받아요. 즉 오버레이는 오프라인 상태로 이미 진입해 있는 채로 "이동"할 때만 나타나요.
| ) | ||
| } | ||
|
|
||
| RetryOnReconnect(onRetry = { onTabRefresh(pageState.tabType) }) |
There was a problem hiding this comment.
FollowListScreen만 HorizontalPager { page -> ... } 콘텐츠 람다 내부에 배치되어 있어서 두 탭 모두 LocalReconnectEvents를 구독하고, 재연결 시 onTabRefresh가 화면에 보이지 않는 탭까지 포함해 두 번 호출되어 중복 네트워크 요청이 발생하는데 어떤 의도가 있을까요?? 의도에 따라서 달라질 부분 같아요
| onRetryClick = { | ||
| if (isOffline) { | ||
| shouldShowNetworkError = true | ||
| onShowMessage(Toast("인터넷 연결이 불안정해요.")) |
There was a problem hiding this comment.
연결이 복구되면 자동으로 사라지니까"다시 시도" 버튼은 사실상 isOffline == true일 때만 노출되는거고.... onRetryClick은 거의 항상 if (isOffline) 분기를 타는데 그럼 토스트를 다시 띄우고 shouldShowNetworkError = true를 재대입할 뿐이네요?
reconnectEvents를 발행하는 else 분기는 아주 짧은 레이스 구간에서만 도달 가능하겠어요.
그리고 자동 재연결 컬렉터가 이미 재연결 시 이벤트를 발행하니까 수동 경로가 실행되면 실제 재연결 시 이중 트리거가 된느거 같아요
| val isBottomBarVisible by appState.isBottomBarVisible.collectAsStateWithLifecycle() | ||
| val currentTab by appState.currentTab.collectAsStateWithLifecycle() | ||
| val currentBackStackEntry by appState.navController.currentBackStackEntryAsState() | ||
| var shouldShowNetworkError by remember { mutableStateOf(false) } |
There was a problem hiding this comment.
이건 유지보수/정확성 면에서 좋지 않은거 같아요.
이건 isOffline이랑 현재 목적지만으로 결정되는데 굳이 두개로 나눠서 관리한게 와닿지 않아요. 어떻게 개선해보면 좋을까요?
| .collect { reconnectEvents.emit(Unit) } | ||
| } | ||
|
|
||
| LaunchedEffect(isOffline) { |
There was a problem hiding this comment.
이거 스플래시랑 로그인에서도 뜨지 않을까요? 확인해보셨나요?
| onBackClick = appState::navigateUp, | ||
| onRetryClick = { | ||
| if (isOffline) { | ||
| shouldShowNetworkError = true |
| @Composable | ||
| fun RetryOnReconnect(onRetry: () -> Unit) { | ||
| val reconnectEvents = LocalReconnectEvents.current | ||
| val currentOnRetry by rememberUpdatedState(onRetry) | ||
|
|
||
| LaunchedEffect(Unit) { | ||
| reconnectEvents.collect { currentOnRetry() } | ||
| } | ||
| } |
There was a problem hiding this comment.
이거 오프라인 상태에서 로드 실패한 화면이 재연결 이후 복귀하면 이미 지나간 재연결 이벤ㅌ트를 받지 못하고 대신 컴포지션 복원시 재실행 되는 자체 init에 의존하는데요. 데이터 로드가 Route의 이펙트핸들러가 아니라 viewModel init에서 이뤄지는 화면들 확인을 해봐야겠어요
nhyeonii
left a comment
There was a problem hiding this comment.
작업 고생하셨습니다 !! 에러처리 하나하나 짚고 넘어가야할 부분들이 많네요..
| } | ||
|
|
||
| private fun loadInitialData() { | ||
| fun loadInitialData() { |
There was a problem hiding this comment.
RetryOnReconnect(onRetry = viewModel::xxx)에 넘기기 위해 원래 private였던 최초 로딩 함수를 그대로 public으로 열었습니다. 이렇게 하면 최초 로딩용 내부 함수라는 의도가 사라지고 외부에서 아무 때나 재호출 가능한 public API가 되어버릴 것 같아요. retry()나 refresh()처럼 의도가 드러나는 별도 public 함수를 하나 두고 그 안에서 기존 private 함수를 호출하는 편이 캡슐화 측면에서 더 안전할 수도 있을 것 같다는 생각이 드네용... !
Related issue 🛠
Work Description ✏️
네트워크 오프라인 상태를 앱 전역에서 처리하도록 추가했습니다.
HilingualNetworkErrorView를 추가했습니다.LocalReconnectEvents를 추가해 온라인 복귀 이벤트를 CompositionLocal로 전달합니다.RetryOnReconnect를 추가해 각 화면이 재연결 시 데이터 재요청을 쉽게 구독할 수 있게 했습니다.MainScreen에서 오프라인 상태일 때 네트워크 에러 화면을 overlay로 노출합니다.Screenshot 📸
Screen_Recording_20260701_234710.mp4
To Reviewers 📢
MainScreen에서Splash,AuthRoute의 경우에는 에러뷰 표시를 제외시켰습니다.