[Swift] UrlSession사용하기
⛔️ 이번포스트는 URLSession - Apple Documnetation을 참고하여 정리한 글입니다.
틀린내용에 대한 피드백은 메일로 보내주시면 감사하겠습니다🙏🏻
1️⃣ URLSession을 이용한 네트워킹 구현순서
- URL 만들기
- URLSession 구성하기
- dataTask 만들기
- 네트워크 요청완료 핸들러 처리하기
1️⃣ URL 만들기
URL(string:)을 이용해서 String타입의 주소를 URL로 바로 만들어서 사용해도 되지만 String을 하드코딩해서 다루는 것은 단점이 많습니다. REST API의 url 설계구조를 감안하면 아래의 이미지의 구조로 나눠서 관리하는 것이 더 좋습니다. URLComponents를 이용하면 좀 더 명확하게 관리를 해줄 수 있습니다.
struct NetworkAPI {
static let schema = "https"
static let host = "api.unsplash.com"
static let path = "/photos"
func getRandomImageAPI(page: Int) -> URLComponents {
var components = URLComponents()
components.scheme = NetworkAPI.schema
components.host = NetworkAPI.host
components.path = NetworkAPI.path
components.queryItems = [
URLQueryItem(name: "client_id", value: "xxxxxxxxx"),
URLQueryItem(name: "count", value: "15"),
URLQueryItem(name: "page", value: String(page))
]
return components
}
}
URLComponents의 .url요소에 접근하여 조립된 URL값을 옵셔널형태로 얻을 수 있습니다.
let url = api.getRandomImageAPI(page: page).url
2️⃣ URLSession 구성하기
URLSession은 싱글톤(shared)으로 만들어 줄 수 있는데, 사용자 정의는 할 수 없기 때문에 간단한 기본 요청에 대한 처리로 사용하면 될 것 같습니다.
URLSeeion.shared
다른 방법으로 직접 URLSessionConfiguration을 설정해서 만들어줄 수 있습니다.
- .default: 싱글턴(share)과 비슷하게 작동하지만 delegate를 이용하여 데이터를 점진적으로 얻을 수 있습니다.
- .ephemeral: 캐시, 쿠키 또는 자격증명을 디스크에 저장하지않을때 사용하는 임시세션입니다.
- .background(withIdentifier:): 앱이 실행되지 않는 동안 백그라운드에서 콘테츠를 업로드 및 다운로드작업을 수행할 수 있습니다.
이번 포스트에서는 기본세션인 .default를 이용해서 세션을 만들어 줬습니다.
class URLSessionManager {
static let shared = URLSessionManager()
private let session = URLSession(configuration: .default)
private let api = NetworkAPI()
private init() { }
/* 코드 생략 */
}
3️⃣ Task 만들기
위에서 만들어준 URLSession을 이용해서 Task를 만들 수 있습니다.
- Data Task: NSData 객체를 이용해서 통신합니다. 짧은 대화형 요청을 위한 것입니다.(GET)
- Upload Task: Data task와 비슷하지만 파일형태의 데이터도 보내며, 백그라운드 통신도 지원합니다.(POST, PUT)
- Download Task: 데이터를 다운로드(파일형태도가능)를 하며, 백그라운드 통신도 지원합니다.
- WebSocket Task: RFC 6455에 정의된 WebSocket프로토콜을 사용하여 TCP 및 TLS를 통해 메시지를 교환
이번 포스트에서는 간단한 JSON타입으로 만들 data이므로 Data Task를 이용하여 만들어줄 예정입니다.
[url vs request]
Task는 URL or URLRequest로 만들어줄 수 있습니다.
URLRequest를 이용하면
사실
가끔, Authorization와 같이 인증키가 GET요청을 할때 필요할 수 있습니다. 이때는 GET요청일때도 URLRequest를 이용해서 dataTask를 만들어 세팅해주면 됩니다. POST요청의 경우 반드시 URLRequest로 Task를 만들어서 아래 코드와 같은 세팅이 필요합니다.
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
이번 포스트에서는 따로 헤더를 세팅할 필요가없는 GET요청이기 때문에 아래와 같이 URL을 그대로 사용해서 dataTask를 만들어 줬습니다.
func getImageInfo(page: Int, completion: @escaping (Result<[ImageInfo], CustomError>) -> ()) {
guard let url = api.getRandomImageAPI(page: page).url else {
completion(.failure(CustomError.makeURLError))
return
}
session.dataTask(with: url) { data, response, error in
...
}.resume()
}
4️⃣ 네트워크 요청완료 핸들러 처리하기
네트워킹이 끝나면 completion(핸들러, 비동기)형태로 응답값을 받을 수 있습니다. 읍답값은 다음과 같이 Data, URLResponse, Error의 옵셔널형태로 구성되어 있습니다. 다음일때
- error값이 존제할때
- response의 상태코드값이 200번대가 아닐때
- data값이 없을때
자세한 에러처리 방법은 에러핸들링하기 포스트 - kirkim를 참고해주세요. data값이 존제하는 것을 최종적으로 확인하게 되면, JSONDecoder()를 이용해서 원하는 타입으로 디코딩을 해주면 됩니다.
session.dataTask(with: url) { data, response, error in
guard error == nil else {
completion(.failure(.error(error: error)))
return
}
if let httpResponse = response as? HTTPURLResponse,
!(200...299).contains(httpResponse.statusCode) {
completion(.failure(.responseError(code:httpResponse.statusCode)))
return
}
guard let data = data else {
completion(.failure(.noData))
return
}
/* 코드 생략 */
}.resume()
[Decodable 데이터 타입만들기]
data(Data타입)이 nil값이 아닌 것 까지 확인하면 원하는 타입으로 디코딩(decoding)하는 과정이 필요합니다. Decodable을 준수하면 디코딩이 가능한 타입이 됩니다.
- Decodable: 디코딩가능한 타입
- Encodable: 인코딩가능한 타입
- Codable: Decodable + Encodable
아래의 코드는 이번에 사용한 데이터 타입니다. 필요한 요소만 선택해서 구성할 수 있으며 다계층구조로도 구현할 수 있습니다.
만약 원하는 변수명으로 사용하고 싶다면 CodingKey프로토콜 을 이용하면 됩니다.이떄, enum 타입명도 반드시 CodingKeys로 만들어 줘야 합니다.
struct ImageInfo: Decodable {
var updatedAt: String
var imageURL: ImageURL
var author: Author
var likes: Int
var width: Int
var height: Int
private enum CodingKeys: String, CodingKey {
case updatedAt = "updated_at"
case imageURL = "urls"
case author = "user"
case likes, width, height
}
}
이제 다음과 같이 JSONDecoder()를 이용해여 디코딩을 해주면 됩니다. 실패시
do {
let hasData = try JSONDecoder().decode([ImageInfo].self, from: data)
completion(.success(hasData))
} catch let error as NSError {
completion(.failure(.decodingError(error: error)))
}
5️⃣ NetworkManager 전체코드
import Foundation
final class NetworkManager {
static let shared = NetworkManager()
private init() { }
private let api = NetworkAPI()
private let session = URLSession(configuration: .default)
func getImageInfo(page: Int, completion: @escaping (Result<[ImageInfo], CustomError>) -> ()) {
guard let url = api.getRandomImageAPI(page: page).url else {
completion(.failure(CustomError.makeURLError))
return
}
session.dataTask(with: url) { data, response, error in
guard error == nil else {
completion(.failure(.error(error: error)))
return
}
if let httpResponse = response as? HTTPURLResponse,
!(200...299).contains(httpResponse.statusCode) {
completion(.failure(.responseError(code:httpResponse.statusCode)))
return
}
guard let data = data else {
completion(.failure(.noData))
return
}
do {
let hasData = try JSONDecoder().decode([ImageInfo].self, from: data)
completion(.success(hasData))
} catch let error as NSError {
completion(.failure(.decodingError(error: error)))
}
}.resume()
}
}