Skip to content

중규_네트워크 Endpoint 분석, Moya 네트워크 레이어 분석, Moya 네트워크 레이어 구현

joongkyu-park edited this page May 1, 2023 · 1 revision

네트워크 Endpoint 분석

  • Endpoint

    • 서버와 클라이언트 간의 통신을 위한 API 요청이 가능한 주소를 나타내는 특정 URL 또는 URI
  • Endpoint 구성

    1. baseURL
    2. path
    3. method
      • HTTP 요청 방법을 지정. GET, POST, PUT, DELETE 등
    4. queryParameters
      • URL에 추가되는 쿼리 매개변수를 지정.
        쿼리 매개변수는 키와 값의 쌍으로 구성되며, 여러 개의 매개변수를 전달할 경우 "&"로 구분.
    5. bodyParameters
      • HTTP 요청에서 전송되는 데이터를 지정.
        일반적으로 POST나 PUT 메소드에서 사용되며, JSON이나 Form Data 형식으로 전송될 수 있다.
    6. headers
      • HTTP 요청에서 사용되는 헤더를 지정.
        일반적으로, 인증 토큰이나 언어, 콘텐츠 유형 등의 정보를 전송.
    7. sampleData
      • Endpoint 요청 시 반환되는 샘플 데이터를 지정.
        API 문서 작성 또는 테스트 용도로 사용된다.
  • 클라이언트 단에서는 이 Endpoint 요소들을 조합하여 request를 만들어서 서버와 HTTP 통신을 할 수 있다.

  • 예시

    // 참고: https://velog.io/@isouvezz/Swift-Network-Layer-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0#network-%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%84%A4%EA%B3%84
    
    enum HttpMethod: String {
        case get = "GET"
        case post = "POST"
        case put = "PUT"
        case delete = "DELETE"
    }
    
    // Endpoint 구성 요소
    protocol Requestable {
        var baseURL: String { get }
        var path: String { get }
        var method: HttpMethod { get }
        var queryParameters: Encodable? { get }
        var bodyParameters: Encodable? { get }
        var headers: [String: String]? { get }
        var sampleData: Data? { get }
    }
    
    extension Requestable {
        func getUrlRequest() throws -> URLRequest {
            let url = try makeUrl()
            var urlRequest = URLRequest(url: url)
            urlRequest.httpMethod = method.rawValue
    
            if let bodyParameters = try bodyParameters?.toDicionary() {
                if !bodyParameters.isEmpty {
                    urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: bodyParameters)
                }
            }
    
            headers?.forEach { key,value in
                urlRequest.setValue(value, forHTTPHeaderField: "\(key)")
            }
    
            return urlRequest
        }
    
        func makeUrl() throws -> URL {
            let fullPath = "\(baseURL)\(path)"
            guard var urlComponents = URLComponents(string: fullPath) else { throw NetworkError.componentsError}
    
            var urlQueryItems = [URLQueryItem]()
            if let queryParameters = try queryParameters?.toDicionary() {
                queryParameters.forEach { key,value in
                    urlQueryItems.append(URLQueryItem(name: key, value: "\(value)"))
                }
            }
    
            urlComponents.queryItems = urlQueryItems.isEmpty ? nil : urlQueryItems
            guard let url = urlComponents.url else { throw NetworkError.componentsError }
            return url
        }
    }

Moya 네트워크 레이어 분석

Moya 네트워크 레이어 구현

  • NetworkAPI

    import Foundation
    
    struct NetworkAPI {
        private let provider = NetworkProvider<NetworkService>()
    }
    
    // MARK: - API 요청 함수들
    extension NetworkAPI {
        func getMovies(category: String, completionHandler: @escaping (MovieResponse?, Error?) -> Void) {
            do {
                let request = try provider.request(.getMovies(category: category))
                let task = URLSession.testSession.dataTask(with: request) { (data, response, error) in
                    guard let data = data else {
                        completionHandler(nil, NetworkError.nilData)
                        return
                    }
                    guard let response = response as? HTTPURLResponse else {
                        completionHandler(nil, NetworkError.invalidServerResponse)
                        return
                    }
                    do {
                        let result = try self.judgeStatus(by: response.statusCode, data, type: MovieResponse.self)
                        completionHandler(result, nil)
                    } catch let error {
                        completionHandler(nil, error)
                    }
                }
                task.resume()
            } catch let error {
                completionHandler(nil, error)
            }
        }
    
    		// 중략
    }
    
    extension NetworkAPI {
        private func judgeStatus<T: Decodable>(by statusCode: Int, _ data: Data, type: T.Type) throws -> T {
            switch statusCode {
            case 200:
                return try decodeData(from: data, to: type)
            case 400..<500:
                throw NetworkError.requestError(statusCode)
            case 500:
                throw NetworkError.serverError(statusCode)
            default:
                throw NetworkError.networkFailError(statusCode)
            }
        }
        
        private func decodeData<T: Decodable>(from data: Data, to type: T.Type) throws -> T {
            guard let decodedData = try? JSONDecoder().decode(T.self, from: data) else {
                throw NetworkError.decodeError(toType: T.self)
            }
            return decodedData
        }   
    }
  • NetworkProvider

    import Foundation
    
    struct NetworkProvider<Target: TargetType> {
        func request(_ target: Target) throws -> URLRequest {
            
            // MARK: - Path
            let path = target.baseURLPath + target.path
            guard let urlComponents = URLComponents(string: path) else {
                throw NetworkError.invalidURLComponents(path)
            }
            
            // MARK: - URL
            var url: URL?
            let task = target.task
            switch task {
            case .requestPlain, .requestJSONEncodable:
                url = urlComponents.url
            }
            guard let url = url else {
                throw NetworkError.invalidURLString
            }
            
            // MARK: - Ruqest
            var request = URLRequest(url: url)
            switch task {
            case .requestPlain: break
            case .requestJSONEncodable(let body):
                let bodyEncoded = try JSONEncoder().encode(body)
                request.httpBody = bodyEncoded
            }
            
            // MARK: - Header
            request.httpMethod = target.method.rawValue
            if let headerField = target.headers {
                _ = headerField.map { (key, value) in
                    request.addValue(value, forHTTPHeaderField: key)
                }
            }
            return request
        }
    }
  • TargetType

    import Foundation
    
    protocol TargetType {
        
        var baseURLPath: String { get }
        var path: String { get }
        var method: HTTPMethod { get }
        var task: NetworkTask { get }
        var headers: [String: String]? { get }
        
    }
  • NetworkService

    import Foundation
    
    enum NetworkService {
        case getMovies(category: String)
        case createFavorite(id: String, name: String)
        case deleteFavorite(id: String)
    
    		// 중략
    }
    
    extension NetworkService: TargetType {
        
        var baseURLPath: String {
            return "https://api.example.com/"
        }
        
        var path: String {
            switch self {
            case .getMovies:
                return "/movies"
            case .createFavorite:
                return "/favorite"
            case .deleteFavorite:
                return "/favorite"
            }
        }
        
        var method: HTTPMethod {
            switch self {
            case .getMovies:
                return .get
            case .createFavorite:
                return .post
            case .deleteFavorite:
                return .delete
            }
        }
        
        var task: NetworkTask {
            switch self {
    				case .getMovies, .deleteFavorite:
                return .requestPlain
            case .createFavorite(let id, let name):
                let item = FavoriteRequestItem(id: id, name: name)
                return .requestJSONEncodable(body: item)
            }
        }
        
        var headers: [String: String]? {
            switch self {
            case .getMovies, .createFavorite:
                return nil
            case .deleteFavorite(let id):
                return ["id": id]
            }
        }
    }
  • NetworkService를 위한 요소들(HTTPMethod, NetworkTask …)

    import Foundation
    
    struct HTTPMethod: RawRepresentable, Equatable, Hashable {
        
        static let get = HTTPMethod(rawValue: "GET")
        static let post = HTTPMethod(rawValue: "POST")
        static let patch = HTTPMethod(rawValue: "PATCH")
        static let delete = HTTPMethod(rawValue: "DELETE")
        
        let rawValue: String
        
        init(rawValue: String) {
            self.rawValue = rawValue
        }
        
    }
    
    enum NetworkTask {
        
        case requestPlain
        case requestJSONEncodable(body: Encodable)
        
    }
    
    // 중략