-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
async/await 와 URLSession 사용하기(2) - abstraction layer 구축해보기 #6
Labels
documentation
Improvements or additions to documentation
Comments
구현 부분
HTTP Method
import Foundation
public struct HTTPMethod: RawRepresentable, Equatable, Hashable {
/// `CONNECT` method.
public static let connect = HTTPMethod(rawValue: "CONNECT")
/// `DELETE` method.
public static let delete = HTTPMethod(rawValue: "DELETE")
/// `GET` method.
public static let get = HTTPMethod(rawValue: "GET")
/// `HEAD` method.
public static let head = HTTPMethod(rawValue: "HEAD")
/// `OPTIONS` method.
public static let options = HTTPMethod(rawValue: "OPTIONS")
/// `PATCH` method.
public static let patch = HTTPMethod(rawValue: "PATCH")
/// `POST` method.
public static let post = HTTPMethod(rawValue: "POST")
/// `PUT` method.
public static let put = HTTPMethod(rawValue: "PUT")
/// `QUERY` method.
public static let query = HTTPMethod(rawValue: "QUERY")
/// `TRACE` method.
public static let trace = HTTPMethod(rawValue: "TRACE")
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
} TargetType
import Foundation
public protocol TargetType {
/// The target's base `URL`.
var baseURLPath: String { get }
/// The path to be appended to `baseURL` to form the full `URL`.
var path: String { get }
/// The HTTP method used in the request.
var method: HTTPMethod { get }
/// ✅ 실제로는 이번 프로젝트에서 사용되지 않으나 extension 을 활용하여 protocol method 의 기본값을 설정할 수 있음을 확인.
/// Provides stub data for use in testing. Default is `Data()`.
var sampleData: Data { get }
/// The type of HTTP task to be performed.
/// ✅ NetworkTask: 서버통신의 작업을 위해서 종류에 따라서 일관된 작업을 하기 위한 목적으로 만듦.
var task: NetworkTask { get }
/// The headers to be used in the request.
var headers: [String: String]? { get }
}
public extension TargetType {
/// Provides stub data for use in testing. Default is `Data()`.
var sampleData: Data { Data() }
} NetworkTask
import Foundation
public enum NetworkTask {
/// 추가적인 데이터를 더하지 않는다.
case requestPlain
/// 파라미터를 인코딩해서 request 에 더합니다.
case requestParameters(parameters: [String : Any], encoding: ParameterEncoding)
} ParameterEncoding
import Foundation
public enum ParameterEncoding {
/// ✅ request query 방법으로 URL 에 파라미터를 추가합니다.
case queryString
} NetworkProvider
import Foundation
struct NetworkProvider<Target: TargetType> {
func request(_ target: Target) throws -> URLRequest {
// url path
let path = target.baseURLPath + target.path
guard var urlComponents = URLComponents(string: path) else {
throw DataDownloadError.invalidURLComponents
}
// ✅ task
var url: URL?
let task = target.task
switch task {
case .reqiestPlan:
url = urlComponents.url
case .requestParameters(let parameters, let encoding):
switch encoding {
case .queryString:
// parameter query
let queryItemArray = parameters.map {
URLQueryItem(name: $0.key, value: $0.value as? String)
}
urlComponents.queryItems = queryItemArray
url = urlComponents.url
}
}
guard let url = url else {
throw DataDownloadError.invalidURLString
}
// ✅ method
var request = URLRequest(url: url)
request.httpMethod = target.method.rawValue
// ✅ header
if let headerField = target.headers {
_ = headerField.map { (key, value) in
request.addValue(value, forHTTPHeaderField: key)
}
}
return request
}
} ImageFetchProvider
import UIKit
struct ImageFetchProvider {
static let shared = ImageFetchProvider()
private init() { }
/// ✅ URL 을 가지고 data 를 다운받아서 UIImage 로 변환하는 메서드.
/// - Parameter urlString: URL 가 될 String 자료형의 값.
/// - Returns: 다운 받은 data 를 UIImage 로 변환해서 리턴. 변환되지 않는 경우 에러를 던집니다.
public func fetchImage(with urlString: String) async throws -> UIImage {
guard let url = URL(string: Const.Path.imageURLPath + urlString) else {
throw ImageDownloadError.invalidURLString
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw ImageDownloadError.invalidServerResponse
}
guard let image = UIImage(data: data) else {
throw ImageDownloadError.unsupportImage
}
return image
}
} 적용NetworkService
import Foundation
enum NetworkService {
/// ✅ 인기있는 영화 목록을 가져오는 서버통신.
/// - Parameter page : pagination 을 지원하는 파라미터. Default is nil.
case popular(page: Int? = nil)
/// ✅ 이미지를 가져오는 서버통신.
/// - Parameter option : request parameter 로 이미지 사이즈 옵션을 전달하는 파라미터. Default is original.
/// - Parameter url : option 뒤에 붙을 URL.
case fetchImage(option: ImageSizeOptions = .original, url: String)
}
/// ✅ 해당 open API 에서 지원하는 이미지 사이즈 옵션.
enum ImageSizeOptions: String {
case original = "original"
case w500 = "w500"
}
extension NetworkService: TargetType {
var baseURLPath: String {
switch self {
case .popular(_):
return Const.Path.baseURLPath
case .fetchImage(_, _):
return Const.Path.imageURLPath
}
}
var path: String {
switch self {
case .popular(let page):
if let page = page {
return "/movie/popular/\(page)"
} else {
return "/movie/popular"
}
case .fetchImage(let option, let url):
return "/\(option.rawValue)/\(url)"
}
}
var method: HTTPMethod {
switch self {
case .popular(_):
return .get
case .fetchImage(_, _):
return .get
}
}
var task: NetworkTask {
switch self {
case .popular(let page):
let parameters: [String : Any]
if let page = page {
parameters = ["api_key" : Const.Key.apiKey,
"page" : page]
} else {
parameters = ["api_key" : Const.Key.apiKey]
}
return .requestParameters(parameters: parameters, encoding: .queryString)
case .fetchImage(_, _):
return .reqiestPlan
}
}
var headers: [String : String]? {
switch self {
case .popular(_):
return nil
case .fetchImage(_, _):
return nil
}
}
} NetworkAPI
import Foundation
public struct NetworkAPI {
static let shared = NetworkAPI()
private let provider = NetworkProvider<NetworkService>()
private init() { }
/// ✅ 인기있는 영화 목록을 가져오는 서버통신 메서드.
/// - Parameter page : pagination 할 수 있는 매개변수. default value is nil.
func fetchPopularMovies(page: Int? = nil) async throws -> PopularMovie {
let request = try provider.request(.popular(page: page))
// MARK: - 통신
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw DataDownloadError.invalidServerResponse
}
let networkResult = try self.judgeStatus(by: httpResponse.statusCode, data, type: PopularMovie.self)
return networkResult
}
/// ✅ 상태코드를 가지고 에러 핸들링하는 메서드.
/// - Parameter statusCode : 상태 코드.
/// - Parameter data : 디코딩 할 JSON 객체.
/// - Parameter type : JSON 객체로 부터 디코딩 당할 값의 자료형.
private func judgeStatus<T: Codable>(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)
}
}
/// ✅ 디코딩하고, 에러를 핸들링하는 메서드.
/// - Parameter data : 디코딩 할 JSON 객체.
/// - Parameter type : JSON 객체로 부터 디코딩 당할 값의 자료형.
private func decodeData<T: Codable>(from data: Data, to type: T.Type) throws -> T {
guard let decodedData = try? JSONDecoder().decode(T.self, from: data) else {
throw NetworkError.decodError(toType: T.self)
}
return decodedData
}
} Error HandlingNetworkErrorimport Foundation
/// ✅ 서버통신 시 발생하는 에러.
enum NetworkError: Error {
/// 디코딩 에러.
/// - Parameter toType : Deciadable 을 채택하는 디코딩 가능한 자료형. existential metatype 이다.
case decodeError(toType: Decodable.Type)
/// 서버 요청 에러.
case requestError(_ statusCode: Int)
/// 서버 내부 에러.
case serverError(_ statusCode: Int)
/// 네트워크 연결 실패 에러.
case networkFailError(_ statusCode: Int)
} 사용하기override func viewDidLoad() {
super.viewDidLoad()
/// ...
Task {
do {
movies = try await getMovie()
movieCollectionView.reloadData()
} catch DataDownloadError.invalidURLString {
print("movie error - invalidURLString")
} catch DataDownloadError.invalidServerResponse {
print("movie error - invalidServerResponse")
} catch NetworkError.decodeError(let type) {
print("network error - decodeError : \(type)")
} catch NetworkError.requestError(let statusCode) {
print("network error - requestError : \(statusCode)")
} catch NetworkError.serverError(let statusCode) {
print("network error - serverError : \(statusCode)")
} catch NetworkError.networkFailError(let statusCode) {
print("network error - networkFailError : \(statusCode)")
}
}
}
private func getMovie() async throws -> [Result] {
let popularMovie = try await NetworkAPI.shared.fetchPopularMovies()
return popularMovie.results
} |
hyun99999
changed the title
iOS) async/await 와 URLSession 사용하기(2) - abstraction layer 구축해보기
async/await 와 URLSession 사용하기(2) - abstraction layer 구축해보기
Jun 3, 2022
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The text was updated successfully, but these errors were encountered: