이미지 요청 취소하기(Cancel Image Request)

taekki
daily-monster
Published in
6 min readJan 26, 2024

안녕하세요, 금요괴물 태끼(taekki)입니다 :) 일간 괴물 크루에서 함께 글을 쓸 수 있다니 영광입니다! 오늘은 이미지 요청 취소에 대한 이야기를 포스팅하고자 해요. 그럼 시작해볼까요?

이미지 또 너야?

거의 모든 어플리케이션에서 이미지를 표시합니다. 사실상 이미지가 없는 앱은 없다고 볼 수 있죠. 그리고 아이콘, 로고 등을 제외한 이미지는 원격 서버에서 가져오는 경우가 대부분입니다. 이미지가 원격 서버에 있다는 의미는 어떤 의미일까요? 결국 그 역시 네트워크 요청이기 때문에 일정 시간이 소요되고 비용이 든다는 이야기입니다.

시간과 비용이 드는 작업이기 때문에 클라이언트 입장에서는 요청 횟수를 최소화하거나 적절한 타이밍에만 요청을 해야 합니다.

이미지 요청 취소 작업의 필요성

이미지 요청의 경우 시간이 걸리는 비동기 작업이기 때문에 애플리케이션 내에서 경쟁 조건이 발생할 수 있습니다. 어떤 요청 작업이 먼저 끝날지 예측하기도 어렵죠. 테이블 뷰나 컬렉션 뷰 처럼 많은 셀을 가질 수 있는 상황에서는 예상치 못한 문제가 발생할 수도 있습니다. 지금 셀에서 보여져야 하는 이미지가 아닌 다른 이미지가 보여질 수도 있는 것입니다.

좀 더 이야기해보면 화면 밖으로 벗어난 셀에서 진행된 요청 작업이 이제 막 끝나서 화면에 보여지고 있는 셀에 이미지를 전달해버리는 상황이 생기는 것입니다.

이미지 요청 취소 작업

그러면 어떻게 취소 작업을 수행할 수 있을까요?

가령 URLSession을 이용하는 상황이라고 생각해보면 URLSessionDataTask를 반환하는 dataTask 메서드를 사용해서 처리를 해볼 수 있어요.

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

task를 변수에 임시 저장해서 사용할 건데, 이 task에 대해서 다음과 같은 작업들을 수행할 수 있습니다:

  1. resume() : 작업 시작
  2. cancel() : 작업 취소
  3. 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과 같은 프레임워크만 하더라도 취소 가능한 타입을 정의해두고 사용합니다. 한 번은 고민해보고 넘어갈 중요한 지점이라고 생각이 드네요.

--

--