SC #8: Task 취소

발행: (2026년 1월 20일 오전 05:09 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

참고: Swift에서 Task를 취소한다고 해서 실행이 즉시 중단된다는 보장은 없습니다. 각 Task는 수동으로 취소 여부를 확인해야 합니다(예: Task.checkCancellation() 또는 Task.isCancelled 사용).

1. 기본 예제: 이미지 다운로드

func fetchImage() async throws -> UIImage? {
    let imageTask = Task { () -> UIImage? in
        let imageURL = URL(string: "https://httpbin.org/image")!
        var imageRequest = URLRequest(url: imageURL)
        imageRequest.allHTTPHeaderFields = ["accept": "image/jpeg"]

        print("Starting network request...")
        let (imageData, _) = try await URLSession.shared.data(for: imageRequest)

        return UIImage(data: imageData)
    }

    // La tarea se cancela inmediatamente después de ser creada.
    imageTask.cancel()

    // Esperamos el valor (o el error) de la tarea.
    return try await imageTask.value
}

무슨 일이 일어나는가

단계설명
1️⃣URLSession.shared.data(for:) 호출을 UIImage?를 반환하는 Task 안에 감싸습니다.
2️⃣imageTask.cancel() 로 작업을 취소합니다.
3️⃣try await imageTask.value 로 결과를 기다립니다.
4️⃣네트워크 요청이 취소가 적용되기 전에 시작되지만, URLSession은 취소 신호를 존중하고 요청을 중단합니다.

콘솔 출력

Starting network request...
Image loading failed: Error Domain=NSURLErrorDomain Code=-999 "cancelled"

2. Task.checkCancellation()

Task.checkCancellation() 은 작업이 취소된 경우 자동으로 CancellationError 를 발생시킵니다.

func fetchImage() async throws -> UIImage? {
    let imageTask = Task { () -> UIImage? in
        let imageURL = URL(string: "https://httpbin.org/image")!
        // ...

        try Task.checkCancellation()               // 작업이 이미 취소된 상태에서 `checkCancellation()`을 호출했기 때문에 웹 요청이 수행되지 않습니다.
    }
}

3. Task.isCancelled

Task.isCancelled는 예외를 발생시키지 않고 취소 상태를 검사할 수 있게 해줍니다; 우리는 수동으로 처리할 수 있습니다.

func fetchImage() async throws -> UIImage? {
    let imageTask = Task { () -> UIImage? in
        let imageURL = URL(string: "https://httpbin.org/image")!
        // ...

        guard Task.isCancelled == false else {
            print("Image request was cancelled")
            return nil
        }

        let (imageData, _) = try await URLSession.shared.data(for: imageRequest)

        // ...
    }

    imageTask.cancel()
    return try await imageTask.value
}

출력

Image request was cancelled

4. async 함수를 Task로 감싸야 할까요?

작업을 의도적으로 취소하고 싶을 때만 명시적으로 Task를 생성하면 됩니다. 수동 취소가 필요하지 않다면, 래퍼 없이 함수를 작성할 수 있습니다:

func fetchImage() async throws -> UIImage? {
    let imageURL = URL(string: "https://httpbin.org/image")!
    var imageRequest = URLRequest(url: imageURL)
    imageRequest.allHTTPHeaderFields = ["accept": "image/jpeg"]

    try Task.checkCancellation()

    let (imageData, _) = try await URLSession.shared.data(for: imageRequest)

    try Task.checkCancellation()

    return UIImage(data: imageData)
}

5. SwiftUI에서 수동 취소

5.1 작업 참조 저장

@State private var image: UIImage?
@State private var imageDownloadingTask: Task?

var body: some View {
    VStack {
        if let image {
            Image(uiImage: image)
        } else {
            Text("Loading...")
        }
    }
    .onAppear {
        imageDownloadingTask = Task {
            do {
                image = try await fetchImage()
                print("Image loading completed")
            } catch {
                print("Image loading failed: \(error)")
            }
        }
    }
    .onDisappear {
        imageDownloadingTask?.cancel()
    }
}

5.2 .task(priority:_: ) 수정자 사용

SwiftUI는 뷰가 사라질 때 작업을 자동으로 생성하고 취소합니다.

@State private var image: UIImage?

var body: some View {
    VStack {
        if let image {
            Image(uiImage: image)
        } else {
            Text("Loading...")
        }
    }
    .task {
        do {
            image = try await fetchImage()
            print("Image loading completed")
        } catch {
            print("Image loading failed: \(error)")
        }
    }
}

중요: .task 내부의 코드는 .onAppear에서 실행되는 모든 코드보다 먼저 실행됩니다. 그러나 작업이 정확히 언제 실행되는지는 보장되지 않으며, .onAppear의 동기 코드가 .task 작업보다 먼저 실행될 수 있습니다.

6. 작업 계층 구조에서 취소 (슈퍼 작업 → 하위 작업)

슈퍼 작업이 취소되면, 모든 하위 작업에 취소 신호가 전달됩니다. 각 하위 작업은 자신의 상태를 명시적으로 확인해야 합니다 (isCancelled 또는 checkCancellation()). 확인하지 않으면 계속 실행됩니다.

예시

func parentTask() async {
    let handler = Task {
        print("Parent task started")
        async let child1 = childTask(id: 1)
        async let child2 = childTask(id: 2)

        // 두 하위 작업이 끝날 때까지 기다립니다.
        _ = try await [child1, child2]
    }

    // 일부 작업을 시뮬레이션한 뒤 취소합니다.
    try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 s
    handler.cancel()

    do {
        _ = try await handler.value
    } catch let error as CancellationError {
        print("La tarea fue cancelada")
    } catch {
        print("Hubo otro error")
    }

    print("Parent task finished")
}

func childTask(id: Int) async throws -> Int {
    for i in 1..<4 {
        try Task.checkCancellation()                // <‑‑ 취소 여부 확인
        try await Task.sleep(nanoseconds: 100_000_000) // 0.1 s
        print("Subtarea: \(id), paso: \(i)")
    }
    print("Terminé la subtarea \(id)")
    return id
}

가능한 결과

  • 슈퍼 작업이 하위 작업이 끝나기 전에 취소되면, childTask의 각 반복이 CancellationError를 발생시키고 실행이 중단됩니다.
  • 어떤 하위 작업이 취소를 확인하지 않으면, 슈퍼 작업이 취소되었음에도 계속 실행됩니다.

7. 빠른 요약

도구무엇을 하는가예외를 발생시키나요?
task.cancel()작업을 중단해야 함를 표시합니다.아니오
Task.checkCancellation()작업이 취소된 경우 CancellationError를 발생시킵니다.
Task.isCancelled작업이 취소된 경우 true를 반환합니다.아니오 (guard/if에서 사용).
.task {} (SwiftUI)뷰가 사라질 때 자동으로 취소되는 작업을 생성합니다.아니오 (하지만 내부 작업은 예외를 발생시킬 수 있음).

이 개념들을 통해 제어하고 최적화하여 Swift 및 SwiftUI 애플리케이션에서 비동기 작업의 취소를 관리할 수 있습니다. 코딩하세요!

btarea: \(id)")
  return id
}
Back to Blog

관련 글

더 보기 »

SC #11: 작업 그룹

TaskGroup은 동적으로 생성된 subtasks를 포함하며, 이 subtasks는 serial 또는 concurrent 방식으로 실행될 수 있습니다. 그룹은 완료된 것으로 간주됩니다…

SC #10: 분리된 작업

Detached Task는 구조화된 동시성 컨텍스트를 벗어나 비동기적으로 작업을 실행하는 분리된 작업입니다. 이를 둘러싼 구조화된 동시성 컨텍스트를 상속하지 않습니다. 이 c를 상속하지 않음...