들어가며
[정리] URLSession 뜯어보기 - Shared Session 사용해서 데이터 보내고 받아보기 에 이어서 진행하는 포스팅입니다. 기본적인 개념은 이전 포스팅에 정리되어있어서 보고 오는 걸 추천드립니다.
이전 포스팅에 이어서 이번 포스팅에서는 URLSessionConfiguration, Delegate를 사용해서 요청 메시지를 보내고 응답 메시지를 받는 예시를 만들어 보겠습니다.
예시를 만든 후에 지금까지 작성했던 코드들을 공통화해볼게요.
URLSessionConfiguration 사용해서 데이터 가져오기
이전에는 Shared Session를 사용해서 uploadTask 메소드에 데이터를 보냈습니다. 이번에는 default Session를 사용해서 직접 URLSessionConfiguration를 만들어보려고 합니다. 그리고 Completion Handler가 아닌 Delegate 메서드를 사용해서 응답 메시지 정보를 받으려고 합니다.
Delegate 메서드를 사용하게 되면 Task 활동의 Access level이 높아지게 됩니다. Protocol에 정의된 방식 외에 이벤트를 처리할 수 있기 때문이죠.
이전 포스팅에서 하던 Collection 만드는 작업을 Configuration를 사용해서 만들어보려고 하다가, Unsplash에서 제공해주는 Collection 목록 가져오기 API 를 사용해서 이전과는 다른 예시를 만들어 보려고 합니다.
먼저, UI를 구성해봅시다.
가져온 Collection 목록은 UITableView 위에 보여줄 겁니다. 그렇기 때문에 이전에 만들어 둔 화면 위에 TableView를 잘 올리고 이리저리 설정해줬습니다. 이번 포스팅에서도 네트워크 코드를 작성하는 것이 더 중요하기 때문에 UI 관련 코드는 자세하게 설명하지 않을게요.
이제 Collection를 가져오는 코드를 짜봅시다.
여러 Collection를 가져오는 GET /collections 를 사용해서 Collection를 가져와봅시다. 먼저, URLSession 인스턴스를 생성해야겠죠. 이전과는 다르게 URLSessionConfiguration를 사용해서 생성해줄겁니다.
let session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.waitsForConnectivity = true
return URLSession(configuration: configuration,
delegate: self, delegateQueue: nil)
}()
Swift
복사
Shared Session과 비슷한 기능을 하는 default Session 타입을 사용해서 URLSession를 만들었습니다. Configuration에 있는 waitsForConnectivity 속성을 사용해서 필요한 연결을 사용할 수 없는 경우에 즉시 실패하지 않고 적절한 연결을 기다리도록 설정할게요.
그리고 dataTask 메서드에 url를 넣어서 Task를 실행시켜보도록 할게요.
*이번에도 clientId를 Authorization 헤더 필드에 넣어주어야 합니다.
let url = URL(string: "https://api.unsplash.com/collections")!
var urlRequest = URLRequest(url: url)
urlRequest.allHTTPHeaderFields = ["Authorization": "\(clientId)"]
let task = session.dataTask(with: urlRequest)
task.resume()
Swift
복사
이제 URLSessionDataDelegate 메서드들을 사용해서 데이터들을 받아볼게요.
urlSession(_:dataTask:didReceive:completionHandler:)
urlSession(_:dataTask:didReceive:)
urlSession(_:task:didCompleteWithError:)
네트워크 코드를 다 구성했으니 화면을 빌드해보겠습니다.
짜잔, Collection 목록 Title이 잘 들어옵니다.
코드 공통화
지금까지 URLSession, URLSessionConfiguration, Delegate를 사용해서 네트워크 코드를 구성하는 예시를 봤습니다.
예시를 만들다보니 공통으로 사용할 수 있는 속성들이 여러 군데에서 중복으로 사용이 되더라구요.
그래서, 통신 부분 코드를 공통화 해보겠습니다.
1.
먼저, API를 정리하겠습니다.
공통으로 사용하는 https://api.unsplash.com URL를 APIEnvironment 라는 enum 타입 안에 넣어주겠습니다.
enum APIEnvironment {
static let url = "https://api.unsplash.com"
}
Swift
복사
2.
EndPoint enum를 만들어서 내부에 path, httpMethod, httpBody, header 등을 정리할 것이기 때문에 여러 곳에서 동일한 포맷을 사용할 수 있도록 Protocol를 하나 만들어보겠습니다.
protocol EndPointable {
var requestTimeOut: Float { get }
var httpMethod: HTTPMethod { get }
var data: HTTPTask { get }
var path: String { get }
var header: [String: String]? { get }
}
Swift
복사
httpMethod는 HTTPMethod 라는 enum 타입을 만들어서 String 타입으로 일일이 적지 않아도 HTTPMethod 를 선택해서 쓸 수 있도록 했습니다.
enum HTTPMethod: String {
case GET
case POST
case PUT
case PATCH
case DELETE
}
Swift
복사
data에는 httpBody, QueryParameter 또는 아무것도 들어가지 않는 경우로 HTTPTask enum를 구성했습니다. requestPlain 를 사용하면 GET 에서 아무 값도 넣지 않는 것처럼 사용되고, requestBody 를 사용하면 POST, PUT, PATCH, DELETE 에서 요청 바디 본문을 넣을 수 있도록 했습니다. requestQueryParameter 를 사용하면 url에 쿼리를 추가할 수 있습니다.
enum HTTPTask {
case requestPlain
case requestBody(bodyData: Data)
case requestQueryParameter(parameter: [String: String])
}
Swift
복사
각 case에 대한 구현은 추후에 자세하게 하겠습니다.
3.
EndPointable 를 conform하는 enum 타입의 EndPoint를 만들어봅시다.
Photo 데이터를 가져와서 화면에 띄워주는 fetchImages 케이스를 가진 PhotoEndPoint 를 만들었습니다. 해당 케이스는 이전 포스팅([정리] URLSession 뜯어보기 - Shared Session 사용해서 데이터 보내고 받아보기)에서 보면 쿼리 파라미터를 받습니다. 따라서 해당 케이스를 사용할 때 쿼리 파라미터를 받도록 해줬습니다. 인증 관련된 헤더 필드도 사용하기 때문에 Authorization 필드를 넣어줬어요.
enum PhotoEndPoint {
case fetchImages(query: [String: String])
}
extension PhotoEndPoint: EndPointable {
var requestTimeOut: Float {
return 20
}
var httpMethod: HTTPMethod {
switch self {
case .fetchImages:
return .GET
}
}
var data: HTTPTask {
switch self {
case .fetchImages(let query):
return .requestQueryParameter(parameter: query)
}
}
var path: String {
switch self {
case .fetchImages:
return "/photos"
}
}
var header: [String : String]? {
switch self {
case .fetchImages:
return ["Authorization": "\(KeyProvider.appKey(of: .clientId))"]
}
}
}
Swift
복사
Collection 관련된 부분도 동일한 방식으로 CollectionEndPoint 를 만들어 줬습니다. 해당 코드를 보고 싶으시다면 하단 코드 공통화 링크를 확인해주시면 됩니다!
4.
Request를 보내봅시다.
만들어질 Session에 대한 기본적인 정보들은 정리가 완료된 거 같습니다. 위의 정보들을 토대로 request를 보내면 될 것 같네요. Task 메소드에 넣어줄 URLRequest를 만들어 봅시다.
기본적인 URLRequest 의 틀은 NetworkRequest 라는 구조체로 만들어 줄게요.
struct NetworkRequest {
var url: URL
let header: [String: String]?
var body: Data?
let requestTimeOut: Double
let httpMethod: String
init(requestTimeout: Double,
httpMethod: String,
data: HTTPTask,
url: String,
header: [String: String]? = nil
) {
self.requestTimeOut = requestTimeout
self.httpMethod = httpMethod
self.url = URL(string: url)!
self.header = header
self.handleHTTPTask(data)
}
}
Swift
복사
NetworkRequest 에서는 url, header, body, requestTimeOut, httpMethod가 들어있습니다. 그 중 body 데이터는 data로 들어온 HTTPTask 를 handleHTTPTask 에서 가공하는 과정에서 request body가 있으면 데이터가 들어갑니다. 그게 아니라면 아무런 데이터가 들어오지 않거나, URL의 쿼리 파라미터로 들어갑니다.
extension NetworkRequest {
private mutating func handleHTTPTask(_ task: HTTPTask) {
switch task {
case .requestPlain:
break
case .requestBody(let bodyData):
self.body = bodyData
case .requestQueryParameter(let parameter):
let queryItem = self.buildQueryItem(parameter)
self.url.append(queryItems: queryItem)
}
}
private func buildQueryItem(_ parameter: [String: String]) -> [URLQueryItem] {
return parameter.compactMap { URLQueryItem(name: $0, value: $1) }
}
}
Swift
복사
NetworkRequest 에는 buildURLRequest, buildURLSessionConfiguration 메서드가 있습니다 .해당 메서드들은 URLRequest와 URLSessionConfiguration를 Task 생성 시에 만들어 줍니다.
func buildURLRequest() -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.allHTTPHeaderFields = header
urlRequest.httpMethod = httpMethod
urlRequest.httpBody = body
return urlRequest
}
func buildURLSessionConfiguration() -> URLSessionConfiguration {
var urlSessionConfiguration = URLSessionConfiguration.default
urlSessionConfiguration.timeoutIntervalForRequest = TimeInterval(floatLiteral: self.requestTimeOut)
return urlSessionConfiguration
}
Swift
복사
5.
Task를 만들어 봅시다.
Request까지 만들었으니 이제 Task에 Request와 Data를 보내봅시다. APIRequest 라는 구조체를 만들어서 Task를 만들고 Request를 보내고 응답을 받아서 에러를 처리하는 부분까지 작성해보겠습니다.
struct APIRequest {
typealias Response = (data: Data, response: URLResponse)
func request<T: Decodable>(_ request: NetworkRequest) async throws -> (statusCode: Int, data: T?) {
let responseData = try await self.requestDataToURL(request)
guard let httpResponse = responseData.response as? HTTPURLResponse else {
throw NetworkError.serverError
}
switch httpResponse.statusCode {
case (200..<300):
do {
let decoder = JSONDecoder()
let baseModelData: T? = try decoder.decode(T.self, from: responseData.data)
return (httpResponse.statusCode, baseModelData)
} catch {
throw NetworkError.decodingError
}
case (300..<500):
throw NetworkError.clientError(message: httpResponse.statusCode.description)
default:
throw NetworkError.serverError
}
}
private func requestDataToURL(_ request: NetworkRequest) async throws -> Response {
let urlRequest = request.buildURLRequest()
let sessionConfiguration = request.buildURLSessionConfiguration()
let session = URLSession(configuration: sessionConfiguration)
return try await session.data(for: urlRequest)
}
}
Swift
복사
•
request 메서드는 NetworkRequest 구조체를 받습니다. 해당 구조체를 받아서 requestDataToURL 이라는 메서드에 넣습니다. 해당 메서드에서는 URLRequest, URLSessionConfiguration를 만들어서 URLSession과 Session Task를 생성합니다. Task를 통해서 들어온 data, response 는 다시 request 메서드로 넘어갑니다.
•
response 데이터를 받은 request 메소드는 200대 상태 코드가 왔을 때 데이터를 가공해서 넘겨주고 다른 경우에는 NetworkError를 throw하도록 합니다.
enum NetworkError: Error {
case decodingError
case clientError(message: String?)
case serverError
case unknownError
}
Swift
복사
6.
API를 호출할 수 있는 PhotoAPI 를 만듭니다.
이제 만들어진 API를 호출할 수 있는 부분을 만들어줍니다. PhotoAPI에서는 원하는 상황별로 API를 호출할 수 있는 메서드들이 들어있습니다.
먼저, PhotoProtocol를 만들어줍니다.
protocol PhotoProtocol {
func fetchImages(perPage: Int, orderBy: String) async throws -> (statusCode: Int, data: [Image]?)
}
Swift
복사
그리고 PhotoProtocol 를 conform하는 PhotoAPI 를 만듭니다.
struct PhotoAPI: PhotoProtocol {
func fetchImages(perPage: Int, orderBy: String) async throws -> (statusCode: Int, data: [Image]?) {
let queryParameter = [
"per_page": perPage.description,
"order_by": orderBy
]
let request = PhotoEndPoint
.fetchImages(query: queryParameter)
.buildRequest()
return try await APIRequest().request(request)
}
}
Swift
복사
APIRequest 에 보내기 위한 NetworkRequest 구조체를 만들 buildRequest 라는 메서드를 EndPointable 내부에 만들어 줬습니다.
extension EndPointable {
func buildRequest() -> NetworkRequest {
let url = APIEnvironment.url + self.path
return NetworkRequest(requestTimeout: self.requestTimeOut,
httpMethod: self.httpMethod.rawValue,
data: self.data,
url: url,
header: self.header)
}
}
Swift
복사
7.
이전처럼 fetchImages가 잘 작동하는지 확인해보겠습니다.
fetchImages 코드를 수정하고서 빌드를 돌려보겠습니다.
private func fetchImages() {
Task {
do {
let response = try await PhotoAPI().fetchImages(perPage: 3, orderBy: "popular")
if let data = response.data {
DispatchQueue.main.async {
self.imageURLs = data.compactMap { $0.urls?.regular }
}
} else {
self.handleError("데이터가 들어오지 않았습니다.")
}
} catch NetworkError.decodingError {
self.handleError("데이터 디코딩에 실패했습니다.")
} catch NetworkError.clientError(let message) {
self.handleError(message ?? "")
} catch NetworkError.serverError {
self.handleError("서버에 문제가 발생하였습니다.")
}
}
}
Swift
복사
결과물!
카테고리 생성은 Response 디코딩 에러가 나긴 했지만, 생성이 되긴 했답니다. \( ˆoˆ )/
마치며
아직 더 수정하고픈 부분들이 있지만 네트워크 코드 관련된건 아니라서 혼자 천천히 수정을 해보도록 하겠습니다. 해당 포스트를 읽는 분들께 도움이 되면 좋겠네요.
Alamofire, Moya 처럼 간편하게 URLSession를 쓰고 싶어서 공통화를 진행한건데, 아직 미흡한 부분이 많습니다. multipart 같은 것도 지원할 수 있도록 수정하고 싶고, Moya 처럼 Network Logger를 넣으면 콘솔창에 Log가 남도록 해보고 싶기도 하네요. 천천히 진행해보려고 합니다.
해당 포스트에서 잘못된 부분이나 질문이 있다면 하단 채팅으로 말씀해주세요.