wift 정리 - URLSession + Combine
출처: Processing URL Session Data Task Results with Combine - Apple Developer
비동기 연산자들을 사용해 URL로부터 받아온 데이터를 처리하는 방법을 알아봅니다.
네트워크 작업은 본질적으로 비동기 작업이고, 이러한 비동기 작업들을 처리하는 방법은 여러 가지가 있다.
Combine 또한 비동기를 처리하는 프레임워크이므로, 이를 사용하여 네트워크 작업을 간단하게 처리할 수 있다.
dataTaskPublisher(for:)
URLSession
은 URL
혹은 URLRequest
로부터 받아온 데이터를 publish하기 위한 URLSession.DataTaskPublisher
라는 Combine publisher을 제공한다.
dataTaskPublisher(for:)
메서드를 통해 publisher을 생성할 수 있고, 이는 다음 두 가지를 방출할 수 있다.
- task 성공 시
data와URLResponse
로 이루어진 튜플 - task 실패 시
error
기존 dataTask(with:completionHandler:)
와는 다르게, publisher이 옵셔널을 해제해 줍니다.
또한 completion handler을 사용한 코드는 관련 작업들을 전부 다 completion handler 클로저를 사용하여 처리해야 합니다. 하지만 publisher을 사용한다면 클로저의 수많은 작업들을 Combine 연산자들로 대체할 수 있습니다.
Raw Data -> Custom Type
네트워크 작업을 마치면 Data
타입의 값이 전달됩니다.
Combine은 Data
에서 Custom Type
으로 변환하는 작업을 연산자들로 지원해줍니다.
위에서 언급한 대로, task 성공 시 Data
와 URLResponse
가 전달되는데, map(_:)
또는 tryMap(_:)
으로 타입을 변경해줄 수 있습니다.
Data
를 Custom Type
으로 변경하기 위해
(Custom Type
은 Decodable
을 채택한다고 가정합니다.)
Combine의 decode(type:decoder:)
을 사용합니다.
다음은 URL에서 받은 json 데이터를 User
타입으로 변환하는 과정입니다.
struct User: Codable {
let name: String
let userID: String
}
let url = URL(string: "https://example.com/endpoint")!
cancellable = urlSession
.dataTaskPublisher(for: url)
.tryMap() { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return element.data
}
.decode(type: User.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print ("Received completion: \($0).") },
receiveValue: { user in print ("Received user: \(user).")})
-
dataTaskPublisher(for: url)
로 네트워크 작업을 시작합니다. -
tryMap() { element -> Data in }
으로 원래dataTaskPublisher
이 방출하는 튜플인 (Data
,Response
)을 검사하고, 원하는 형태로 가공합니다. -
decode(type: User.self, decoder: JSONDecoder())
으로tryMap()
에서 내려온 data를 User타입으로 변환합니다.
Retry + Error Handling
네트워크 작업은 수많은 에러가 예상되는 작업입니다. 따라서 우리의 앱은 에러를 훌륭히 처리해줘야 할 필요가 있습니다.
경우에 따라서 실패한 작업을 재시도해야하는 상황이 필요할 수 있습니다.
completion handler을 사용한다면, 재시도를 위해 handler을 다시 작성해야 합니다.
retry(_:)
Combine은 retry(_:)
한 줄로 가능합니다.
에러를 받으면 upstream publisher에 대한 구독을 지정된 횟수만큼 다시 생성합니다.
하지만 네트워크 작업이 비용이 많이 드는 작업이므로 너무 많이 재시도를 요청하는것은 좋지 않습니다. 또한 모든 요청이 유효한지 확인해야 합니다.
catch(_:)
에러를 다른 publisher로 변환합니다.
fallback URL에서 데이터를 로드하는 것과 같이, 다른 URLSession.DataTaskPublisher
과 사용할 수 있습니다.
replaceError(with:)
개발자가 제공한 내용으로 에러를 변경합니다.
에러를 다른 내용으로 변경하여, 성공했을 때와 동일한 처리를 실행할 수도 있습니다.
let pub = urlSession
.dataTaskPublisher(for: url)
.retry(1)
.catch() { _ in
self.fallbackUrlSession.dataTaskPublisher(for: fallbackURL)
}
cancellable = pub
.sink(receiveCompletion: { print("Received completion: \($0).") },
receiveValue: { print("Received data: \($0.data).") })
retry(1)
로, 에러가 발생하면 upstream publisher에 대한 구독을 한 번 다시 생성합니다.catch()
로, 에러가 발생하면fallBackUrlSession
을 호출할 수 있습니다.
해당 구문이 실행되는 것은retry(1)
을 한 번 거친 후 실행된다는 것을 의미하므로, 두 번째error
이라고 할 수 있습니다.
Dispatch Queue + Scheduling Operators
URLSession
을 completion handler로 처리할 때에는 handler 내부에서 직접 Dispatch Queue 등을 사용하여 특정 큐로 작업을 옮겨야 했습니다.
receive(on:options:)
메서드로 해당 메서드 이후의 subscriber와 operator의 작업 큐를 지정해줄 수 있습니다.
DispatchQueue와 RunLoop 모두 Combine의 Scheduler
프로토콜을 구현했기 때문에 바로 사용할 수 있습니다.
cancellable = urlSession
.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { print ("Received completion: \($0).") },
receiveValue: { print ("Received data: \($0.data).")})
receive(on: DispatchQueue.main)
으로,sink
의 로그를 Dispatch Queue의 main에서 출력합니다.
Share Data Task Publisher
네트워크 작업은 비용이 많이 드는 작업입니다. 따라서 하나의 요청으로 앱의 여러 부분에서 하나의 DataTaskPublisher
을 구독할 수 있습니다.
Combine의 share()
연산자를 사용하면 여러 연산자 및 subscriber을 연결할 수 있으며, upstream publisher은 하나의 downstream만을 확인할 수 있습니다. 이는 URLSession.DataTaskPublisher
이 네트워크 작업을 한 번만 수행한다는 의미입니다.
let sharedPublisher = urlSession
.dataTaskPublisher(for: url)
.share()
cancellable1 = sharedPublisher
.tryMap() {
guard $0.data.count > 0 else { throw URLError(.zeroByteResource) }
return $0.data
}
.decode(type: User.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { print ("Received completion 1: \($0).") },
receiveValue: { print ("Received id: \($0.userID).")})
cancellable2 = sharedPublisher
.map() {
$0.response
}
.sink(receiveCompletion: { print ("Received completion 2: \($0).") },
receiveValue: { response in
if let httpResponse = response as? HTTPURLResponse {
print ("Received HTTP status: \(httpResponse.statusCode).")
} else {
print ("Response was not an HTTPURLResponse.")
}
}
)
-
share()
으로, 여러 subscriber이 있지만 네트워크 작업을 한 번만 수행할 수 있도록 합니다. -
두 개의 subscriber을
sharedPublisher
에 연결합니다. -
sharedPublisher
이 정말로 한 번만 요청하는지 확인하려면.print(_:to:)
를.share()
연산자 위에 넣어 디버깅할 수 있습니다.
URLSession은 downstream subscriber이 연결되지 않아도, 첫 번째 sink
가 연결되면 데이터를 로드합니다.
다른 subscriber을 연결하는 데 시간이 필요하다면 makeConnectable()
을 사용하여 Publisher.Share
publisher을 ConnectablePublisher
로 변환합니다.
그 후 모든 subscriber이 준비되면, connect()
메서드로 데이터 로드를 시작할 수 있습니다.
Author And Source
이 문제에 관하여(wift 정리 - URLSession + Combine), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@yy0867/Swift-정리-URLSession-Combine저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)