도입 배경
네트워크 통신에 관해 디버깅하던 도중 추상화되어 있는 Moya를 print로 일일히 원하는 시점에 찍어가며 디버깅하다보니 디버깅하는데 너무나 큰 불편함을 느껴 Moya 라이브러를 통한 Logging을 적용하게 되었습니다.
Moya Plugins
Moya 공식 문서에 따르면 Plugins 요청과 응답을 수정(modify)하거나 side-effects를 수행하는 데 사용된다고 기술되어 있습니다.
Moya plugins are used to modify requests and responses or perform side-effects.
이 수정 과정을 도와주는 메서드들이 있는데요. 이들은 다음과 같습니다.
public protocol PluginType {
/// Called to modify a request before sending.
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
/// Called immediately before a request is sent over the network (or stubbed).
func willSend(_ request: RequestType, target: TargetType)
/// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
/// Called to modify a result before completion.
func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
}
- prepare 메서드는 TargetType에서 URLRequest 타입으로 네트워크 요청을 위한 URLRequest 구성을 모두 마친 후 호출되는데, 이 시점에 Request에 새로운 Header등의 추가적인 작업을 진행할 수 있습니다.
- willSend 메서드는 네트워크 요청이 보내기 직전에 호출되며, 이 시점에 요청을 검사하고 side-effects 수행할 수 있습니다.(e.g. logging)
- didReceive 메서드는 네트워크 응답을 수신한 뒤 호출되며, 이 시점에 응답에 대한 검사 및 side-effects를 수행할 수 있습니다.
- process 메서드는 응답을 가공하거나 추가적인 처리를 위해 호출됩니다. 예를 들어, 응답에서 특정 데이터를 추출하거나 변환할 수 있습니다.
Plugins의 역할
Moya 공식문서에 따르면 Plugins의 Authentication, Access Token 처리, Network Activity Indicator, Logging 등의 역할을 수행할 수 있다고 합니다. 그중 제가 가장 필요한 기능인 Logging에 대해서만 구현해보았습니다.
Logging 구현
import Foundation
import Moya
final class LoggerPlugin: PluginType {
func prepare(_ request: URLRequest, target: any TargetType) -> URLRequest {
var request = request
let accessToken = KeychainManager.read(key: .accessToken)
let refreshToken = KeychainManager.read(key: .refreshToken)
request.setValue(accessToken, forHTTPHeaderField: Headers.authorization.rawValue)
request.setValue(refreshToken, forHTTPHeaderField: Headers.refreshToken.rawValue)
return request
}
// Request를 보낼 때 호출
func willSend(_ request: RequestType, target: TargetType) {
guard let httpRequest = request.request else {
print("--> 유효하지 않은 요청")
return
}
let url = httpRequest.description
let method = httpRequest.httpMethod ?? "unknown method"
var log = "----------------------------------------------------\n\n[\(method)] \(url)\n\n----------------------------------------------------\n"
log.append("API: \(target)\n")
log.append("\n------------------- Network Request Headers -------------------\n")
if let headers = httpRequest.allHTTPHeaderFields, !headers.isEmpty {
log.append("header: \(headers)\n")
}
log.append("\n------------------- Network Request Body -------------------\n")
if let body = httpRequest.httpBody, let bodyString = body.toPrettyPrintedString {
log.append("\(bodyString)\n")
}
log.append("------------------- END \(method) --------------------------\n")
print(log)
}
// Response가 왔을 때
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case let .success(response):
onSuceed(response, target: target, isFromError: false)
case let .failure(error):
onFail(error, target: target)
}
}
func onSuceed(_ response: Response, target: TargetType, isFromError: Bool) {
let request = response.request
let url = request?.url?.absoluteString ?? "nil"
let statusCode = response.statusCode
var log = "------------------- 네트워크 통신 성공 -------------------"
log.append("\n[\(statusCode)] \(url)\n----------------------------------------------------\n")
log.append("API: \(target)\n")
log.append("\n------------------- Network Response Headers -------------------\n")
response.response?.allHeaderFields.forEach {
log.append("\($0): \($1)\n")
}
log.append("\n------------------- Network Response Body -------------------\n")
if let reString = response.data.toPrettyPrintedString {
log.append("\(reString)\n")
}
log.append("------------------- END HTTP (\(response.data.count)-byte body) -------------------")
print(log)
if isFromError {
do {
let errorCode = try JSONDecoder().decode(ErrorCode.self, from: response.data)
if errorCode.errorCode == "E06" {
print("Refresh Token is Expired.")
KeychainManager.deleteAll()
NotificationCenter.default.post(name: .GoBackToOnboardingView, object: nil, userInfo: ["goBackToOnboardingViewTrigger": true])
}
} catch {
print("Decoding Error", error)
}
}
}
func onFail(_ error: MoyaError, target: TargetType) {
if let response = error.response {
onSuceed(response, target: target, isFromError: true)
return
}
var log = "네트워크 오류"
log.append("<-- \(error.errorCode) \(target)\n")
log.append("\(error.failureReason ?? error.errorDescription ?? "unknown error")\n")
log.append("<-- END HTTP")
print(log)
}
}
참고 사이트
https://github.com/Moya/Moya/blob/master/docs/Plugins.md (Moya 공식문서 - Plugins)
https://velog.io/@ezidayzi/iOS-Moya-Logging-Plugin (Logging 코드 참고)
https://eeyatho.tistory.com/256 (Plugins 도식도 참고)
'TIL' 카테고리의 다른 글
[TIL] 카카오 소셜 로그인 구현 (0) | 2024.06.13 |
---|---|
[TIL] Keychain 적용기 (0) | 2024.06.13 |
[TIL] SwiftUI에서 Bottom Sheet 구현기 (0) | 2024.06.10 |
[TIL] SwiftUI 시작화면 제작기 (0) | 2024.06.10 |
[TIL] Custom Modifier 적용기 (0) | 2024.06.09 |