-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FEAT: 상황에 따른 에러 핸들링 #16
Changes from 5 commits
ff20c62
6b69591
eac3ca5
8a96b38
02a3bc2
ff14031
4a39ff5
55f1954
923ab7d
0791aa8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// | ||
// Error.swift | ||
// 24HANE | ||
// | ||
// Created by Katherine JANG on 5/10/24. | ||
// | ||
|
||
import Foundation | ||
|
||
enum CustomError: Error { | ||
case tokenExpired | ||
case wrongQueryType | ||
case networkDisconnected | ||
case unAuthorized | ||
case internalServer | ||
case responseBodyEmpty | ||
case decodeFailed | ||
case invalidURL | ||
case unknownError(String) | ||
case none | ||
} | ||
|
||
extension CustomError: LocalizedError { | ||
public var errorDescription: String? { | ||
switch self { | ||
case .tokenExpired: | ||
return "사용자 토큰이 만료되었습니다" | ||
case .wrongQueryType: | ||
return "잘못된 요청입니다" | ||
case .networkDisconnected: | ||
return "네트워크 상태가 원활하지 않습니다" | ||
case .unAuthorized: | ||
return "알 수 없는 사용자입니다" | ||
case .internalServer: | ||
return "서버 에러 발생" | ||
case .responseBodyEmpty: | ||
return "내부 에러 발생" | ||
case .decodeFailed: | ||
return "내부 에러 발생" | ||
case .invalidURL: | ||
return "잘못된 접근입니다" | ||
case .unknownError: | ||
return "원인을 알 수 없는 에러 발생" | ||
case .none: | ||
return nil | ||
} | ||
} | ||
|
||
public var recoverySuggestion: String? { | ||
switch self { | ||
case .tokenExpired: | ||
return "다시 로그인해주세요" | ||
case .wrongQueryType: | ||
return "다시 시도해주세요" | ||
case .networkDisconnected: | ||
return "Wi-Fi 혹은 데이터 확인 후 다시 시도해주세요" | ||
case .unAuthorized: | ||
return "로그인 정보를 다시 확인해주세요" | ||
case .internalServer: | ||
return "개발팀에게 문의해주세요" | ||
case .responseBodyEmpty: | ||
return "개발팀에게 문의해주세요" | ||
case .decodeFailed: | ||
return "개발팀에게 문의해주세요" | ||
case .invalidURL: | ||
return "개발팀에게 문의해주세요" | ||
case .unknownError: | ||
return "개발팀에게 문의해주세요" | ||
case .none: | ||
return nil | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// | ||
// ErrorHandler.swift | ||
// 24HANE | ||
// | ||
// Created by Katherine JANG on 5/10/24. | ||
// | ||
|
||
import Foundation | ||
import SwiftUI | ||
|
||
|
||
class ErrorHandler: ObservableObject { | ||
|
||
static let shared = ErrorHandler() | ||
|
||
var errorType: CustomError = .none | ||
var occurredError: Error? = nil | ||
@Published var showAlert: Bool = false | ||
@Published var signInRequired: Bool = false | ||
|
||
private init() { } | ||
|
||
func errorFromHttpRequest(_ statusCode: Int?) throws { | ||
switch statusCode { | ||
case 400: | ||
throw CustomError.wrongQueryType | ||
case 401: | ||
throw CustomError.unAuthorized | ||
case 500: | ||
throw CustomError.internalServer | ||
default: | ||
throw CustomError.unknownError("\(statusCode)") | ||
} | ||
} | ||
|
||
@MainActor | ||
func updateErrorView() { | ||
switch self.errorType { | ||
case .tokenExpired, .unAuthorized: | ||
self.signInRequired = true | ||
case .wrongQueryType, .networkDisconnected, .internalServer, .responseBodyEmpty, | ||
.decodeFailed, .unknownError, .invalidURL: | ||
self.showAlert = true | ||
case .none: | ||
break | ||
} | ||
} | ||
|
||
func verifyError(_ error: Error) async { | ||
switch error { | ||
case DecodingError.dataCorrupted: | ||
self.errorType = .decodeFailed | ||
case URLError.timedOut: | ||
self.errorType = .networkDisconnected | ||
case URLError.networkConnectionLost: | ||
self.errorType = .networkDisconnected | ||
case is CustomError: | ||
self.errorType = error as? CustomError ?? .none | ||
default: | ||
self.errorType = .unknownError(error.localizedDescription.description) | ||
} | ||
} | ||
Comment on lines
+48
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드가 클라이언트 에러를 구분하는 책임을 맡은 만큼 함수 이름에서도 errorFromHttpRequest처럼 그 부분이 보였으면 좋겠습니다. |
||
|
||
@MainActor | ||
func handleError(_ error: Error) { | ||
Task { | ||
await self.verifyError(error) | ||
self.updateErrorView() | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,23 +31,23 @@ class NetworkManager: NetworkProtocol { | |
|
||
func getRequest<T>(_ urlPath: String, type: T.Type) async throws -> T? where T : Decodable { | ||
guard let url = URL(string: apiRoot + urlPath) else { | ||
/// FIXME: invalid URL의 경우 error handling | ||
return nil | ||
throw CustomError.invalidURL | ||
} | ||
|
||
guard let token = UserDefaults.standard.string(forKey: "Token") else { | ||
/// FIXME: token invalid 경우에 signIn 상태 변경 | ||
throw MyError.tokenExpired("get new token!") | ||
throw CustomError.tokenExpired | ||
} | ||
|
||
var request = URLRequest(url: url) | ||
request.httpMethod = "GET" | ||
request.allHTTPHeaderFields = [ | ||
"Authorization": "Bearer \(String(describing: token) )"] | ||
let (data, response) = try await session.data(for: request) | ||
guard (response as? HTTPURLResponse)?.statusCode == 200 else { | ||
/// FIXME: Status Code에 따른 Error Handling | ||
throw MyError.tokenExpired("request Failed") | ||
let statusCode = (response as? HTTPURLResponse)?.statusCode | ||
guard statusCode == 200 else { | ||
try ErrorHandler.shared.errorFromHttpRequest(statusCode) | ||
throw CustomError.unknownError("\(statusCode)") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요 부분이 이해가 잘 안갑니다. 코드가 200이 아닌 경우 그 상태코드를 들고 다시 errorFormHttpRequest를 이용해서 커스텀 에러에 속해있는지를 파악하는 거라 생각합니다. 이 부분에서 catch를 사용하지 않는 이유가 궁금합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아뇨,errorFormHttpRequest를 이용해서 커스텀 에러에 속해있는지를 파악하는 것이 아닌 errorFormHttpRequest를 이용하여서 statuscode에 따라 발생한 에러를 custom 에러로 변환하는 과정입니다..! catch 후 에러를 핸들링 하는 과정을 한 곳에서 진행하는게 통일성 있을 듯 하여(요청한 vm class 내의 메서드에서) 해당 부분에서는 catch 하지 않고 다시 throw 후, 상위 메서드에서 catch 후 핸들링 하는 방식을 선택하였습니다. 만약 해당 request 메서드 내에서 catch 를 하게 되면, 상위 request를 호출한 메서드 내에서는 api request 시도에 실패하였다는 것을 알기 어려울 것 같습니다. 그렇게 될 경우, 요청 실패로인해 class내의 프로퍼티를 업데이트 하지 않아도 되는 경우에도 업데이트가 진행되는 등의 문제가 발생할 것 같습니다! 이와 같은이유로 다시 throw하게 처리하였는데 에러를 어디서 catch 하고 핸들링 하면 좋을지에 대해서는 추가적으로 고민해보겠습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 의도: 최상단으로 throw를 하기 위한 방법 의문: 책임의 소재 |
||
} | ||
let decodedData = try JSONDecoder().decode(type.self, from: data) | ||
return decodedData | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
만약 좀 더 발전시킨다면 에러가 일어나면 개발진 슬랙이나 이메일, 구글 스프레드에 바로 올려버리는 것도 좋아보이네요