๐ŸŽ iOS & Swift

[iOS] ์ด๋ฏธ์ง€ ์š”์ฒญ ์ทจ์†Œํ•˜๊ธฐ

taeeekki 2023. 10. 26. 20:48

 

์ด๋ฏธ์ง€ ๋˜ ๋„ˆ๋ƒ?

๊ฑฐ์˜ ๋ชจ๋“  ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ด๋ฏธ์ง€๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์‹ค์ƒ ์ด๋ฏธ์ง€๊ฐ€ ์—†๋Š” ์•ฑ์€ ์—†๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์ฃ . ๊ทธ๋ฆฌ๊ณ  ์•„์ด์ฝ˜, ๋กœ๊ณ  ๋“ฑ์„ ์ œ์™ธํ•œ ์ด๋ฏธ์ง€๋Š” ์›๊ฒฉ ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋Œ€๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€๊ฐ€ ์›๊ฒฉ ์„œ๋ฒ„์— ์žˆ๋‹ค๋Š” ์˜๋ฏธ๋Š” ์–ด๋–ค ์˜๋ฏธ์ผ๊นŒ์š”? ๊ฒฐ๊ตญ ๊ทธ ์—ญ์‹œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ผ์ • ์‹œ๊ฐ„์ด ์†Œ์š”๋˜๊ณ  ๋น„์šฉ์ด ๋“ ๋‹ค๋Š” ์ด์•ผ๊ธฐ์ž…๋‹ˆ๋‹ค.

์‹œ๊ฐ„๊ณผ ๋น„์šฉ์ด ๋“œ๋Š” ์ž‘์—…์ด๊ธฐ ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๋Š” ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ตœ์†Œํ™”ํ•˜๊ฑฐ๋‚˜, ์ ์ ˆํ•œ ํƒ€์ด๋ฐ์— ์š”์ฒญ์„ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€ ์š”์ฒญ ์ทจ์†Œ(Cancel) ์ž‘์—…์˜ ํ•„์š”์„ฑ

๊ทธ๋ฆฌ๊ณ  ์ด๋ฏธ์ง€ ์š”์ฒญ์˜ ๊ฒฝ์šฐ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๋Š” ๋น„๋™๊ธฐ ์ž‘์—…์ด๊ธฐ ๋•Œ๋ฌธ์— ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ ๊ฒฝ์Ÿ ์กฐ๊ฑด์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์–ด๋–ค ์š”์ฒญ ์ž‘์—…์ด ๋จผ์ € ๋๋‚ ์ง€ ์˜ˆ์ธกํ•˜๊ธฐ๋„ ์–ด๋ ต์ฃ . ํ…Œ์ด๋ธ” ๋ทฐ๋‚˜ ์ปฌ๋ ‰์…˜ ๋ทฐ ์ฒ˜๋Ÿผ ๋งŽ์€ ์…€์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์—์„œ๋Š” ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ ์…€์—์„œ ๋ณด์—ฌ์ ธ์•ผ ํ•˜๋Š” ์ด๋ฏธ์ง€๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ์ด๋ฏธ์ง€๊ฐ€ ๋ณด์—ฌ์งˆ ์ˆ˜๋„ ์žˆ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ข€ ๋” ์ด์•ผ๊ธฐํ•ด๋ณด๋ฉด ํ™”๋ฉด ๋ฐ–์œผ๋กœ ๋ฒ—์–ด๋‚œ ์…€์—์„œ ์ง„ํ–‰๋œ ์š”์ฒญ ์ž‘์—…์ด ์ด์ œ ๋ง‰ ๋๋‚˜์„œ ํ™”๋ฉด์— ๋ณด์—ฌ์ง€๊ณ  ์žˆ๋Š” ์…€์— ์ด๋ฏธ์ง€๋ฅผ ์ „๋‹ฌํ•ด๋ฒ„๋ฆฌ๋Š” ์ƒํ™ฉ์ด ์ƒ๊ธฐ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€ ์š”์ฒญ ์ทจ์†Œ(Cancel) ์ž‘์—…

๊ทธ๋Ÿฌ๋ฉด ์–ด๋–ป๊ฒŒ ์ทจ์†Œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”?

๊ฐ€๋ น URLSession์„ ์ด์šฉํ•˜๋Š” ์ƒํ™ฉ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ด๋ณด๋ฉด URLSessionDataTask๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” dataTask ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด๋ณผ ์ˆ˜ ์žˆ์–ด์š”.

open func dataTask(with url: URL, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
 

task๋ฅผ ๋ณ€์ˆ˜์— ์ž„์‹œ ์ €์žฅํ•ด์„œ ์‚ฌ์šฉํ•  ๊ฑด๋ฐ, ์ด task์— ๋Œ€ํ•ด์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž‘์—…๋“ค์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

resume() : ์ž‘์—… ์‹œ์ž‘
cancel() : ์ž‘์—… ์ทจ์†Œ
suspend() : ์ž‘์—… ์ผ์‹œ์ค‘์ง€

์ด ์ž‘์—… ์ค‘์—์„œ cancel() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.


์˜ˆ๋ฅผ ๋“ค์–ด์„œ Image ์š”์ฒญ์„ ํ•˜๊ณ  ๊ฐ€์ ธ์˜ค๋Š” ์„œ๋น„์Šค ๊ฐ์ฒด๊ฐ€ ์žˆ๋‹ค๊ณ  ํ•ด๋ณผ๊ฒŒ์š”.

final class ImageService {
  
  func image(for url: URL, completion: @escaping((UIImage?) -> Void)) -> URLSessionDataTask {
    let dataTask = URLSession.shared.dataTask(with: url) { data, _, _ in
      var image: UIImage?
      
      
      defer {
        DispatchQueue.main.async {
          completion(image)
        }
      }
      
      if let data {
        image = UIImage(data: data)
      }
    }
    
    dataTask.resume()
    return dataTask
  }
}
 
// cell
var task: URLSessionDownloadTask?

let service = ImageService()

task = service.image(url: imageURL) {
  // work
}

// cell reuse or collection view end displaying
task.cancel()
 

๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ๋Š” ๋Œ€๋žต ์œ„์™€ ๊ฐ™์ด ํ•ด์ค„ ์ˆ˜ ์žˆ๊ฒ ์ฃ . image๋ฅผ ์š”์ฒญํ•˜๋Š” ์ž‘์—… ์ž์ฒด๋ฅผ task์— ๋‹ด์•„๋‘๊ณ , ์ทจ์†Œํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์—์„œ๋Š” cancel ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ๋ฅผ ๊ฐœ์„ ํ•ด๋ณผ๊นŒ์š”?

์ €๋Š” ์ง€๊ธˆ๋ถ€ํ„ฐ Cancellable์ด๋ž€ ํ”„๋กœํ† ์ฝœ์„ ๋งŒ๋“ค ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ํ”„๋กœํ† ์ฝœ์€ ์ทจ์†Œ ๊ฐ€๋Šฅํ•œ ํƒ€์ž…์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
์œ„์˜ ํ”„๋กœํ† ์ฝœ์„ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ์—๋Š” ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์ง€๋งŒ ์ €๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค:

1. ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์—์„œ ๊ตฌ์ฒด์ ์ธ ํƒ€์ž…์„ ์•Œ์ง€ ๋ชปํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์˜๋„์™€ ๋‹ค๋ฅด๊ฒŒ ๋‹ค๋ฅธ ์ž‘์—…๋“ค์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ทจ์†Œ ์ž‘์—…๋งŒ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.
2. ๋‹น์žฅ์€ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ž‘์—…๋งŒ ๋‹ค๋ฃจ๊ณ  ์žˆ์ง€๋งŒ, ์•ฑ ๋‚ด์—์„œ๋Š” ์ •๋ง ๋‹ค์–‘ํ•œ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ผ๊ด„์ ์œผ๋กœ ๋‹ค์–‘ํ•œ ์ž‘์—…์„ ์ทจ์†Œํ•˜๊ณ  ์‹ถ์„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ด๋ฆ„์€ Combine์˜ ํ”„๋กœํ† ์ฝœ๊ณผ ์ค‘๋ณต๋˜๋‹ˆ, ๋‹น์žฅ์€ ๋‹ค๋ฅธ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

protocol WorkCancellable {
  func cancel()
}
 
WorkCancellable์ด๋ž€ ํ”„๋กœํ† ์ฝœ์„ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ํ”„๋กœํ† ์ฝœ์€ ์ทจ์†Œ๋ฅผ ํ•˜๋Š” cancel() ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
 
extension URLSessionTask: WorkCancellable {}

 

๊ทธ๋ฆฌ๊ณ  URLSessionTask์— WorkCancellable์„ ์ฑ„ํƒํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. (URLSessionDataTask๋Š” URLSessionTask๋ฅผ ์ƒ์†ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.) URLSessionTask๋Š” ์ด๋ฏธ ๋‚ด๋ถ€์ ์œผ๋กœ cancel()๋ฉ”์„œ๋“œ๋ฅผ ๋“ค๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ž๋™์œผ๋กœ ํ”„๋กœํ† ์ฝœ์„ ์ค€์ˆ˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

final class ImageService {
  
  func image(for url: URL, completion: @escaping((UIImage?) -> Void)) -> WorkCancellable {
    let dataTask = URLSession.shared.dataTask(with: url) { data, _, _ in
      var image: UIImage?
      
      
      defer {
        DispatchQueue.main.async {
          completion(image)
        }
      }
      
      if let data {
        image = UIImage(data: data)
      }
    }
    
    dataTask.resume()
    return dataTask
  }
}

 

๊ธฐ์กด์—๋Š” URLSessionDataTask๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ ์ด๊ฒƒ์„ WorkCancellable๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ(์‚ฌ์šฉํ•˜๋Š” ์ชฝ)์—์„œ๋Š” ์ด๋ฏธ์ง€ ์š”์ฒญํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๋•Œ WorkCancellable ํƒ€์ž…์— ๋‹ด์•„๋‘์—ˆ๋‹ค๊ฐ€ ์ž‘์—…์„ ์ทจ์†Œํ•˜๊ณ  ์‹ถ์„ ๋•Œ cancel()๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

์ƒ๊ฐ์„ ํ•ด๋ณด๋ฉด ์ž‘์—…์„ ์ทจ์†Œํ•œ๋‹ค๋Š” ๊ฒƒ์€ ๋„“๊ฒŒ ์ƒ๊ฐํ•ด๋ณด๋ฉด ๋ถˆํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ค„์ด๊ณ , ์•ฑ์˜ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” ์ค‘์š”ํ•œ ์ž‘์—…์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ๋น„๋™๊ธฐ ์ž‘์—…๊ณผ ๋งค์šฐ ๋ฐ€์ ‘ํ•œ ๊ด€๋ จ์ด ์žˆ์ฃ . ์ˆ˜๋งŽ์€ ์˜คํ”ˆ์†Œ์Šค, ๊ทธ๋ฆฌ๊ณ  ๋‹น์žฅ Combine๊ณผ ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋งŒ ํ•˜๋”๋ผ๋„ ์ทจ์†Œ ๊ฐ€๋Šฅํ•œ ํƒ€์ž…์„ ์ •์˜ํ•ด๋‘๊ณ  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ•œ ๋ฒˆ์€ ๊ณ ๋ฏผํ•ด๋ณด๊ณ  ๋„˜์–ด๊ฐˆ ์ค‘์š”ํ•œ ์ง€์ ์ด๋ผ๊ณ  ์ƒ๊ฐ์ด ๋“œ๋„ค์š”.