---
# System prepended metadata

title: 네트워크 로직

---

# 네트워크 로직


### 우선 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
  }
}

```