wift ์ •๋ฆฌ - URLSession + Combine

19604 ๋‹จ์–ด ์ •๋ฆฌswiftiOSiOS

์ถœ์ฒ˜: 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() ๋ฉ”์„œ๋“œ๋กœ ๋ฐ์ดํ„ฐ ๋กœ๋“œ๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ข‹์€ ์›นํŽ˜์ด์ง€ ์ฆ๊ฒจ์ฐพ๊ธฐ