안녕하세요. 오늘은 URLSession을 통한 네트워크 통신을 Result Type을 사용해서 개선해보려고 합니다.
예제로 사용해 볼 오픈 API는 영화진흥위원회의 일별 박스오피스입니다.
영화진흥위원회 오픈API
제공서비스 영화관입장권통합전산망이 제공하는 오픈API서비스 모음입니다. 사용 가능한 서비스를 확인하고 서비스별 인터페이스 정보를 조회합니다.
www.kobis.or.kr
1. 모델 생성하기
struct BoxOfficeResult: Decodable {
let boxOfficeResult: DailyBoxOffice
}
struct DailyBoxOffice: Decodable {
let dailyBoxOfficeList: [Movie]
}
struct Movie: Decodable {
let rank: String
let movieNm: String
let audiCnt: String
}
2. 네트워크 매니저 만들기
싱글톤 패턴으로 구성해 주었습니다.
final class NetworkManager {
static let shared = NetworkManager()
private init() {}
}
3. URLSession으로 통신하기
func getURLRequest(_ targetDt: String) -> URLRequest? {
// URLComponents
guard var urlComponents = URLComponents(string: "https://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json") else { return nil }
// Query Parameters
let queryItems: [URLQueryItem] = [
URLQueryItem(name: "key", value: APIKey.movieKey),
URLQueryItem(name: "targetDt", value: targetDt)
]
urlComponents.queryItems = queryItems
// URL
guard let url = urlComponents.url else { return nil }
// Request
let request = URLRequest(url: url)
return request
}
func callRequest(
_ targetDt: String,
completion: @escaping ([Movie]?, Error?) -> Void
) {
// Request
guard let request = getURLRequest(targetDt) else { return }
// Session
let session = URLSession.shared.dataTask(with: request) { data, response, error in
// 실패 - 에러 존재 (data == nil)
if let error = error {
print("에러 존재")
completion(nil, NSError(domain: "No Data", code: 0, userInfo: nil))
return
}
// 실패 - 데이터 X
guard let data = data else {
print("데이터 nil")
completion(nil, error)
return
}
do {
let result = try JSONDecoder().decode(BoxOfficeResult.self, from: data)
// 성공
print("성공")
completion(result.boxOfficeResult.dailyBoxOfficeList, nil)
} catch {
// 실패 - 디코딩 실패
print("디코딩 실패")
completion(nil, error)
}
}
// resume
session.resume()
}
주목해서 봐야 할 부분은 이 부분입니다.
// completion: @escaping ([Movie]?, Error?) -> Void
let session = URLSession.shared.dataTask(with: request) { data, response, error in
// 실패 - 에러 존재 (data == nil)
if let error = error {
print("에러 존재")
completion(nil, error)
return
}
// 실패 - 데이터 X
guard let data = data else {
print("데이터 nil")
completion(nil, error)
return
}
do {
let result = try JSONDecoder().decode(BoxOfficeResult.self, from: data)
// 성공
print("성공")
completion(result.boxOfficeResult.dailyBoxOfficeList, nil)
} catch {
// 실패 - 디코딩 실패
print("디코딩 실패")
completion(nil, error)
}
}
completion이 ([Movie]?, Error?) -> Void의 타입으로 구성되어 있고,
호출할 땐 completion(data, nil) 혹은 completion(nil, error)의 형태로 호출하게 됩니다.
왜 completion(nil, nil)의 형태나 completion(data, error)의 형태로는 호출하지 않을까요?
그 이유는 dataTask가 다음과 같이 구현되어 있기 때문입니다.
open func dataTask(
with request: URLRequest,
completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void
) -> URLSessionDataTask
completionHandler를 통해 (data, response, error)를 받게 되는데,
통신에 성공하면 data의 값이 존재하고, error는 nil,
실패하면 error가 존재하고, data가 nil이 됩니다.
둘 다 값이 있거나, 둘 다 nil인 경우는 없습니다.
표로 살펴보면 다음과 같습니다.
| data | error | |
| 성공 | O | X |
| 실패 | X | O |
| 논리적으로 존재하지 않음 | O | O |
| 논리적으로 존재하지 않음 | X | X |
그래서 저희가 만든 completion을 사용하면 항상 completion(data, nil) 혹은 completion(nil, error)의 형태로 호출하게 되는 것이죠.
우리는 completion(data, error)나 completion(nil, nil)의 경우는 없을 것이란 걸 알고 있지만, 문법적으로는 얼마든지 가능한 상태입니다. 따라서 XCode는 이것을 에러로 감지하지 못하고, 휴먼 에러의 가능성을 높이게 됩니다.
이것을 해결하기 위해서 Result Type을 사용해 보려고 합니다.
Result | Apple Developer Documentation
A value that represents either a success or a failure, including an associated value in each case.
developer.apple.com
Result는 success와 failure의 case로 구성된 열거형이라고 하네요. failure는 Error를 채택해야 합니다.
바로 코드로 살펴보겠습니다.
func callRequest(
_ targetDt: String,
completion: @escaping (Result<[Movie], Error>) -> Void
) {
// Request
guard let request = getURLRequest(targetDt) else { return }
// Session
let session = URLSession.shared.dataTask(with: request) { data, response, error in
// 실패 - 에러 존재 (data == nil)
if let error = error {
print("에러 존재")
completion(.failure(error))
return
}
// 실패 - 데이터 X
guard let data = data else {
print("데이터 nil")
completion(.failure(NSError(domain: "No Data", code: 0, userInfo: nil)))
return
}
do {
let result = try JSONDecoder().decode(BoxOfficeResult.self, from: data)
// 성공
print("성공")
completion(.success(result.boxOfficeResult.dailyBoxOfficeList))
} catch {
// 실패 - 디코딩 실패
print("디코딩 실패")
completion(.failure(error))
}
}
// resume
session.resume()
}
completion이 (Result<[Movie], Error>) -> Void의 형태로 구성되어 있어서,
호출할 때 .success나 .failure 중 하나를 선택해야 합니다.
자동적으로 둘 다 값이 있을 경우나, 둘 다 값이 없을 경우를 제거할 수 있겠네요!
// 기존 코드
NetworkManager.shared.callRequest(targetDt) { data, error in
if let error = error {
// 실패 시 실행할 코드
print(error)
return
}
if let data = data {
// 성공 시 실행할 코드
dump(data)
}
}
// Result 사용
NetworkManager.shared.callRequest(targetDt) { response in
switch response {
case .success(let data):
// 성공 시 실행할 코드
dump(data)
case .failure(let error):
// 실패 시 실행할 코드
print(error)
}
}
사용할 때의 코드를 비교해 보면 data, error에서 Result 타입의 response 하나로 바뀐 것을 볼 수 있습니다.
각각의 case에서 연관값으로 넘어오는 값은 Optional이 아니라 바인딩을 하는 부분도 사라졌네요.
오늘은 네트워크 통신 상황에서 Result Type을 활용하는 방법을 알아보았습니다. 이외에도 다양한 상황에서 Result Type을 활용할 수 있을 것 같아요. 긴 글 읽어주셔서 감사합니다! :)
'iOS' 카테고리의 다른 글
| [iOS] Code Base로 시작하기 (0) | 2024.08.19 |
|---|---|
| [iOS] ARC(Automatic Reference Counting) (0) | 2024.08.12 |
| [iOS] 싱글톤 패턴 (0) | 2024.07.30 |
| [iOS] Dispatch Queue (0) | 2024.07.18 |
| [iOS] identifier를 다루는 세가지 방법 (0) | 2024.07.10 |