본문 바로가기

TIL

[TIL] Moya 라이브러리 Plugins Logging 도입기

도입 배경

네트워크 통신에 관해 디버깅하던 도중 추상화되어 있는 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