# 네트워크 로직 ### 우선 1차 ```swift import Combine import Foundation protocol EndPoint { var path: String { get } var method: HTTPMethodType { get } var queryItem: [URLQueryItem]? { get } } // MARK: - APIEndpoint enum APIEndpoint: EndPoint { case postTest, getText, getDatePop /// 이렇게하면 케이스가 늘어 날수있지만 그래도 가독성면에서는 확실하게 좋을꺼같습니다. var path: String { switch self { case .postTest: return "https://test1 포스트" case .getText: return "https://test2 겟" case .getDatePop: return "https://test3 겟" } } // 엔드포인트에 맞게 메서드 설정 겟 포스트 풋 var method: HTTPMethodType { switch self { case .postTest: return .post case .getText: return .get case .getDatePop: return .put } } // 있으면 추가하기 없으면 닐 var queryItem: [URLQueryItem]? { switch self { case .getDatePop: return nil case .getText: return nil case .postTest: return [URLQueryItem(name: "test", value: "test")] } } } // MARK: - HTTPMethodType enum HTTPMethodType: String { case get = "GET" case head = "HEAD" case post = "POST" case put = "PUT" case patch = "PATCH" case delete = "DELETE" } // MARK: - NetworkService protocol NetworkService { func fetchData( endpoint: APIEndpoint, header: [String: String]? ) -> AnyPublisher<Data, APIError> func postData( endpoint: APIEndpoint, data: Data?, header: [String: String]? ) -> AnyPublisher<Data, APIError> } // MARK: - NetworkManager final class NetworkManager: NetworkService { // TODO: Get func fetchData( endpoint: APIEndpoint, header: [String: String]?) -> AnyPublisher<Data, APIError> { guard let urlRequest = createURLRequest(endpoint: endpoint, header: header) else { return Fail(error: APIError.invalidURL).eraseToAnyPublisher() } return performNetworkRequest(urlRequest: urlRequest) } //TODO: escaping func fetchDataEscaping( endpoint: APIEndpoint, header: [String: String]?, completion: @escaping (Result<Data, APIError>) -> Void) { guard let url = URL(string: endpoint.path) else { completion(.failure(.invalidURL)) return } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = endpoint.method.rawValue header?.forEach { urlRequest.addValue($1, forHTTPHeaderField: $0) } let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in if let error = error { completion(.failure(.requestFail)) return } guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { completion(.failure(.invalidHTTPStatusCode)) return } guard let data = data else { completion(.failure(.invalidData)) return } completion(.success(data)) } task.resume() } //TODO: 공통 메서드를 써서 만든 이스케이핑 func fetachDataRequest( endpoint: APIEndpoint, header: [String: String]?, completion: @escaping (Result<Data, APIError>) -> Void) { guard let urlRequest = createURLRequest(endpoint: endpoint, header: header) else { completion(.failure(.invalidURL)) return } performNetworkRequestEscaping(urlRequest: urlRequest, completion: completion) } } //MARK: - Post extension NetworkManager { // TODO: Post func postData( endpoint: APIEndpoint, data: Data?, header: [String: String]? ) -> AnyPublisher<Data, APIError> { guard let urlRequest = createURLRequest(endpoint: endpoint, data: data, header: header) else { return Fail(error: APIError.invalidURL).eraseToAnyPublisher() } return performNetworkRequest(urlRequest: urlRequest) } //TODO: escaping func postDataEscaping( endpoint: APIEndpoint, data: Data?, httpMethod: HTTPMethodType, header: [String: String]?, completion: @escaping (Result<Data, APIError>) -> Void) { guard let url = URL(string: endpoint.path) else { completion(.failure(.invalidURL)) return } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = httpMethod.rawValue urlRequest.httpBody = data header?.forEach { urlRequest.addValue($1, forHTTPHeaderField: $0) } if httpMethod == .post || httpMethod == .patch || httpMethod == .put { urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") } let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in if let error = error { completion(.failure(.requestFail)) return } guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { completion(.failure(.invalidHTTPStatusCode)) return } guard let data = data else { completion(.failure(.invalidData)) return } completion(.success(data)) } task.resume() } // 이스케이핑 함축 버전 func postDataEscapings( endpoint: APIEndpoint, data: Data?, header: [String: String]?, completion: @escaping (Result<Data, APIError>) -> Void) { guard let urlRequest = createURLRequest(endpoint: endpoint, data: data, header: header) else { completion(.failure(.invalidURL)) return } performNetworkRequestEscaping(urlRequest: urlRequest, completion: completion) } } //MARK: 공통 메서드 extension NetworkManager { // 공통 URLComponents 로 URL 관리 private func createURLRequest(endpoint: APIEndpoint, header: [String: String]?) -> URLRequest? { var components = URLComponents(string: endpoint.path) components?.queryItems = endpoint.queryItem guard let url = components?.url else { return nil } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = endpoint.method.rawValue header?.forEach { urlRequest.addValue($1, forHTTPHeaderField: $0) } return urlRequest } // 컴바인 private func performNetworkRequest(urlRequest: URLRequest) -> AnyPublisher<Data, APIError> { return URLSession.shared.dataTaskPublisher(for: urlRequest) .tryMap { output in guard let httpResponse = output.response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { throw APIError.invalidHTTPStatusCode } return output.data } .mapError { error -> APIError in (error as? APIError) ?? .requestFail } .eraseToAnyPublisher() } // 이스케이핑 private func performNetworkRequestEscaping(urlRequest: URLRequest, completion: @escaping (Result<Data, APIError>) -> Void) { let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in if let error = error { completion(.failure(.requestFail)) return } guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { completion(.failure(.invalidHTTPStatusCode)) return } guard let data = data else { completion(.failure(.invalidData)) return } completion(.success(data)) } task.resume() } // Post 메서드 private func createURLRequest(endpoint: APIEndpoint, data: Data?, header: [String: String]?) -> URLRequest? { guard let url = URL(string: endpoint.path) else { return nil } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = endpoint.method.rawValue header?.forEach { urlRequest.addValue($1, forHTTPHeaderField: $0) } urlRequest.httpBody = data if endpoint.method == .post || endpoint.method == .patch || endpoint.method == .put { urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") } return urlRequest } } // MARK: - APIError enum APIError: LocalizedError { case invalidURL case requestFail case invalidData case dataTransferFail case decodingFail case invalidHTTPStatusCode case requestTimeOut var errorDescription: String? { switch self { case .invalidURL: return "유효하지 않은 URL입니다." case .requestFail: return "요청에 실패했습니다." case .decodingFail: return "디코딩 실패했습니다." case .invalidData: return "잘못된 데이터 입니다." case .dataTransferFail: return "데이터 변환에 실패했습니다." case .invalidHTTPStatusCode: return "잘못된 HTTPStatusCode입니다." case .requestTimeOut: return "요청시간이 초과되었습니다." } } } ``` ---- ### 추가적으로 공통메서드 2개를 1개로 만들경우 예 ```swift extension NetworkManager { // 공통 URLRequest 생성 메소드 private func createURLRequest( endpoint: APIEndpoint, data: Data? = nil, header: [String: String]? ) -> URLRequest? { var components = URLComponents(string: endpoint.path) components?.queryItems = endpoint.queryItem guard let url = components?.url else { return nil } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = endpoint.method.rawValue header?.forEach { urlRequest.addValue($1, forHTTPHeaderField: $0) } if let data = data, endpoint.method.requiresBody { urlRequest.httpBody = data urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") } return urlRequest } } extension HTTPMethodType { var requiresBody: Bool { switch self { case .post, .put, .patch: return true default: return false } } } ``` ---- ## 최종 ```swift import Combine import Domain import Foundation // MARK: - EndPoint protocol EndPoint { var path: String { get } var method: HTTPMethodType { get } var queryItem: [URLQueryItem]? { get } } // MARK: - APIEndpoint enum APIEndpoint: EndPoint { case postTest(id: String) case getText case getDatePop case deleteDate case putData /// 이렇게하면 케이스가 늘어 날수있지만 그래도 가독성면에서는 확실하게 좋을꺼같습니다. var path: String { // postData(endpoint: .postTest(id: "12345")) switch self { case let .postTest(id): return "https://test1 포스트\(id)" case .getText: return "https://test2 겟" case .getDatePop: return "https://test3 겟" case .deleteDate: return "delete" case .putData: return "put" } } /// 엔드포인트에 맞게 메서드 설정 겟 포스트 풋 var method: HTTPMethodType { switch self { case .postTest: return .post case .getText: return .get case .getDatePop: return .put case .deleteDate: return .delete case .putData: return .put } } /// 있으면 추가하기 없으면 닐 var queryItem: [URLQueryItem]? { switch self { case .getDatePop: return nil case .getText: return nil case .postTest: return [URLQueryItem(name: "test", value: "test")] case .deleteDate: return nil case .putData: return nil } } } // MARK: - HTTPMethodType enum HTTPMethodType: String { case get = "GET" case post = "POST" case put = "PUT" case patch = "PATCH" case delete = "DELETE" } // MARK: - NetworkService protocol NetworkService { func fetchDataPublisher( endpoint: APIEndpoint, header: [String: String]? ) -> AnyPublisher<Data, APIError> func fetchDataAsync( endpoint: APIEndpoint, header: [String: String]?, completion: @escaping (Result<Data, APIError> ) -> Void ) func postDataPublisher( endpoint: APIEndpoint, data: Data?, header: [String: String]? ) -> AnyPublisher<Data, APIError> func postDataAsync( endpoint: APIEndpoint, data: Data?, header: [String: String]?, completion: @escaping (Result<Data, APIError> ) -> Void ) func deleteResourceAsync( endpoint: APIEndpoint, data: Data?, header: [String: String]?, completion: @escaping (Result<Data, APIError> ) -> Void ) func deleteResourcePublisher( endpoint: APIEndpoint, data: Data?, header: [String: String]? ) -> AnyPublisher<Data, APIError> func putDataAsync( endpoint: APIEndpoint, data: Data?, header: [String: String]?, completion: @escaping (Result<Data, APIError>) -> Void ) func putDataPublisher( endpoint: APIEndpoint, data: Data?, header: [String: String]? ) -> AnyPublisher<Data, APIError> } // MARK: - NetworkManager // TODO: Get final class NetworkManager: NetworkService { func fetchDataPublisher( endpoint: APIEndpoint, header: [String: String]? ) -> AnyPublisher<Data, APIError> { guard let urlRequest = createURLRequest(endpoint: endpoint, header: header) else { return Fail(error: APIError.invalidURL).eraseToAnyPublisher() } return performNetworkRequest(urlRequest: urlRequest) } func fetchDataAsync( endpoint: APIEndpoint, header: [String: String]?, completion: @escaping (Result<Data, APIError> ) -> Void ) { guard let urlRequest = createURLRequest(endpoint: endpoint, header: header) else { completion(.failure(.invalidURL)) return } performNetworkRequestEscaping(urlRequest: urlRequest, completion: completion) } } // MARK: - Post extension NetworkManager { func postDataPublisher( endpoint: APIEndpoint, data: Data?, header: [String: String]? ) -> AnyPublisher<Data, APIError> { guard let urlRequest = createURLRequestPost(endpoint: endpoint, data: data, header: header) else { return Fail(error: APIError.invalidURL).eraseToAnyPublisher() } return performNetworkRequest(urlRequest: urlRequest) } func postDataAsync( endpoint: APIEndpoint, data: Data?, header: [String: String]?, completion: @escaping (Result<Data, APIError> ) -> Void ) { guard let urlRequest = createURLRequestPost(endpoint: endpoint, data: data, header: header) else { completion(.failure(.invalidURL)) return } performNetworkRequestEscaping(urlRequest: urlRequest, completion: completion) } } // MARK: - Delete extension NetworkManager { func deleteResourceAsync( endpoint: APIEndpoint, data: Data?, header: [String: String]?, completion: @escaping (Result<Data, APIError>) -> Void ) { guard let request = createURLRequestPost( endpoint: endpoint, data: nil, header: header ) else { completion(.failure(.invalidURL)) return } performNetworkRequestEscaping(urlRequest: request) { result in switch result { case let .success(data): completion(.success(data)) case let .failure(error): completion(.failure(error)) } } } func deleteResourcePublisher( endpoint: APIEndpoint, data: Data?, header: [String: String]? ) -> AnyPublisher<Data, APIError> { guard let request = createURLRequestPost( endpoint: endpoint, data: nil, header: header ) else { return Fail(error: APIError.invalidURL).eraseToAnyPublisher() } return performNetworkRequest(urlRequest: request) } } // MARK: - Put extension NetworkManager { func putDataAsync( endpoint: APIEndpoint, data: Data?, header: [String: String]?, completion: @escaping (Result<Data, APIError>) -> Void ) { guard let request = createURLRequestPost( endpoint: endpoint, data: data, header: header ) else { completion(.failure(.invalidURL)) return } performNetworkRequestEscaping(urlRequest: request) { result in switch result { case let .success(data): completion(.success(data)) case let .failure(error): completion(.failure(error)) } } } func putDataPublisher( endpoint: APIEndpoint, data: Data?, header: [String: String]? ) -> AnyPublisher<Data, APIError> { guard let request = createURLRequestPost( endpoint: endpoint, data: nil, header: header ) else { return Fail(error: APIError.invalidURL).eraseToAnyPublisher() } return performNetworkRequest(urlRequest: request) } } // MARK: 공통 메서드 extension NetworkManager { /// 공통 URLComponents 로 URL 관리 private func createURLRequest( endpoint: APIEndpoint, header: [String: String]? ) -> URLRequest? { var components = URLComponents(string: endpoint.path) components?.queryItems = endpoint.queryItem guard let url = components?.url else { return nil } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = endpoint.method.rawValue header?.forEach { urlRequest.addValue($1, forHTTPHeaderField: $0) } return urlRequest } /// 컴바인 private func performNetworkRequest(urlRequest: URLRequest) -> AnyPublisher<Data, APIError> { return URLSession.shared.dataTaskPublisher(for: urlRequest) .tryMap { output in guard let httpResponse = output.response as? HTTPURLResponse, 200 ... 299 ~= httpResponse.statusCode else { throw APIError.invalidHTTPStatusCode } return output.data } .mapError { error -> APIError in (error as? APIError) ?? .requestFail } .eraseToAnyPublisher() } /// 이스케이핑 private func performNetworkRequestEscaping( urlRequest: URLRequest, completion: @escaping (Result<Data, APIError> ) -> Void ) { let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in if let error { completion(.failure(.requestFail)) return } guard let httpResponse = response as? HTTPURLResponse, 200 ... 299 ~= httpResponse.statusCode else { completion(.failure(.invalidHTTPStatusCode)) return } guard let data else { completion(.failure(.invalidData)) return } completion(.success(data)) } task.resume() } /// Post 메서드 private func createURLRequestPost( endpoint: APIEndpoint, data: Data?, header: [String: String]? ) -> URLRequest? { guard let url = URL(string: endpoint.path) else { return nil } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = endpoint.method.rawValue header?.forEach { urlRequest.addValue($1, forHTTPHeaderField: $0) } urlRequest.httpBody = data if endpoint.method == .post || endpoint.method == .patch || endpoint.method == .put { urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") } return urlRequest } } ```