# 네트워크 로직
### 우선 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
}
}
```