[Combine] Networking

컴바인을 이용해서 네트워킹 쉽게하기

naljin
9 min readApr 24, 2020

시작하기

자자 오늘은 컴바인의 네트워킹에 대해 알아봅시다!

백엔드와 통신하기, 데이터 가져오기, 업데이트 푸쉬하기, 인코딩 및 디코딩.. 익숙한가요? 이렇게 모바일 개발자는 많은 네트워킹 작업을 마주할 수 밖에 없는데요! 우리의 Combine은 이러한 작업들을 도와주는 API를 제공합니다. 그리고 이 API는 아래의 두가지 요소를 중심으로 동작합니다.

  1. URLSession
  2. Codable 프로토콜을 통한 JSON 인코딩 / 디코딩

하나씩 알아보도록 하죠!

URLSession extensions

URLSession은 네트워크 데이터 전송 작업에 권장되는 방법입니다! URLSession이 지원하는 다양한 작업들을 살펴볼까요?

  • URL의 내용을 얻기 위한 데이터 전송 작업
  • URL의 내용을 얻어서 파일로 저장하는 다운로드 작업
  • URL에 파일 및 데이터를 업로드하는 업로드 작업
  • 두 당사자 간에 데이터를 스트리밍하는 스트리밍 작업
  • Webocket에 연결하는 Webocket 작업

이 중에서 데이터 전송 작업만이 Combine의 Publisher를 반환합니다. 진짜 그런지 확인해봅시다.

URLSession에서 dataTaskPublisher는 있지만 uploadTaskPublisher는 없는 것을 볼 수 있습니다!

자 그럼 어떻게 실제로 사용하는지 코드를 봅시다.

guard let url = URL(string: "https://mysite.com/mydata.json") else { 
return
}
// 1. 여기서는 구독의 결과를 변수에 담아 유지하는 것이 중요합니다.
// 이렇게 하지 않는다면 즉시 취소되고 요청이 실행되지 않습니다.
let subscription = URLSession.shared
.dataTaskPublisher(for: url)
.sink(receiveCompletion: { completion in
// 2. 네트워크 연결은 실패하기 쉽기 때문에 오류처리를 해줍니다.
if case .failure(let err) = completion {
print("Retrieving data failed with error \(err)")
}
}, receiveValue: { data, response in
// 3. 데이터와 URLResponse로 이뤄진 튜플이 반환된 것을 볼 수 있습니다.
print("Retrieved data of size \(data.count), response = \(response)")
})

이렇게Combine은 URLSession.dataTask위의 publisher 추상화를 제공합니다.

Codable support

Codable프로토콜은 Swift의 강력한 인코딩 /디코딩 메커니즘입니다.
위의 코드에서 JSON을 다운로드 했다면, 이것을 JSONDecoder로 디코딩할 수 있습니다.

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을 디코딩하던 방식이죠?

.tryMap { data, _ in
try JSONDecoder().decode(MyType.self, from: data)
}

하지만 Combine에서 제공하는 연산자를 통해 이를 간편하게 처리할 수 있습니다. 아래처럼 말이죠!

.map(\.data)
.decode(type: MyType.self, decoder: JSONDecoder())

tryMap 을 이용할때 closure에서 JSONDecoder를 매번 생성했습니다. 하지만 이렇게 처리하면 publisher를 설정할때 JSONDecoder를 한번만 초기화하면 됩니다!

전체 코드로 다시 한번 볼까요?

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

decode(type:decoder:) 전에는 반드시 map(_:) 을 사용해야 합니다. 우리가 디코딩하려고 하는건 dataTaskPublisher(for:) 에서 방출한 (data, response) 튜플 중 데이터 부분이기 때문이죠!

다수의 subscriber에 네트워크 데이터 Publishing

Publisher는 구독(subscribe)될 때마다 일을 시작합니다. 네트워크 요청의 경우, 다수의 subscriber가 결과를 필요로 할 때 동일한 요청을 여러 번 보내는 것을 의미합니다. 이게 무슨 말일까요?

let publisher = URLSession.shared
.dataTaskPublisher(for: url)
.map({ (data, res) -> Data in
print("동작!")
return data
})

만약 위의 publisher를 .sink 등을 통해 두번 구독한다고 한다고 해봅시다. 그러면 위의 코드가 두번 수행되고 동작! 은 콘솔에 두번 찍히게 되는 것이죠.하지만 나는 같은것이라면 한번만 요청하고 싶은데..!

안타깝게도 이를 쉽게 해결할 수 있는 연산자는 부족합니다. 캐싱을 사용하는 것 외에, 하나의 해결책은 multicast 연산자를 사용하는 것입니다. 이는 ConnectablePublisher를 만듭니다. 여러 번 구독할 수 있고 이후 준비가 되면 publisher의 connect 함수를 호출할 수 있도록 합니다.

let url = URL(string: "https://medium.com/@rkdthd0403")!
let publisher = URLSession.shared
// 1. multicast의 클로져는 적절한 유형의 subject를 반환해야합니다.
.dataTaskPublisher(for: url)
.map(\.data)
.multicast { PassthroughSubject<Data, URLError>() }
// 2. publisher를 첫번째로 구독합니다. 다만 ConnectablePublisher이기 때문에 바로 작동되지는 않습니다.
let subscription1 = publisher
.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)")
})

// 3. 두번째로 구독합니다.
let subscription2 = publisher
.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)")
})
// 4. 준비가 되면 publisher를 연결합니다. publisher는 동작을 시작하고 모든 subscriber에게 값을 주입할 것입니다.
let subscription = publisher.connect()

이렇게 요청은 한 번 보내고 그 결과를 두 가입자에게 공유할 수 있습니다.

참고: 모든 Cancellable은 저장해야합니다. 그렇지 않으면 현재 코드 범위를 벗어날 때 deallocated 및 취소됩니다.

Key points

  • Combine은 dataTask(with:completionHandler:)메서드에 대한 publisher-based 추상화를 제공합니다. 바로 dataTaskPublisher(for:) 이죠!
  • 데이터 값을 방출하는 publisher에 내장 디코드 연산자를 사용하여 Codable을 따르는 모델을 디코딩할 수 있습니다.
  • 여러 subscriber에 대한 구독의 replay를 공유하는 연산자는 없지만, ConnectablePublishermulticast 연산자를 사용하여 이러한 동작을 재현할 수 있습니다.

이전 포스팅 👈🏻

다음 포스팅 👉🏻

--

--

Responses (2)