Skip to content

[feat] 에러처리 추가 + 에러 모달,뷰 추가 #453

Open
hye0njuoo wants to merge 23 commits into
feat/base-error_silo_7from
feat/#435-errormodal
Open

[feat] 에러처리 추가 + 에러 모달,뷰 추가 #453
hye0njuoo wants to merge 23 commits into
feat/base-error_silo_7from
feat/#435-errormodal

Conversation

@hye0njuoo

Copy link
Copy Markdown
Member

✅ Check List

  • merge할 브랜치의 위치를 확인해 주세요.(main❌/develop⭕)
  • 2인 이상의 Approve를 받은 후 머지해주세요.
  • 변경사항은 500줄 이하로 유지해주세요.
  • PR에는 핵심 내용만 적고, 자세한 내용은 트슈에 작성한 뒤 링크를 공유해주세요.
  • Approve된 PR은 Assigner가 직접 머지해주세요.
  • 수정 요청이 있다면 반영 후 다시 push해주세요.

📌 Related Issue

  • closed #이슈번호

📎 Work Description

서비스 전반의 에러 응답을 4가지 케이스(① 존재하지 않는 데이터 ② 서버 통신 오류 ③ 서버 통신 오류 - AI 피드백 ④ 네트워크 오류)로 분류하고, 모든 네트워크 화면이 일관된 정책으로 사용자에게 안내하도록 전역 에러 처리 구조를 도입했습니다.

  • 예외 땜질이 아니라 단일 매핑 지점 + 표현 정책 분리로 구조적으로 해결하기 위해 고민했고 아래 구조를 도입했습니다
  • 앞으로 추가되는 화면도 동일 정책을 그대로 따르도록 설계했습니다
  • 정책(문구/이미지/버튼)은 한 곳(SSOT)에서만 수정하면 전 화면 반영될수 있도록 별도 presenter 컴포넌트를 추가했습니다
  • 기존에 전역 브로드케스트로 에러를 처리하고 모든 에러든 동일하게 처리했으나 해당 구조를 제거하고 에러별로 처리할수 있도록 개선했습니다

에러 정책 (4 케이스)

케이스 표현 형태 동작
존재하지 않는 데이터 페이지(탭X: 이전 페이지로 돌아가기) / 단어장은 팝업(확인) 탈퇴 유저 프로필·삭제/공유취소된 일기 등
서버 통신 오류 페이지(탭O·탭X: 다시 시도) / 모달(확인, 콘텐츠 유지 필요 시) 5xx·일시 장애. 다시 시도 → API 재호출
서버 통신 오류 - AI 피드백 전용 풀페이지(피드백 다시 요청하기) 피드백 생성 실패. 뒤로가기 시 작성 내용 보존 화면으로
네트워크 오류 페이지(다시 시도) / 토스트(화면 이동 직전 끊김 감지 시) "인터넷 연결이 불안정해요"

탭O(탭바 유지)/탭X(뒤로가기 버튼)는 네비게이션 스택 깊이로 자동 판별.
네트워크 오류는 기획상 모달로 노출하지 않음 → 모달 요청이라도 네트워크면 토스트로 override.


아키텍처

[Network 응답] → NetworkError
      │  toHilingualError()  (단일 매핑 지점, DataNotFoundPolicy)
      ▼
HilingualError (Core: dataNotFound / server / network / unauthorized)
      │  BaseService.mapError (choke-point)
      ▼
[화면] errorPresenter.show(error, form:, page:, retry:)
      │  ErrorContentPolicy (문구·이미지·버튼 SSOT)
      ▼
FullPageErrorView / 모달(DialogManager) / 토스트
  • HilingualError (HilingualCore): 앱 전역 도메인 에러. Presentation이 Network에 의존하지 않으므로 Core에 위치.
  • NetworkError+HilingualError (HilingualNetwork): NetworkError → HilingualError 변환의 유일한 지점. DataNotFoundPolicy(백엔드 비즈니스 코드 집합)로 "존재하지 않는 데이터"를 분류.
  • BaseService: 모든 요청이 거치는 choke-point에서 .mapError { $0.toHilingualError() }. (전역 NotificationCenter 브로드캐스트 방식 제거)
  • ErrorPresenter (@MainActor): 화면에 붙어 form(fullPage/modal/toast/fullPageFeedback)과 retry만 받아 표현. 풀페이지는 네비게이션 컨트롤러 뷰 위에 올려 헤더까지 덮고(탭만 유지), 시스템 스와이프-백 비활성화.
  • ErrorContentPolicy: 케이스별 문구/이미지/버튼/배치 SSOT.
  • FullPageErrorView: 탭O(탭바로부터 230 위 버튼)/탭X(좌상단 뒤로가기 + 중앙 버튼)/AI피드백(하단 풀폭) 레이아웃.
  • NetworkMonitor + NetworkAwareNavigationController: NWPathMonitor로 끊김 상태 추적, 화면 이동(push) 직전 오프라인이면 이동을 막고 토스트. (OfflineNavigable로 예외 지정 가능)
  • BaseUiViewController: 화면별 NWPathMonitor 중복 제거, errorPresenter 공통 제공.

적용 화면 (전 네트워크 화면)

홈 · 단어장(목록/상세/복습/북마크) · 피드(목록/공감/비공개) · 피드 상세(공유일기) · 피드 프로필(내/타유저) · 일기 상세/피드백 · 추천표현 · 마이페이지 · 공지(목록/상세/설정) · 팔로우/차단 목록 · 프로필/닉네임 편집 · 온보딩(가입) · 피드 검색 · AI 피드백(LoadingView)

각 화면: 전체 로드 실패 → loadError(페이지), 부분 동작 실패 → actionError(모달, 네트워크면 토스트).


주요 버그 수정 (작업 중 발견)

  • Feed 좋아요/비공개 API 미호출 버그: _ = self?.toggleLike(...)로 Combine 퍼블리셔를 구독하지 않아 실제 API가 호출되지 않던 문제 → flatMap 구독으로 수정. 좋아요 토스트도 응답 성공 시에만 노출(오프라인 시 네트워크 토스트와 중복 제거).
    최대한 앞으로는 낙관적 ui 갱신을 지향해야될거 같습니다 유의해주세요
  • 단어 상세/복습 조회 실패 삼킴: receiveCompletion: { _ in } / catch { Empty }로 에러를 삼켜 "단어 없음"으로 오표시 → actionError로 surface(네트워크면 토스트).
  • 홈 재진입 시 네트워크 에러 누락, 단어장 풀로드 모달/페이지 구분, 추천표현 북마크 오프라인 처리, 404 일괄 매핑 오류(정상 빈 상태까지 dataNotFound 처리되던 것) 등 정정.

디버그 도구 (DEBUG 전용, prod 빌드 미포함)

디버그 행을 추가했습니다 추후에 디버그가 필요할경우 추가해주시면 될거 같습니다

  • 마이 → 🛠 디버그 → 강제 에러: 화면별로 끔/서버/데이터없음/네트워크를 런타임 토글, 강제 실패 횟수(재시도 복구 확인용) 설정. NetworkDebug(Core, #if DEBUG)가 BaseService에서 해당 path 요청을 강제 실패하도록해 QA를 우선 진행했습니다
  • 디버그 홈(DebugViewController)은 추후 디버그 기능을 행으로 추가 가능한 메뉴 구조로 추가했습니다 @jjwm10625 앞으로 디버깅 필요할때 여기서 버튼같은거 만들어서 사용하면 될거 같아요~! ㅋㅋ

수정

  • HilingualNetwork/.../BaseService.swift (choke-point 매핑, transport 에러 재귀 추출했습니다. 이제 network 패키지의 단순 네트워크 유실만 처리하는게 아닌 실 서버로 부터 전달받는 유실을 처리합니다)
  • Common/Base/{BaseUiViewController, TabBarViewController}.swift
  • 전 화면 VM/VC (loadError/actionError 적용해 일괄적으로 적용되도록했습니다)
  • Loading/{LoadingView, LoadingViewController}.swift (AI 피드백 오류페이지를 신규로 추가했습니다)

테스트

  • 마이 → 디버그 → 강제 에러에서 화면별로 서버/데이터없음/네트워크 토글 후 진입하여 탭O·탭X 페이지/모달/팝업/토스트 UI 확인.
  • 다시 시도(강제 실패 횟수=1) → 실제 API 재호출 및 복구 확인.
  • 비행기모드로 화면 이동 차단 토스트 확인.

📷 Screenshots

기능/화면 iPhone SE iPhone 16 Pro
A 기능
B 기능
C 기능

💬 To Reviewers

  • 이전에 모든 화면에서 error처리가 단순 Error로 처리되던걸 새로운 정책을 추가하는 대공사를 했습니다... QA도 한번씩 해주십셔
  • 우선은 작업할때 domain모듈이랑 networkf 모듈의 따로 테스트코드를 추가해 무결성을 검증하긴 했습니다만 이번 PR에는 추가하지 않았습니다. 추후에 지속가능한 테스트코드를 도입하기 위함이고 현구조상 테스트가 유리하기때문에 테스트코드 도입을 적극적으로 검토해봅시다

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: dc71fe4d-7cf9-4f33-9b0c-0c8e2f55321c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#435-errormodal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hyeyeonie hyeyeonie left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

와 끝까지 다봤다
현주야 넌 도대체 어떤 싸움을 한 거니 ,, ? 에러처리의 권위자야 정말 고생많았다 큐ㅠㅠ

Comment on lines +8 to +29
// 앱 전역에서 사용하는 에러 분류 타입.
// 네트워크/인프라 계층의 구체적인 에러(NetworkError, URLError 등)는
// Data·Network 경계에서 이 타입으로 변환되어 상위 계층으로 흐른다.
// Presentation 은 이 타입만 보고 에러 정책(표현 형태/문구/CTA)을 결정한다.

import Foundation

/// 기획 에러 정책의 4가지 분류와 1:1 대응되는 도메인 에러.
///
/// - `dataNotFound`: 존재하지 않는 데이터 조회 (탈퇴 유저, 공유 취소된 일기 등 / HTTP 404·약속된 비즈니스 코드)
/// - `network`: 인터넷 연결 불안정/끊김
/// - `server`: 서버 통신 오류 (5xx, 타임아웃, 디코딩 실패, 분류 불가 등 일시적 오류)
/// - `unauthorized`: 인증 만료. 세션 만료 흐름에서 별도 처리되므로 화면 에러 UI 대상이 아니다.
public enum HilingualError: Error, Equatable {
case dataNotFound
case network
case server
case unauthorized

/// 분류 불가능한 임의의 `Error` 를 안전하게 도메인 에러로 흡수한다.
/// 이미 `HilingualError` 면 그대로, 아니면 일시적 서버 오류(`.server`)로 본다.
/// (예: Data 계층에서 던지는 디코딩 실패 등은 정책상 서버 오류로 취급)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

주석이 꽤나 많은데 다 필요한 부분일까요?

Comment on lines +8 to +23
// 화면(UIViewController)에 붙어 에러를 정책에 따라 표현하는 경량 헬퍼.
// 화면은 "이 에러를 어떤 형태로 보여줄지(form)"와 "다시 시도 동작(retry)"만 넘기면 된다.
// 실제 문구/이미지/버튼/내비게이션은 ErrorContentPolicy 가 결정한다.
//
// 사용 예 (ViewController):
// output.loadError
// .sink { [weak self] error in
// self?.errorPresenter.show(error, form: .fullPage) {
// self?.reloadSubject.send(()) // 다시 시도
// }
// }
// output.data
// .sink { [weak self] data in
// self?.errorPresenter.dismiss() // 성공 시 에러 뷰 제거
// ...
// }

Copy link
Copy Markdown
Contributor

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.

그러면,, 꼭 사용법 지켜주기,,,, 일부러 주절 주절 적긴 했는데,, 지웁니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants