[Combine] Networking
시작하기
자자 오늘은 컴바인의 네트워킹에 대해 알아봅시다!
백엔드와 통신하기, 데이터 가져오기, 업데이트 푸쉬하기, 인코딩 및 디코딩.. 익숙한가요? 이렇게 모바일 개발자는 많은 네트워킹 작업을 마주할 수 밖에 없는데요! 우리의 Combine은 이러한 작업들을 도와주는 API를 제공합니다. 그리고 이 API는 아래의 두가지 요소를 중심으로 동작합니다.
URLSession
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를 공유하는 연산자는 없지만,
ConnectablePublisher
와multicast
연산자를 사용하여 이러한 동작을 재현할 수 있습니다.