ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Chapter 9: Networking
    Raywenderlich/Combine: Asynchronous Programming 2020. 8. 13. 01:35

    Version

    Swift 5.3, iOS 14, Xcode 12

     

    프로그래머(programmers)로서 하는 일의 대부분 네트워킹(networking)을 중심(revolves)으로 이루어진다. 백엔드와 통신하고(communicating with a backend), 데이터를 가져오고(fetching data), 업데이트를 푸시하고(pushing updates), JSON을 인코딩 및 디코딩하는(encoding and decoding JSON) 등 이 모두가 모바일(mobile) 개발자(developer)의 일상적인 일이다.

    Combine은 일반적인 작업을 선언적(declaratively)으로 수행하는 데 도움이 되는 몇 가지 선택(select) API를 제공한다. 이러한 API는 최신 애플리케이션(applications)의 두 가지 주요 구성 요소(components)를 중심(key)으로 한다:

    • URLSession.
    • JSON encoding and decoding through the Codable protocol.

     

    URLSession extensions

    URLSession은 네트워크(network) 데이터 전송(transfer) 작업을 수행하는데 권장되는 방법이다. 강력한 구성(configuration) 옵션과 완전히 투명한(transparent) 백그라운드(backgrounding) 지원(support)이 포함된 최신(modern) 비동기(asynchronous) API를 제공(offers)한다. 다음과 같은 다양한 작업(operations)을 지원한다:

    • URL의 내용을 검색(retrieve)하기 위한 데이터 전송(transfer) 작업.
    • URL의 내용을 검색(retrieve)하여 파일에 저장(save)하는 다운로드(download) 작업.
    • URL에 파일(files) 및 데이터를 업로드(upload)하는 작업.
    • 두 당사자(parties) 간의 데이터 스트리밍(stream) 작업.
    • 웹소켓(websockets) 연결(connect) 작업.

    이 중에서 첫 번째 데이터 전송(transfer) 작업만 Combine publisher를 노출한다. Combine은 URLRequest 또는 URL만 사용하는 두 가지의(variants) 단일(single) API를 사용하여 이러한 작업을 처리(handles)한다.

    이 API를 사용하는 방법은 다음과 같다:

    guard let url = URL(string: "https://mysite.com/mydata.json") else {
        return
    }
    
    let subscription = URLSession.shared
        //결과 구독을 유지하는 것이 중요하다. 그렇지 않으면 즉시 취소되고 요청이 실행되지 않는다.
        .dataTaskPublisher(for: url)
        //URL을 매개변수로 사용하는 dataTaskPublisher(for:)의 overload를 사용한다.
        .sink(receiveCompletion: { completion in
            if case .failure(let err) = completion {
                //항상 오류 처리를 해야 한다. 네트워크 연결은 실패하기 쉽다.
                print("Retrieving data failed with error \(err)")
            }
        }, receiveValue: { data, response in
            //결과는 Data와 URLResponse 객체의 튜플이다.
           print("Retrieved data of size \(data.count), response = \(response)")
        })

    보다시피 Combine은 URLSession.dataTask의 위(top)에서 투명한(transparent) 베어 본(bare-bones) publisher 추상화(abstraction)를 제공하고, 클로저(closure) 대신 publisher만 노출한다.

     

    Codable support

    Codable 프로토콜(protocol)은 반드시 알아야 할 현대적(modern)이고 강력한(powerful) Swift 전용 인코딩(encoding) 및 디코딩(decoding) 메커니즘(mechanism)이다. 그렇지 않다면, raywenderlich.com의 Apple의 문서(documentation)와 튜토리얼(tutorials)에서 이에 대해 배울 수 있다.

    Foundation은 JSONEncoderJSONDecoder으로 JSON의 인코딩(encoding)과 디코딩(decoding)을 지원한다. PropertyListEncoderPropertyListDecoder를 사용할 수도 있지만, 네트워크(network) 요청(requests) 컨텍스트(context)에서는 유용하지 않다.

    이전 예제에서는 일부 JSON을 다운로드했다. 물론 JSONDecoder로 이를 디코딩(decode)할 수 있다:

    let subscription = URLSession.shared
        .dataTaskPublisher(for: url)
        .tryMap { data, _ in
            try JSONDecoder().decode(MyType.self, from: data)
        }
        .sink(receiveCompletion: { completion in
            if case .failure(let err) = completion {
                print("Retrieving data failed with error \(err)")
            }
        }, receiveValue: { object in
            print("Retrieved object \(object)")
        })

    위와 같이 tryMap으로 JSON을 디코딩(decode)할 수 있지만, Combine은 도움이 되는 다른 연산자(operator)인 decode(type:decoder:)를 제공(provides)한다. 

    위의 예제에서 tryMap 연산자(operator)를 다음으로 변경한다:

    .map(\.data)
    //dataTaskPublisher는 튜플을 내보내므로, map을 사용하여 data 부분만 따로 가져온다.
    .decode(type: MyType.self, decoder: JSONDecoder())

    안타깝게도 dataTaskPublisher(for:)는 튜플(tuple)을 내보내(emits)므로, 결과의 Data 부분만 내보내(emits)는 map(_:)을 먼저 사용하지 않고는 decode(type:decoder:)를 직접(directly) 사용할 수 없다.

    이것의 장점은 publisher를 설정할 때 tryMap(_:) 클로저(closure)에서는 JSONDecoder를 매번 생성하는 것에 비해, decode(type:decoder:)는 JSONDecoder를 한 번만(only once) 인스턴스화(instantiate)한다는 것이다. 

     

    Publishing network data to multiple subscribers

    publisher를 구독(subscribe)할 때마다, 작업이 시작(starts)된다. 네트워크(network) 요청(requests)의 경우에는, 복수(multiple)의 subscribers가 그 결과를 필요로 할 때 동일한 요청(request)을 여러 번 보내는(sending) 것을 의미한다.

    놀랍게도 Combine에는 다른 프레임 워크(frameworks)처럼 이를 쉽게 구현하는 연산자(operators)가 없다. share() 연산자(operator)를 사용할 수도 있지만, 결과가 나오기 전에 모든 subscribers가 구독(subscribe)해야하므로 까다롭다(tricky).

    캐싱(caching) 메커니즘(mechanism)을 사용하는 것 외에 한 가지 다른 해결 방법(solution)은 multicast() 연산자(operator)를 사용하여 Subject로 값(values)을 publishes하는 ConnectablePublisher를 만드는 것이다. subject를 여러 번(multiple times) 구독(subscribe)한 다음, 준비가 되면 publisher의 connect() 메서드(method)를 호출(call)할 수 있다:

    let url = URL(string: "https://www.raywenderlich.com")!
    let publisher = URLSession.shared
        .dataTaskPublisher(for: url) //DataTaskPublisher를 생성
        .map(\.data) //해당 데이터 매핑
        .multicast { PassthroughSubject<Data, URLError>() }
        //multicast의 클로저는 적절한 유형의 subject를 반환해야 한다.
        //혹은 multicast(subject:)에 subject를 전달해야 한다.
    
    let subscription1 = publisher //publisher를 구독한다.
        //ConnectablePublisher이므로, 바로 작동하지 않는다.
        .sink(receiveCompletion: { completion in
            if case .failure(let err) = completion {
                print("Sink1 Retrieving data failed with error \(err)")
            }
        }, receiveValue: { object in
            print("Sink1 Retrieved object \(object)")
        })
    
    let subscription2 = publisher //publisher를 구독한다.
        //ConnectablePublisher이므로, 바로 작동하지 않는다.
        .sink(receiveCompletion: { completion in
            if case .failure(let err) = completion {
                print("Sink2 Retrieving data failed with error \(err)")
            }
        }, receiveValue: { object in
            print("Sink2 Retrieved object \(object)")
        })
    
    let subscription = publisher.connect()
    //준비가 되면, publisher를 연결한다. 작업을 시작하고 모든 subscribers에게 값을 보낸다.

    multicast의 클로저(closure)는 적절한 유형의 subject를 반환하거나 기존 subject를 multicast(subject:)에 전달해야 한다. 13장(chapter), "Resource Management"에서 multicast에 대해 자세히 알아본다.

    이 코드를 사용하여 요청(request)을 한 번 보내고, 결과를 두 구독자(subscribers)에게 공유(share)한다.

    Note: 모든 Cancellables 항목을 저장(store)한다. 그렇지 않으면 현재 코드 범위(scope)를 벗어날 때 할당이 취소되며(deallocated), 이 경우에는 즉시(immediate) 할당이 취소(canceled)된다.

    Combine은 다른 리액티브(reactive) 프레임 워크(frameworks)와 달리, 이러한 종류의 시나리오(scenario)에 대한 연산자(operators)를 제공하지 않기 때문에 이 과정(process)은 약간 복잡(convoluted)하다. 18장(chapter), "Custom Publishers & Handling Backpressure"에서 더 나은 해결 방법(solution)을 살펴본다.

     

    Key points

    • Combine은 dataTaskPublisher(for:)라는 dataTask(with:completionHandler:) 메서드(method)에 대한 publisher-based 추상화(abstraction)를 제공한다.
    • Data 값(values)을 내보내는(emits) publisher에서 내장된(built-in) decode 연산자(operator)를 사용하여 Codable 준수(Codable-conforming) 모델(models)을 디코딩(decode)할 수 있다.
    • 여러(multiple) subscribers와 구독(subscription) 재생(replay)을 공유(share)하는 연산자(operator)는 없지만, ConnectablePublishermulticast 연산자(operator)를 사용하여 이 동작(behavior)을 만들 수 있다.

    'Raywenderlich > Combine: Asynchronous Programming' 카테고리의 다른 글

    Chapter 11: Timers  (0) 2020.08.13
    Chapter 10: Debugging  (0) 2020.08.13
    Chapter 8: In Practice: Project "Collage"  (0) 2020.08.11
    Chapter 7: Sequence Operators  (0) 2020.08.09
    Chapter 6: Time Manipulation Operators  (0) 2020.07.27
Designed by Tistory.