Task.yield()와 Cancellation 완전 정리
Task.yield()의 동작 원리, Cooperative Cancellation, Structured vs Unstructured Concurrency를 Q&A 형식으로 정리
1️⃣ Task.yield() 기초
Task.yield()는 무엇인가?
현재 실행 중인 Task를 자발적으로 일시 중단(suspend)하고, Cooperative Thread Pool에 스레드를 반환하여 다른 Task들이 실행될 기회를 주는 함수입니다.
await Task.yield()를 앞에 해야 하나, 뒤에 해야 하나?
상황에 따라 다르지만, 앞에 하는 게 더 안전하고 공정한 패턴입니다.
yield한 다음에 try Task.checkCancellation()을 하는 이유는?
네, 맞습니다! yield() 중에 (suspend 되어 있는 동안) 다른 곳에서 Task가 취소될 수 있기 때문입니다.
시간 12ms: 사용자가 뒤로가기 → Task 취소됨 ❌
시간 13ms: Task resume
시간 14ms: try Task.checkCancellation() → CancellationError throw → 작업 중단 ✅
yield는 동기 코드를 비동기로 변환하는 것인가?
아니요! yield는 스레드 양보일 뿐, 동기 코드는 여전히 동기 코드로 실행됩니다.
2️⃣ Cooperative Cancellation (협력적 취소)
뒤로가기 버튼 누르면 for문이 자동으로 멈추나?
아니요. Swift의 Cooperative Cancellation이라서 명시적으로 체크해야 합니다.
예외: 일부 API는 내부적으로 취소 체크함
3️⃣ Task.detached의 Suspend와 Cancel
Task.detached할 때도 suspend, cancel 체크하나?
네, 해야 합니다! Task.detached도 Task의 일종이므로 suspend/cancel 체크가 필요합니다.
부모 Task가 취소되면 detached도 자동으로 취소되나?
아니요! Unstructured Concurrency이므로 부모 취소를 자동으로 상속받지 않습니다.
4️⃣ Structured vs Unstructured Concurrency
VC가 deinit되면 withTaskGroup의 작업도 취소되나?
네, 맞습니다! Structured Concurrency의 핵심입니다.
0초: Task 시작
0초: withTaskGroup 시작
0초: heavyWork() 시작 (10분 예정)
3초: 사용자가 뒤로가기 버튼 탭
3초: VC deinit ✅
3초: Parent Task 취소 ✅
3초: withTaskGroup 취소 ✅
3초: group의 모든 자식 Task 자동 취소 ✅
3초: heavyWork() 안에서 await하는 부분이 throw
3초: 종료 (10분 기다리지 않음)
defer에 cancel을 넣는 건 unstructured라서 수동 관리?
정확합니다!
이 코드는 "structured(자동 취소) vs unstructured(수동 취소)" 대비를 보여주려는 예시다. 의도는 "Task.detached는 부모와 분리되니 defer로 수동 취소하자"는 것. 하지만 실제로는 defer cancel이 의도대로 동작하지 않는다:
await detached.value(와.result)는 현재 task의 취소에 반응하지 않는다 — detached가 끝날 때까지 그냥 기다림(CancellationError를 던지지 않음).- 그래서 부모가 취소돼도 이 await는 안 멈추고,
defer { detached.cancel() }는 detached가 이미 끝난 뒤에 불려 사실상 무효. - 정상 완료 시에도 마찬가지 — value를 받은 뒤 함수가 끝나며 cancel하니 이미 끝난 task라 무의미.
- 즉 "부모 취소 시 detached도 취소" 의도를 이 패턴으로는 달성 못 한다.
그럼 detached의 취소를 제대로 전파하려면?
withTaskCancellationHandler의 onCancel을 쓴다 — 부모 취소되는 즉시 호출된다(defer처럼 함수 끝까지 기다리지 않음).
withTaskCancellationHandler를 쓰거나, 애초에 structured(withTaskGroup / async let)를 써라 — 부모 취소가 자식에 자동 전파된다. defer cancel은 "함수가 끝날 때만" 불려서 취소 전파엔 부적합하다.
5️⃣ defer 호출 타이밍
VC가 닫히면 defer가 바로 호출되나?
아니요! Task가 취소되어도 defer가 즉시 호출되지 않습니다. 함수가 return 또는 throw해야 defer가 호출됩니다.
3초: [VC 닫힘 → Task 취소됨]
3초: defer 호출 안 됨! ← Task만 취소됨, 함수는 아직 안 끝남
5초: Task.sleep 완료 → CancellationError throw! ✅
5초: 함수 종료! ← 여기서 defer 호출됨 ✅
5초: "defer 실행"
defer 호출 조건
6️⃣ Parent Task vs detached Task
Parent Task가 Task { await processData() }에 있는 Task인가?
정확합니다!
Task 계층 구조
| 생성 방식 | 종류 | 부모 취소 자동 전파 | actor 상속 |
|---|---|---|---|
async let / withTaskGroup | structured | ✅ | ✅ |
Task { } | unstructured | ❌ | ✅(현재 actor·task-local) |
Task.detached { } | unstructured(detached) | ❌ | ❌ 완전 독립 |
Task { }는 현재 actor/priority/task-local은 상속하지만 부모 취소가 자동 전파되지 않는다(unstructured). structured는 async let / withTaskGroup뿐 — 이것만 스코프 기반으로 완료 보장 + 취소 자동 전파.
try Task.checkCancellation()은 detached가 아니라 processData()의 Parent Task 취소를 보는 거지?
정확합니다!
7️⃣ 실전 문제와 해결
캔슬 체크를 하지 않으면 무작정 기다리는 거 아닌가?
정확합니다! Parent Task가 취소되어도 detached가 끝날 때까지 기다립니다.
5초: [사용자가 뒤로가기 → Parent Task 취소]
5초: 하지만 for 루프는 계속 진행 🔴
5초: try await task.value ← 각 task가 끝날 때까지 기다림
60초: 모든 task 완료
60초: 함수 종료
60초: defer 실행 → tasks.forEach { $0.cancel() } (이미 끝났는데?)
해결 방법 1: 주기적으로 Parent 취소 체크
해결 방법 2: withThrowingTaskGroup 사용 (권장)
tasks[0].value만 하면 0번째만 나오면 바로 사용할 수 있나?
정확합니다! 0번째만 완료되면 바로 진행됩니다. 나머지는 백그라운드에서 계속 실행됩니다 (고아 Task).
defer + checkCancellation 둘 다 필요한 이유
3초: defer { detached.cancel() } → 플래그 true로 설정 ✅
3초: detached 루프 계속 진행
3초: try Task.checkCancellation() → isCancelled == true → throw ✅
3초: detached 종료
📌 핵심 정리
1. Task.yield()
- 스레드 양보 (동기→비동기 변환 아님)
- yield 직후 = 취소 확인 최적 타이밍
- await 키워드 있으면 자동 yield (수동 yield 불필요)
2. Cooperative Cancellation
- Task 취소되어도 자동으로 안 멈춤
try Task.checkCancellation()명시적 체크 필수- URLSession 같은 일부 API는 내부적으로 체크
3. Task.detached
- Unstructured → 부모 취소를 상속 안 함
defer { detached.cancel() }로 생명주기 관리- detached 안에서도
try Task.checkCancellation()필요
4. Structured vs Unstructured
- Structured (withTaskGroup) → 자동 취소 전파
- Unstructured (Task.detached) → 수동 관리 필요
- defer 없으면 고아 Task 발생
5. defer 호출 타이밍
- Task 취소 = 플래그만 설정
- 함수가 return 또는 throw해야 defer 호출
- await 지점에서 throw → 함수 종료 → defer
6. Parent Task vs detached
try Task.checkCancellation()은 Parent Task 체크 (detached와 무관)- detached 안에서 체크하면 detached 자신의 취소 상태 확인
- 둘은 완전히 독립적
7. 실전 주의사항
- detached.value 대기할 때 Parent 취소 체크 필요
- defer + checkCancellation 둘 다 필요 (플래그 설정 + 확인)
- 가능하면 Structured Concurrency 사용 권장
8️⃣ 함수 vs Task vs Parent Task
함수 안에 Task를 만들면, 그 함수가 Parent Task인가?
아니요! 함수는 Task가 아닙니다. 함수는 어떤 Task의 컨텍스트 안에서 실행되는 코드일 뿐입니다.
Parent Task의 진짜 의미
VC
└─ Task { } ← Parent Task (진짜 Task)
└─ await doWork() ← 함수 (Parent Task 안에서 실행되는 코드)
함수 안에서 Task 생성하면?
Parent Task
└─ doWork() 함수 (Parent Task 안에서 실행)
└─ Task { } ← 새로운 독립 Task (Parent와 관계없음!)
9️⃣ Parent Task 취소 시 Unstructured Task는?
Parent Task가 취소되면 그 안의 Unstructured Task도 취소되나?
아니요! Parent Task 취소되어도 Unstructured Task는 영향 받지 않습니다.
타임라인 (3초 후 View 사라짐)
0초: "2. 새 Task 시작"
0초: "4. Parent 계속 실행"
3초: [View 사라짐 → Parent Task.isCancelled = true]
3초: 새 Task는 영향 없음 ✅ 계속 실행
5초: Parent의 Task.sleep 완료 → CancellationError throw
5초: Parent Task 중단 (종료)
5초: "5. Parent 완료" 출력 안 됨
10초: "3. 새 Task 완료" 출력 ✅ (계속 살아있었음)
1. Parent Task 취소 = await 지점에서 throw할 때 중단됨
2. Unstructured Task는 영향 없이 계속 실행
3. Structured Task는 자동으로 취소됨
Structured vs Unstructured 비교
🔟 일반 Task { } vs SwiftUI .task { } modifier
Task는 생명주기 영향이 없다면서, 왜 VC 닫히면 Parent Task가 취소되나?
SwiftUI의 .task { } modifier는 특별합니다! 일반 Task { }와 다르게 View 생명주기와 연동됩니다.
일반 Task { } - 생명주기 독립
SwiftUI .task modifier - View 생명주기와 연동
SwiftUI .task modifier 내부 동작 (개념)
- 일반 Task { }는 생명주기 독립 ✅
- SwiftUI .task { } modifier는 View와 생명주기 연동 ✅
- .task { }는 SwiftUI가 자동으로 .cancel() 호출해줌
UIKit에서는 수동 관리 필요
1️⃣1️⃣ Task vs Task.detached 상세 비교
부모 Task를 취소해도 그 안의 Task가 취소 안 된다면, Task와 Task.detached의 차이는 context와 priority뿐인가?
정확합니다! Task와 Task.detached는 "무엇을 상속받느냐"만 다르고, 생명주기 관리는 완전히 동일합니다.
차이점 1: Actor Context 상속
차이점 2: Priority 상속
차이점 3: TaskLocal 상속
🚨 둘 다 동일한 것 (중요!)
실무에서 언제 뭘 쓰나?
비교표
| Task { } | Task.detached { } | |
|---|---|---|
| Actor context 상속 | ✅ | ❌ |
| Priority 상속 | ✅ | ❌ |
| TaskLocal 상속 | ✅ | ❌ |
| 부모 취소 전파 | ❌ | ❌ |
| 생명주기 독립 | ✅ | ✅ |
| defer 필요 | ✅ | ✅ |
| Unstructured | ✅ | ✅ |
- 차이점: Context 상속, Priority 상속, TaskLocal 상속
- 공통점: 둘 다 Unstructured, 부모 취소 전파 안 됨, defer 필요
- 실무: 95% → Task { } 사용 (context 유지), 5% → Task.detached { } 사용 (완전 독립)
📌 핵심 정리
1. Task.yield()
- 스레드 양보 (동기→비동기 변환 아님)
- yield 직후 = 취소 확인 최적 타이밍
- await 키워드 있으면 자동 yield (수동 yield 불필요)
2. Cooperative Cancellation
- Task 취소되어도 자동으로 안 멈춤
try Task.checkCancellation()명시적 체크 필수- URLSession 같은 일부 API는 내부적으로 체크
3. Task.detached
- Unstructured → 부모 취소를 상속 안 함
defer { detached.cancel() }로 생명주기 관리- detached 안에서도
try Task.checkCancellation()필요
4. Structured vs Unstructured
- Structured (withTaskGroup) → 자동 취소 전파
- Unstructured (Task, Task.detached) → 수동 관리 필요
- defer 없으면 고아 Task 발생
5. defer 호출 타이밍
- Task 취소 = 플래그만 설정
- 함수가 return 또는 throw해야 defer 호출
- await 지점에서 throw → 함수 종료 → defer
6. Parent Task vs detached
try Task.checkCancellation()은 Parent Task 체크 (detached와 무관)- detached 안에서 체크하면 detached 자신의 취소 상태 확인
- 둘은 완전히 독립적
7. 실전 주의사항
- detached.value 대기할 때 Parent 취소 체크 필요
- defer + checkCancellation 둘 다 필요 (플래그 설정 + 확인)
- 가능하면 Structured Concurrency 사용 권장
8. 함수 vs Task
- 함수는 Task가 아님 (어떤 Task의 컨텍스트에서 실행되는 코드)
- Parent Task = "이 함수를 실행하는 Task"
- Task { }는 함수 안에 있어도 독립적
9. Parent Task 취소
- Parent 취소되어도 Unstructured Task는 영향 없음
- Structured Task는 자동 취소됨
- Parent는 await 지점에서 throw할 때 중단됨
10. 일반 Task vs .task modifier
- 일반 Task { }는 생명주기 독립
- SwiftUI .task { } modifier는 View와 생명주기 연동
- .task { }는 SwiftUI가 자동으로 cancel 호출
11. Task vs Task.detached
- 차이점: Context 상속, Priority 상속만 다름
- 공통점: 둘 다 Unstructured, 부모 취소 전파 안 됨
- 95% → Task { } 사용, 5% → Task.detached { } 사용