본문 바로가기
iOS

[iOS] Result Type으로 네트워크 통신 개선해보기

by seongminmon 2024. 8. 5.

안녕하세요. 오늘은 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