Skip to content

[FEAT/#868] 공통 로드 에러 구조 세팅#869

Open
nahy-512 wants to merge 13 commits into
developfrom
feat/#868-global-server-error
Open

[FEAT/#868] 공통 로드 에러 구조 세팅#869
nahy-512 wants to merge 13 commits into
developfrom
feat/#868-global-server-error

Conversation

@nahy-512

@nahy-512 nahy-512 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Related issue 🛠

Work Description ✏️

공통 로드 에러 처리 구조를 추가하고, 화면별 로드 실패 케이스에 적용했습니다.

  • LoadErrorHandleAction을 추가해 실패 화면에서 수행할 동작을 타입으로 표현했습니다.
    • Retry: 다시 시도
    • Back: 이전 화면으로 돌아가기
    • NotFound: 요청한 데이터를 찾을 수 없음
  • LoadErrorHandleActionRetry / Back / NotFound가 같은 depth를 갖는 sealed interface 구조로 정리했습니다.
  • UiState.Failure가 반드시 handleAction을 갖도록 수정했습니다.
    • 실패 액션의 기본값 판단은 화면이 아니라 ViewModel 생성 시점에서 결정하도록 했습니다.
  • Throwable.isHttpNotFound() / Throwable.toLoadErrorHandleAction(defaultAction)을 추가했습니다.
    • HTTP Status 404는 NotFound로 매핑합니다.
    • 그 외 실패는 호출부에서 넘긴 기본 액션(Retry 또는 Back)을 사용합니다.
  • HilingualLoadErrorView를 추가해 공통 로드 실패 화면의 레이아웃과 문구를 한 곳에서 관리하도록 했습니다.
  • LoadErrorViewAction을 추가해 Retry / Back / NotFound별 버튼 액션을 타입으로 구분했습니다.
    • 기존처럼 onActionClick, isBackVisible, onBackClick을 독립적으로 넘기며 잘못 조합될 수 있는 구조를 줄였습니다.
  • DialogType을 추가해 일반 에러 다이얼로그와 404 다이얼로그 메시지를 분리했습니다.
  • 주요 화면의 로드 실패 상태에 공통 에러뷰와 화면별 에러 액션을 적용했습니다.

Screenshot 📸

image

Uncompleted Tasks 😅

  • 화면별 에러 적용
  • 에러 이벤트 추가

To Reviewers 📢

  • 에러뷰 별로 컴포넌트를 나눌까 하다가, LoadErrorHandleAction(RETRY/BACK/NotFound) 타입으로 구별했습니다.
  • 세 경우는 레이아웃·동작 골격이 같고 문구/버튼/액션만 달라서, UiState.Failure(handleAction)을 그대로 HilingualLoadErrorView에 흘려보내면 상태→UI 매핑이 단순해지고 레이아웃 일관성도 한 곳에서 강제된다고 생각했습니다.

@nahy-512 nahy-512 self-assigned this Jul 1, 2026
@nahy-512 nahy-512 requested a review from a team as a code owner July 1, 2026 13:31
@nahy-512 nahy-512 added FEAT✨ 사용자에게 제공되는 새로운 기능 구현 및 동작 변경 🍀작은나현 작은나현 담당 labels Jul 1, 2026
@nahy-512 nahy-512 requested review from angryPodo and nhyeonii and removed request for a team July 1, 2026 13:31
@nahy-512 nahy-512 linked an issue Jul 1, 2026 that may be closed by this pull request

@angryPodo angryPodo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생했어요! 코멘트 확인하고 재요청 한번 주세요!

import com.hilingual.core.common.model.LoadErrorHandleAction
import retrofit2.HttpException

fun Throwable.isHttpNotFound(): Boolean = this is HttpException && code() == 404

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 not-found를 HTTP Status로 주는지 body code로 주는지 확인이 필요할거 같은데 body-code 방식이라면 BaseResponse.code를 봐야하지 않나요??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 케이스는 retrofit2.HttpException: HTTP 404로 내려와서 HTTP Status 기준 매핑으로 동작하는 것을 확인했습니다.

이번 toLoadErrorHandleAction()은 공통 로드 실패 화면에서 “리소스 없음”을 판단하는 최소 기준으로 두고 싶어서, 우선 transport layer에서 명확한 404인 HttpException.code() == 404만 매핑했습니다. 서버의 body code(40405 등)는 API/도메인별 의미가 섞일 수 있어 공통 Throwable 확장에서 prefix 기준으로 NotFound 처리하면 오탐 가능성이 있다고 봤습니다. 필요하면 이후 서버 코드 체계를 확인한 뒤 별도 에러 모델/매퍼에서 보강하겠습니다!

Comment on lines +35 to +41
@Composable
fun HilingualLoadErrorView(
onActionClick: () -> Unit,
modifier: Modifier = Modifier,
isBackVisible: Boolean = false,
onBackClick: () -> Unit = {},
handleAction: LoadErrorHandleAction = LoadErrorHandleAction.Common(LoadErrorActionType.RETRY),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 자식 컴포넌트 구성이 Spacer(weight(2f)) → 콘텐츠 블록 → Spacer(weight(3f)) 순서로 파악 되는데요.

그러면 Center 배치는 의미가 없지 않나요? 좌상단에 화살표가 잘 뜨는지 프리뷰 말고 인앱 화면 체크해봐야 할거 같아요.

@nahy-512 nahy-512 Jul 7, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러네요ㅎ.ㅎ d2f1a05에서 수정했습니다! 인앱으로도 잘 뜨는 거 확인했어요~~ 매의눈 지적 감사합니다!!


import androidx.compose.runtime.Stable

@Stable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 어노테이션을...붙여야할지 말아야할지 고민이네요...어떤 의도로 붙이신건가요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleAction 파라미터 안정성을 명시하려고 붙였는데, 공통 모델이 Compose annotation에 의존하는 건 과한 것 같다는 생각도 드네요,,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d6b38f9에서 컴포넌트 단에서 action을 새로 관리하면서 Stable 어노테이션 같이 삭제했습니다!

Comment on lines +24 to +26
fun LoadErrorHandleAction?.orRetry(): LoadErrorHandleAction = this ?: LoadErrorHandleAction.Retry

fun LoadErrorHandleAction?.orBack(): LoadErrorHandleAction = this ?: LoadErrorHandleAction.Back

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UiState.Failure(handleAction: LoadErrorHandleAction? = null)와 두 확장 함수 구조상 각 화면이 null의 의미를 .orRetry() 또는 .orBack() 호출로 직접 결정해야 하는데요. 실패가 재시도일지 백일지 아는쪽은 생산자(ViewModel)인데 이번에 뷰모델 코드를 보면 Failure를 쏴서 자식을 버리고 각 화면이 기본값을 다시 판단하게 하고 있어요.

그럼 실제 화면들이 이를 도입하면 state.handleAction.orRetry() / .orBack() 호출이 여기저기 흩어지고 선택 기준이 제각각이 될 거같아요.

생성 시점에 기본값을 주는게 유지보수와 복잡성에서 좋을거같은데 어떻게 생각하시나요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동감합니다,, 실패 시점에 어떤 액션이 필요한지는 화면보다 생산자인 ViewModel이 더 잘 알고 있다고 판단해서 3a14720 UiState.Failure의 기본값을 제거하고, 각 ViewModel에서 Retry/Back/NotFound를 결정하도록 수정했습니다!

Comment on lines +37 to +41
onActionClick: () -> Unit,
modifier: Modifier = Modifier,
isBackVisible: Boolean = false,
onBackClick: () -> Unit = {},
handleAction: LoadErrorHandleAction = LoadErrorHandleAction.Common(LoadErrorActionType.RETRY),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴포넌트가 베리언트 별로 버튼 라벨은 선택하지만 동작 자체는 명확하지 않은 onActionClick 하나에 위임하는데요. Retry에는 재시도 람다를, Back/NotFound에는 뒤로가기 람다를 넘겨야 하는데 이 짝을 강제하는 장치가 없어요.

별개로 isBackVisibleonBackClickhandleAction과 독립적이라 NotFound(버튼 문구는 "돌아가기")를 isBackVisible = false로 렌더링하거나 상단바 뒤로가기는 onBackClick에 연결하면서 아래 버튼은 다른 onActionClick에 연결하는 것도 가능하겠어요.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

af4703e에서 구조 전면 수정했습니다!

HilingualLoadErrorView가 LoadErrorViewAction을 받도록 수정하면서 Retry, Back, NotFound 타입별로 필요한 콜백만 받게 해서 버튼 문구와 실제 액션의 짝이 타입으로 드러나도록 했습니다.

isBackVisible은 '화면에 백스택이 있는가'로 결정되기 때문에, Retry일 경우에 onBackClick이 null이 아닌지로 백 버튼 표시 여부를 판단해주고 있습니다.

Comment on lines +6 to +22
sealed class LoadErrorHandleAction {
data class Common(
val actionType: LoadErrorActionType,
) : LoadErrorHandleAction()

data object NotFound : LoadErrorHandleAction()

companion object {
val Retry = Common(LoadErrorActionType.RETRY)
val Back = Common(LoadErrorActionType.BACK)
}
}

enum class LoadErrorActionType {
RETRY,
BACK,
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모델링이 이중 구조라서 어색하고 일관성이 없다고 생각되는데요...

Retry/Back은 Common(LoadErrorActionType.RETRY/BACK)(sealed class가 enum을 감싸는 2단 구조)로 NotFound는 최상위 data object(1단)로 모델링되어 있죠. 그래서 HilingualLoadErrorView.kt:140-166에 중첩 when이 필요해고요.
API도 Retry/Back은 companion object val, NotFound는 서브타입이라 일관성이 없다고 생각해요.
플랫튼한 sealed 계층(Retry, Back, NotFound) 또는 단일 enum이 더 단순하고 1단 exhaustiveness를 보장하면 좋겠네요.

다만 현재 .contentDialogType.messagewhen은 이미 exhaustive해서 향후 서브타입 추가 시 컴파일 에러로 잡히는 안전성은 유지하는게 좋겠어요.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구현하면서 고민했던 부분이긴 했는데ㅎ.ㅎ 저도 단일 구조 모델링이 더 확인하기 쉽겠다는 생각이 들어 d6b38f9에서 Common + LoadErrorActionType 구조를 제거하고 Retry, Back, NotFound를 같은 depth의 sealed interface 구현체로 플랫하게 정리했습니다. when도 1단 exhaustive 분기로 단순화했어용!

modifier: Modifier = Modifier,
isBackVisible: Boolean = false,
onBackClick: () -> Unit = {},
handleAction: LoadErrorHandleAction = LoadErrorHandleAction.Common(LoadErrorActionType.RETRY),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleAction: LoadErrorHandleAction = LoadErrorHandleAction.Common(LoadErrorActionType.RETRY)는 이미 LoadErrorHandleAction.Retry로 제공되는 공유 인스턴스를 다시 만들어서 중복 생성 되고 있어요. 어떻게 개선해보면 좋을까요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

af4703e에서 HilingualLoadErrorViewhandleAction 기본값 자체를 제거하고, d6b38f9에서 LoadErrorHandleAction도 data object 기반으로 정리해서 1단계 enumd으로 중복 생성 여지가 없도록 수정했습니다.

fun Throwable.isHttpNotFound(): Boolean = this is HttpException && code() == 404

fun Throwable.toLoadErrorHandleAction(): LoadErrorHandleAction? =
if (isHttpNotFound()) LoadErrorHandleAction.NotFound else null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 Back을 만드는 경로가 없는데 의도된건가요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3a14720 toLoadErrorHandleAction(defaultAction) 형태로 바꿔서, 404면 NotFound, 그 외에는 호출부에서 넘긴 기본 액션(Retry 또는 Back)을 사용하도록 수정했습니다.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 파일명 오타 한번 잡아주세요🙏

@nhyeonii nhyeonii left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

방향성 자체가 좋은 것 같아요 !! LoadErrorHandleAction + HilingualLoadErrorView로 실패 화면 골격을 한 곳에 모으고, DialogType으로 에러/404 다이얼로그 문구를 분리한 접근이 인상 깊어요 작업 고생하셨습니다 !

fun show(onClick: () -> Unit) {
onShow(onClick)
fun show(
type: DialogType = DialogType.ERROR,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

show()/rememberDialogTrigger의 시그니처를 (DialogType, () -> Unit) -> Unit으로 바꾸면서 type: DialogType = DialogType.ERROR 기본값으로 하위 호환을 지킨 것 좋은 것 같아요.

@@ -71,6 +72,12 @@ fun HilingualErrorDialog(
}
}

private val DialogType.message: String

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 메세지를 DialogType.message 확장 프로퍼티로 다이얼로그 파일에 위치시켜두셧군뇨 !!

}
}.onLogFailure {
_uiState.update { UiState.Failure }
_uiState.update { UiState.Failure() }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UiState.Failure data class(handleAction)로 바뀌면서 컴파일은 맞춰주셨는데, 실제로는 handleAction을 안 넘기고 있어서 항상 null로 떨어질것같아요 ... !! 여기서 throwable.toLoadErrorHandleAction()으로 매핑해서 UiState.Failure(handleAction = throwable.toLoadErrorHandleAction()) 형태로 넘겨주는 게 이 PR에서 만든 구조를 실제로 쓰는 예시가 될 것 같은데, 이 PR이 공통 세팅만 다루는 PR이라 적용은 아직 작성 안 하신 게 맞는지 확인차 리뷰 남겨요 !

@nahy-512 nahy-512 Jul 7, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넵 공통 세팅만 다루고자 적용은 아직 안했던 게 맞습니다!!
null 부분은 개선 방안 고민해보겠습니다

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성 시점에 오류 처리할 수 있게끔 3a14720에서 수정했습니다!

화면 별로 에러 처리 적용한 것도 f881f64에 남겨두었어요:)

@nahy-512 nahy-512 requested a review from angryPodo July 7, 2026 08:34
@nahy-512

nahy-512 commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

@angryPodo 리뷰 모두 반영하고, 각 화면별 에러 처리 코드까지 추가했습니다!
확인 부탁드리겠습니다~!

@angryPodo angryPodo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코멘트 두개만 확인해주세요~

Comment on lines +123 to +133
val feedLoadError = uiState.loadErrorState(selectedFeedTab)
if (feedLoadError is UiState.Failure) {
HilingualLoadErrorView(
action = LoadErrorViewAction.Retry(
onRetryClick = { viewModel.loadFeedData(selectedFeedTab) },
),
modifier = Modifier.padding(paddingValues),
)
return
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failure면 FeedScreen 호출 자체를 건너뛰는거죠? 얼리리턴하고.
근데 FeedTopAppBAr나 탭로우는 전부 Screen 내부에 있는데요. loadInitialFeedData는 탭을 별개로 로드하니까 추천탭만 실패하고 팔로잉 탭은 성공해도 사용자는 전체 화면 에러뷰만 보게 되는 구조네요. 의도된걸까요?

Comment on lines +65 to +66
.onLogFailure {
showGoogleLoginErrorDialog(context)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일부러 로깅 두번하는건가요???

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 실수입니다ㅜ.ㅜ 제거했어요!! 꼼꼼히 봐주셔서 감사합니다🙌🏻

nahy-512 added 2 commits July 13, 2026 22:37
…-base

# Conflicts:
#	presentation/diarywrite/src/main/java/com/hilingual/presentation/diarywrite/DiaryWriteViewModel.kt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FEAT✨ 사용자에게 제공되는 새로운 기능 구현 및 동작 변경 🍀작은나현 작은나현 담당

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 공통 서버 로드 에러 대응

3 participants