안녕하세요!
요즘 Concurrency 스터디를하고 있는데요! 스터디를하면서 공부한 내용을 시리즈로 정리해보려고 합니다 ㅎㅎ
이 글은 Swift Concurrency를 공부하는 초급~중급 iOS 개발자를 대상으로 작성되었습니다.
Task의 개념부터 실전 활용까지 차근차근 정리해두었으니, Concurrency를 익히고 싶은 분들께 도움 되었으면 좋겠습니다☺️
들어가며..
앱을 개발하다 보면 네트워크 요청, 데이터 처리, 파일 입출력 등 시간이 오래 걸리는 작업을 수행해야 할 때가 있습니다.
이러한 작업을 UI를 담당하는 메인 스레드에서 동기적으로 실행하면, 앱이 멈추거나 사용자가 아무런 반응도 할 수 없는 상태가 될 수도 있습니다.
그래서 지금까지는 GCD(Grand Central Dispatch)나 OperationQueue를 사용하여 비동기 처리를 해왔지만, 이 방식은 콜백이 중첩되면서 코드가 복잡해지고 가독성이 떨어지는 문제가 있었습니다.
그러나 Swift 5.5부터 Swift Concurrency가 도입되면서, async/await과 함께 더 깔끔하고 직관적인 방식으로 비동기 작업을 처리할 수 있는 Task가 추가되었습니다.
이번에는 Task에 대해 살펴보려고 합니다! 그럼 하나씩 알아볼까요?
Task란?
Task는 비동기적인 하나의 작업을 실행하는 구조체이며, 클로저 형태로 실행할 코드를 전달하면서 생성과 동시에 실행됩니다.
즉, Task를 생성하면 내부 코드가 즉시 실행되며, 별도의 예약 없이 바로 시작됩니다.
하지만 Swift의 동시성(Concurrency) 시스템은 Task의 실행을 스케줄링하며, 현재 실행 중인 작업과 시스템 리소스 상황(예: CPU 부하, 다른 비동기 작업들)을 고려하여 실행이 약간 지연될 수도 있습니다.
Task는 코드로 어떻게 사용하는지 살펴볼까요?
아래와 같이 클로저 내부에서 실행할 코드를 작성하면 됩니다!!
Task {
print("Hello from Task!")
}
이 코드를 실행하면 위에 설명한 것과 같이 즉시 Task가 실행되며, 내부 코드가 수행됩니다.
코드를 작성해보면, 위 이미지와 같이 여러 매개변수가 있는 것을 볼 수 있는데요!
operation은 기본 작업 블록이고, priority와 executorPreference는 아래에서 자세히 다루니까 간단히 표로 어떤 역할을 하는지만 찍먹 해봅시다!!
요소 | 의미 |
priority | Task의 실행 우선순위 |
operation | Task가 실행할 클로저(작업 블록) |
executorPreference | Task가 실행될 실행자(Executor)를 결정하는 설정 값 |
Task의 실행과 스케줄링
위에서 설명한 것처럼 Task는 생성되면 즉시 실행될 수도 있지만,
Swift의 동시성 시스템이 CPU 부하나 실행 중인 다른 Task 등을 고려하여 실행 시점을 조정할 수도 있습니다.
실행 시점을 조정하는 데 영향을 주는 요소는 두 가지가 있어요!
1️⃣ Task의 우선순위(priority)
2️⃣ Task가 실행될 환경(executorPreference)
이 두 가지를 적절히 활용하면 Task의 실행 흐름을 더 세밀하게 제어할 수 있습니다.
그럼 하나씩 살펴볼까요? 😃
Task의 실행 순서 지정하기(priority)
priority의 우선순위 종류는 여러개가 있는데, 우선순위가 높을 수록 더 빨리 실행될 가능성이 높지만, 보장은 되지 않습니다.
우선 순위의 종류부터 알아봅시다!
우선순위(priority) | 설명 |
.high | 가장 높은 우선순위를 가지며, 가능한 한 빠르게 실행됨 |
.medium | 기본값으로 설정되며, 일반적인 작업에 적합한 우선순위 |
.low | 우선순위가 낮아, 다른 높은 우선순위 작업이 먼저 실행될 가능성이 큼 |
.background | 시스템이 덜 중요한 작업으로 간주하며, 리소스가 남을 때 실행됨 |
.userInitiated | 사용자의 입력에 즉각 반응해야 하는 작업에 적합 (예: 버튼 클릭 후 실행되는 작업) |
.utility | 지속적으로 실행되지만, 즉각적인 응답이 필요하지 않은 작업에 적합 (예: 백그라운드 데이터 처리) |
예시 코드를 한번 작성해볼까요?
Task(priority: .high) {
print("🔴 High Priority 실행")
}
Task(priority: .medium) {
print("🟠 Medium Priority 실행")
}
Task(priority: .low) {
print("🟡 Low Priority 실행")
}
Task(priority: .background) {
print("🔵 Background Priority 실행")
}
실행 흐름 (스케줄러에 따라 다를 수 있음)
🔴 High Priority 실행
🟠 Medium Priority 실행
🟡 Low Priority 실행
🔵 Background Priority 실행
다행히(?) 순서에 맞게 잘 실행됐네요?
하지만 직접 사용할때는 무조건 순서가 보장되지 않는다는 것을 기억하고 쓰셔야 합니다!!!
Task가 실행될 환경 지정하기 (executorPreference)
executorPreference는 Task가 실행될 실행자(Executor)를 결정하는 설정 값입니다.
즉, Task가 어떤 실행 환경에서 실행될지를 지정할 수 있는 중요한 요소 중 하나예요!
대부분의 경우 executorPreference를 직접 설정하지 않아도 Swift의 동시성 시스템이 최적의 실행 컨텍스트를 자동으로 선택해 줍니다.
하지만 아래와 같이 UI 업데이트, 특정 실행 컨텍스트 유지, 성능 최적화가 필요한 경우에는 executorPreference를 직접 지정하면 더 효율적인 동시성 처리를 할 수 있습니다.
1. UI 업데이트
: UI 변경은 메인 스레드에서 실행해야 함
2. 백그라운드 작업
: 파일 입출력, 네트워크 요청 등은 메인 스레드를 차단하면 안 됨
3. 부모 Task와 동일한 실행 환경 유지
: 실행 환경 전환을 최소화하여 성능 최적화
4. 특정 실행자(Executor)에서 실행
: CPU 연산, 데이터베이스 쿼리 등 특정 실행 컨텍스트에서 실행하고 싶을 때
그럼, 어떤 옵션이 있을까요??
옵션 | 설명 |
.unspecified | 특별한 실행자 선호도를 지정하지 않음 (기본값) |
.executor(Executor) | 특정 실행자(Executor)에서 실행되도록 지정 |
.inheritExecutor | 부모 Task의 실행자를 그대로 상속 |
.default | 기본 실행자 사용 (Swift가 자동 결정) |
자자 이제 옵션에 대해서 알아보았으니, 어떻게 사용할 수 있는지 예시 코드를 살펴보겠습니다!
1. UI 작업을 항상 메인 스레드에서 실행하기 (@MainActor)
UI 업데이트 작업은 항상 메인 스레드에서 실행되어야 하므로, @MainActor를 사용하면 안정적인 UI 처리가 가능합니다.
Task { @MainActor in
print("✅ 메인 스레드에서 실행 중")
}
이 코드는 Task가 반드시 메인 스레드에서 실행되도록 보장합니다.
즉, UI 업데이트를 위한 Task라면, 실행 위치가 메인 스레드인지 명확히 지정하는 것이 중요합니다!
2. 특정 실행자에서 실행
만약 특정 실행 환경(예: 백그라운드 작업 전용 실행자)에서 실행해야 한다면, executorPreference를 사용하여 실행자를 직접 지정할 수도 있습니다.
let customExecutor = someCustomExecutor() // 예제 실행자 (가정)
Task(priority: .high, executorPreference: .executor(customExecutor)) {
print("🔧 지정된 실행자에서 실행")
}
특정 실행 환경에서 실행되어야 하는 작업이 있을 때, 실행 컨텍스트를 직접 설정할 수 있습니다.
예를 들어, 네트워크 요청을 처리하는 Task를 별도의 실행자에서 실행하면 메인 스레드의 부하를 줄일 수 있습니다.
3. 부모 Task의 실행자 상속 (inheritExecutor)
부모 Task와 같은 실행 컨텍스트에서 실행하려면 .inheritExecutor를 사용할 수 있습니다.
Task {
print("🚀 부모 Task 실행")
let childTask = Task(executorPreference: .inheritExecutor) {
print("🔄 부모 Task의 실행자에서 실행")
}
await childTask.value
}
부모 Task가 실행되는 컨텍스트(메인 스레드 또는 특정 실행자)를 그대로 유지하면서 실행됩니다.
이렇게 하면 불필요한 실행 컨텍스트 전환을 방지할 수 있어요!
어때요! 좀 이해가 되셨나요?!
그런데 여기 파라미터 값들을 보면 executorPreference와 priority를 동시에 설정하는것도 보이죠?
같이 사용하면 어떻게 될지 궁금하시지않나요!!? 같이 알아보시죠!
executorPreference와 priority의 관계
Task의 우선순위(priority) 와 실행 컨텍스트(executorPreference) 는 서로 별개지만, 동시에 설정하면 실행 방식에 영향을 줄 수 있습니다.
🤔 실행 우선순위가 높은 Task라도 실행이 지연될 수 있다?
Task(priority: .high, executorPreference: .inheritExecutor) {
print("🚀 우선순위가 높지만 실행 환경을 상속한 Task")
}
- priority: .high
→ 빠르게 실행될 가능성이 높음 - executorPreference: .inheritExecutor
→ 부모 Task의 실행자에 따라 실행 시점이 결정됨
→ 부모 Task가 대기 중이면 실행이 지연될 수도 있음
즉, 실행 우선순위가 높더라도 실행 컨텍스트에 따라 실행이 지연될 수 있으므로,
특정 실행 환경에서 빠르게 실행되어야 하는 Task라면 실행자도 함께 고려하는 것이 중요합니다.
마지막으로 정리해보겠습니당
설정 | 옵션설명 | 언제 사용하면 좋을까? |
.unspecified | 특별한 실행자 지정 없음 (기본값) | 대부분의 Task에서 사용 (Swift가 자동 결정) |
.executor(Executor) | 특정 실행자에서 실행 | 백그라운드 처리 등 특정 실행 환경이 필요한 경우 |
.inheritExecutor | 부모 Task의 실행자 사용 | 실행 컨텍스트를 유지해야 하는 경우 |
.default | Swift가 최적의 실행 컨텍스트 결정 | 기본적으로 최적의 실행 환경을 자동 설정 |
Task의 실행 양보하기 (Task.yield())
앱을 만들다 보면, 한 번 실행되면 오래 걸리는 작업을 수행해야 할 때가 있습니다.
예를 들면:
1. Task가 너무 오랫동안 실행될 경우
2.UI 업데이트를 빠르게 처리해야 할 때
3 백그라운드에서 실행되는 긴 작업을 조절해야 할 때
이런 오래 걸리는 작업이 실행되면, CPU를 독점하면서 다른 Task가 실행되지 않는 문제가 발생할 수 있습니다.
그럼, 현재 실행 중인 Task가 잠시 실행을 멈추고, 다른 Task가 실행될 기회를 줄 방법이 있을까요? 🤔
바로 Task.yield() 를 사용하면 됩니다!
Task.yield()란?
Task.yield()를 호출하면 현재 실행 중인 Task가 일시적으로 실행을 멈추고, Swift의 동시성 시스템이 다른 Task가 실행될 기회를 가질 수 있도록 양보합니다. 이후 다시 실행되지만, 언제 실행될지는 Swift의 스케줄러가 결정합니다!
먼저, Task.yield()가 어떻게 동작하는지 간단한 예제를 살펴볼까요?
Task {
print("✅ Task 실행 시작")
await Task.yield() // 다른 Task에게 실행 기회를 줌
print("✅ Task 다시 실행")
}
실행 흐름 (스케줄러에 따라 다를 수 있음)
✅ Task 실행 시작
(다른 Task 실행 기회 부여)
✅ Task 다시 실행
사용하기 어렵진 않죠?? 사용 예시 하나만 더 살펴봅시다!
리스트에 많은 데이터를 추가하는 작업이 길어지면, 앱이 멈추거나 UI가 끊길 수 있는데요.
코드로 간단하게 배열에 100개의 데이터를 추가하는 작업으로 예시를 들어보겠습니다.
Task {
var items: [String] = []
for i in 1...100 {
items.append("Item \(i)")
// 너무 오래 실행되지 않도록 실행을 잠깐 양보
if i % 10 == 0 {
await Task.yield()
}
}
print("✅ 데이터 로딩 완료: \(items.count)개")
}
10개씩 데이터를 추가할 때마다 다른 Task가 실행할 수 있도록 잠시 양보하고, 실행이 끝나면 print로 완료 메세지를 출력합니다.
이런식으로 구성하면, 다른 Task를 방해하지 않고 작업을 처리할 수 있습니다.
Task 일정 시간 대기하기
비슷한 친구로 일정 시간 동안 기다렸다가 실행되도록하는 sleep() 메소드가 있는데, 이름만 보면 재운다?의 느낌이죠?
즉, 작업 중인 Task를 지정한 시간 동안 멈추고(재우고) 다른 Task를 실행시키는 메소드에요!
그래서 주로 일정 시간 동안 실행을 멈추고 기다려야하는 작업들에 사용하는데요.
예를 들어:
1. 네트워크 요청 후 일정 시간 기다려야 할 때
→ 예: API 호출 후 재요청을 방지하기 위해 일정 시간 대기
2. 타이머 기능을 구현할 때
→ 예: 일정 간격으로 반복되는 작업 (예: 5초마다 알람 체크)
3. 백그라운드에서 실행되는 작업을 일정 시간 간격으로 실행할 때
→ 예: 배터리 절약을 위해 주기적으로 실행되는 백그라운드 작업
이런 경우 Task가 바로 실행되면 불필요한 API 호출이나 리소스 낭비가 발생할 수 있습니다.
이때, Task.sleep()을 사용하면 됩니다!
Task.sleep()란?
비동기 환경에서 일정 시간 동안 현재 Task의 실행을 멈추는 기능을 제공합니다. 즉, Task가 sleep()을 호출하면 지정한 시간 동안 일시적으로 대기하고, 그 시간이 지나면 다시 실행됩니다.
위에서 설명했던 것처럼, 단순히 기다리는 것이 아니라, 그 시간 동안 다른 Task가 실행될 수도 있습니다! 즉, "이 Task는 당분간 할 일이 없으니 다른 Task를 실행하세요!" 라고 양보하는 것과 비슷한 개념이에요ㅎㅎ
Task.sleep()가 어떻게 동작하는지 간단한 예제를 살펴볼까요?
Task {
print("⏳ Task A 시작")
try? await Task.sleep(for: .seconds(2)) // 2초 동안 대기
print("✅ Task A 완료")
}
Task {
print("🔥 Task B 실행!")
}
예상 실행 결과 (스케줄러에 따라 다를 수 있음)
⏳ Task A 시작
🔥 Task B 실행!
✅ Task A 완료
이번에도 사용 예제 하나만 더 살펴봅시다!
타이머 기능을 하는 Task인데요. 일정 시간 (5초) 간격으로 실행되는 작업입니다.
Task {
while true {
print("⏰ 5초마다 실행되는 작업!")
try? await Task.sleep(for: .seconds(5)) // 5초마다 실행
}
}
예상 실행 결과 (5초마다 반복 실행)
⏰ 5초마다 실행되는 작업!
(5초 후)
⏰ 5초마다 실행되는 작업!
(5초 후)
⏰ 5초마다 실행되는 작업!
...
마지막으로, 표로 정리해보겠습니당
메서드 | 동작 방식 | 사용 목적 |
Task.yield() | 현재 Task의 실행을 멈추고, 다른 Task가 실행될 기회를 줌 | CPU를 너무 오래 점유하지 않도록 다른 Task에 실행 기회를 양보 |
Task.sleep() | 현재 Task가 일정 시간 동안 멈춤 (다른 Task 실행 가능) | 특정 시간 동안 대기 후 다시 실행 (네트워크 요청 대기, 타이머 구현 등) |
Task와 상호작용하기
Task와 상호작용이란 Task의 실행을 제어하거나, 결과를 기다리는 것을 의미합니다.
즉, 실행 중인 Task를 취소하거나, Task가 완료될 때까지 기다리거나, 현재 상태를 확인하는 것이 이에 해당됩니다.
Task와 상호작용하려면 어떻게 해야 할까요?
Task와 상호작용하려면 Task의 인스턴스(변수) 를 생성해야 합니다.
변수를 만들지 않으면 작업은 실행되지만, 직접 취소하거나 결과를 기다릴 방법이 없습니다.
하지만, Task를 변수에 바인딩하지 않아도 실행 자체에는 문제가 없습니다.
공식 문서에서도 "참조를 버리는 것은 프로그래밍 오류가 아닙니다." 라고 설명하고 있습니다.
그렇다면 이제 상호작용에대해 알아볼까요?!
1. Task 취소하기
비동기 작업을 실행하다 보면, 더 이상 필요하지 않은 Task를 중간에 취소하고 싶을 때가 있습니다.
예를 들어, 네트워크 요청을 보냈는데 사용자가 뒤로 가기를 눌렀다면, 굳이 응답을 기다릴 필요 없이 Task를 취소하는 것이 좋겠죠? 😊
Task를 취소하려면 Task.cancel() 을 호출하면 됩니다.
간단히 코드를 살펴볼까요?
let task = Task {
for i in 1...5 {
print("✅ Task 실행 중: \(i)")
try? await Task.sleep(for: .seconds(1))
}
}
Task {
try? await Task.sleep(for: .seconds(2))
task.cancel() // 🚨 Task 취소
}
예상 실행 결과
✅ Task 실행 중: 1
✅ Task 실행 중: 2
✅ Task 실행 중: 3
✅ Task 실행 중: 4
✅ Task 실행 중: 5
첫번째 Task에서는 1부터 5까지 숫자를 출력하고 sleep() 메소드를 사용하여 중간중간 1초씩 대기처리가 되고 있습니다.
그러면 cancel()처리를 하는 두번째 Task가 중간에 실행될 수 있잖아요? 그럼 cancel()이 호출된건데 바로 취소가 되지 않고 숫자 5까지 잘 출력되는걸까요?
그 이유는 cancel()을 호출한다고 해서 Task가 즉시 멈추는 것은 아니기 때문입니다.
호출해도 Task는 여전히 실행될 수 있으며, 직접 취소 로직을 구현해 주어야 해요.
그럼 이제, Task를 안전하게 중단하는 방법을 함께 살펴볼까요?
Task.cancel()과 Task.isCancelled 사용하기
위에서 말한 것 처럼, Task.cancel()은 호출하면 즉시 중단되지는 않습니다.
Task가 "취소됨" 상태가 되는건데, 이때 Task.isCancelled을 사용하여 Task가 취소되었을 경우 실행을 멈추도록 구현해야 합니다.
그래서 불필요한 작업을 줄이기 위해 유용하게 사용되는데요
예를 들어,
1. 사용자가 화면을 벗어날 때
→ 네트워크 요청을 취소하여 불필요한 API 호출 방지
2. 백그라운드에서 실행되는 반복 작업 중단
→ 사용자가 앱을 종료하거나 설정을 변경했을 때 불필요한 작업을 멈춤
3. 리소스 낭비 방지
→ 오래 걸리는 연산을 취소하여 CPU와 메모리를 효율적으로 관리
이처럼 네트워크 요청, 파일 다운로드, 반복 작업 등에서 불필요한 작업을 줄이기 위해 유용하게 활용할 수 있습니다.
이제, 간단한 예제 코드를 살펴볼까요?
let task = Task {
for i in 1...5 {
if Task.isCancelled { // 취소 상태 확인
print("🚨 Task가 취소됨. 실행 중단")
return
}
print("✅ Task 실행 중: \(i)")
try? await Task.sleep(for: .seconds(1)) // 1초 대기
}
}
// 2초 후 Task를 취소
Task {
try? await Task.sleep(for: .seconds(2))
task.cancel() // 🚨 Task 취소
}
예상 실행 결과
✅ Task 실행 중: 1
✅ Task 실행 중: 2
🚨 Task가 취소됨. 실행 중단
Task가 취소되었을 때 오류를 던지기 (Task.checkCancellation())
앞서 Task.cancel()을 호출해도 즉시 중단되지 않는다는 것을 알아봤죠?
그래서 취소 처리를 하기위해 저희는 Task.isCancelled을 확인하여 직접 실행을 멈추도록 구현했습니다.
하지만 갑자기 오류를 던져서 Task를 강제로 중단해야하는 상황도 있겠죠?
이때는 Task.checkCancellation()을 사용하면 예외 처리를 쉽게 할 수 있습니다.
이 메소드는 Task가 취소되었을 때, 자동으로 CancellationError를 발생시킵니다.
1. 네트워크 요청 중 취소가 필요할 때
→ 네트워크 요청 중 사용자가 뒤로 가기를 눌렀다면? → 불필요한 API 요청을 방지하기 위해 강제 중단
2. 복잡한 연산 중 취소가 필요할 때
→ 이미지 처리, 데이터 분석 등 오래 걸리는 작업 중 → 불필요한 연산 낭비를 방지하기 위해 Task 중단
3. 백그라운드에서 실행되는 작업을 안전하게 중단해야 할 때
→ 파일 다운로드, 데이터 동기화 등 일정 시간마다 실행되는 Task → 사용자가 앱을 종료하거나 설정을 변경하면 즉시 취소
즉, Task.isCancelled을 활용하면 수동으로 return을 호출해야 하지만, Task.checkCancellation()은 오류를 던져서 더 강력하게 Task 실행을 멈출 수 있습니다.
간단한 예시 코드 살펴봅시다!
let task = Task {
do {
try Task.checkCancellation() // ✅ Task가 취소되었는지 체크
print("✅ Task 실행 중...")
try await Task.sleep(for: .seconds(3))
print("✅ Task 완료!")
} catch {
print("🚨 Task가 취소됨: \(error)")
}
}
// 1초 후 Task를 취소
Task {
try? await Task.sleep(for: .seconds(1))
task.cancel() // 🚨 1초 후 Task 취소
}
위처럼 try-catch 문을 사용하여 Task가 취소되었을 때 적절한 처리를 할 수 있습니다.
예상 실행 결과
🚨 Task가 취소됨: CancellationError()
취소하는 방법에대해 어느정도 이해하셨나요?
관련하여 간단하게 정리하고 다음으로 또 넘어가보죠!
메서드/ 프로퍼티 | 설명 | 사용 예제 |
Task.cancel() | Task를 취소 상태로 변경 (즉시 중단되지 않음) | task.cancel() |
Task.isCancelled | 현재 Task가 취소되었는지 확인 (true/false) | if Task.isCancelled { return } |
Task.checkCancellation() | Task가 취소되었으면 CancellationError 발생 | try Task.checkCancellation() |
2. Task의 결과값을 기다리기(await task.value)
비동기 작업을 실행하다 보면, Task가 완료된 후 결과를 받아야 할 때가 많습니다.
이때 await task.value를 사용하면 Task가 끝날 때까지 기다렸다가 결과를 받아올 수 있습니다.
주로 사용되는 상황 예시는 아래와 같습니다.
1. 네트워크 요청 결과를 받아야 할 때
→ API 호출 후 데이터를 받아와야 하는 경우
예: 로그인 요청 후 토큰을 받아서 저장
2. 비동기 연산 결과를 기다려야 할 때
→ 파일 다운로드, 데이터 변환 등 완료된 후 다음 로직을 실행해야 하는 경우
예: 이미지 다운로드 후 UI 업데이트
3. 여러 Task의 실행 순서를 조절할 때
→ 특정 Task가 완료된 후 다른 Task를 실행해야 할 경우
예: 데이터 로딩 후 화면 전환
간단한 예시 살펴볼까요?
let task = Task { () -> String in
try? await Task.sleep(for: .seconds(2))
return "Task 완료!"
}
Task {
let result = await task.value
print("✅ Task 결과값: \(result)")
}
실행 결과
✅ Task 결과값: Task 완료!
await task.value는 Task가 완료될 때까지 기다린 후 값을 반환하는 것을 볼 수 있습니다.
🤔 Task를 취소한 후 await task.value를 호출한다면?
Task가 취소되었더라도 내부 작업이 즉시 멈추는 것은 아니기 때문에, await task.value는 여전히 Task의 완료를 기다리게 됩니다.
이렇게 되면 이미 불필요해진 Task가 끝까지 실행될 수도 있고, 사용자는 불필요한 대기를 해야 할 수도 있어요.
따라서, Task가 취소되었는지 확인한 후 적절한 값을 반환하는 처리가 필요합니다.
let task = Task { () -> String in
for i in 1...5 {
if Task.isCancelled {
print("🚨 Task가 취소됨. 중단!")
return "취소됨"
}
print("✅ Task 실행 중: \(i)")
try? await Task.sleep(for: .seconds(1))
}
return "Task 완료!"
}
Task {
try? await Task.sleep(for: .seconds(2))
task.cancel() // 🚨 2초 후 Task 취소
}
Task {
let result = await task.value
print("✅ Task 결과값: \(result)")
}
실행 결과
✅ Task 실행 중: 1
✅ Task 실행 중: 2
🚨 Task가 취소됨. 중단!
✅ Task 결과값: 취소됨
지금은 Task가 취소되었을 때 "취소됨"이라는 값을 반환하도록 구현했지만, 실제 구현에서는 적절한 에러 처리를 추가하는 것이 좋습니다.
예를 들어, CancellationError를 던져서 상위 코드에서 처리할 수도 있고, 사용자에게 적절한 메시지를 보여줄 수도 있겠죠!?
Task의 종류
Swift의 Task는 실행 방식에 따라 크게 세 가지로 나눌 수 있습니다.
각 Task의 차이점과 언제 사용하는 것이 적절한지 하나씩 살펴볼게요! 😊
1. 일반 Task (Task {})
일반 Task는 부모 Task 없이 실행되는 기본적인 Task입니다. 시스템에서 관리하는 동시성 환경에서 실행되며, 생성과 동시에 실행됩니다.
Task {
print("✅ 일반 Task 실행")
}
주로 특정 Task의 부모가 필요하지 않은 경우와 네트워크 요청이나 데이터 로딩 같은 독립적인 비동기 작업을 실행할 때 사용합니다.
2. 자식 Task (Child Task)
자식 Task는 부모 Task 내부에서 실행되는 Task로, 부모 Task가 취소되면 함께 취소됩니다.
즉, 부모-자식 관계를 유지하면서 여러 개의 Task를 체계적으로 관리할 때 유용합니다.
Task {
print("✅ 부모 Task 실행")
let childTask = Task {
print("🔹 자식 Task 실행")
}
await childTask.value // 자식 Task가 완료될 때까지 대기
}
주로 부모 Task가 자식 Task의 실행을 관리해야 하는 경우와 특정 Task 그룹이 함께 실행되었다가, 함께 종료되어야 하는 경우에 사용됩니다.
3. Detached Task (독립 실행 Task)
Detached Task는 부모 Task와 독립적으로 실행되는 Task입니다.
부모 Task의 영향을 받지 않으며, 완전히 독립적인 환경에서 실행됩니다.
let detachedTask = Task.detached {
print("🚀 Detached Task 실행")
}
주로 부모 Task와 상관없이 독립적으로 실행되어야 할 작업이 있을 때와 전역적으로 실행해야 하는 Task (예: 백그라운드 데이터 처리, 로깅, 통계 수집 등)에 사용됩니다.
⚠️ 주의할 점! ⚠️
Detached Task는 부모 Task의 실행 컨텍스트를 상속받지 않으므로,
안전한 실행을 위해 @MainActor 같은 실행 컨텍스트를 명확하게 지정해 주는 것이 중요합니다!
let detachedTask = Task.detached { @MainActor in
print("🚀 MainActor에서 실행되는 Detached Task")
}
Detached는 조금 복잡한 것 같아서 따로 한번 더 다루겠습니다ㅎㅎ
어느정도 Task 종류에대해 이해 되셨나요?! 표로 요약해보았습니다!
Task 종류 | 특징 | 부모 Task 영향 | 언제 사용하면 좋을까? |
일반 Task | 부모 Task 없이 실행 | ❌ 영향 없음 | 일반적인 비동기 작업 수행 |
자식 Task | 부모 Task 내에서 실행 | ✅ 부모가 취소되면 함께 취소됨 | 부모-자식 관계를 유지해야 하는 경우 |
Detached Task | 부모 Task와 독립적으로 실행 | ❌ 영향 없음 | 독립적인 백그라운드 작업 수행 |
마치며..
이번 글에서는 Swift Concurrency에서 Task를 생성하고, 실행을 조절하고, 취소하는 방법을 차근차근 살펴보았습니다.
Task 취소는 단순히 "작업을 멈추는 것"이 아니라, 앱의 성능과 리소스를 효율적으로 관리하는 중요한 기능이에요.
이제 Task의 개념을 익혔으니, 실제 프로젝트에서 어떻게 활용할 수 있을지 고민해보는 것이 중요합니다!
- 네트워크 요청을 할 때 Task를 활용해 더 직관적인 코드로 바꿔보기
- UI 업데이트 시 @MainActor와 함께 Task를 활용해보기
- 백그라운드에서 실행해야 할 작업이 있다면 Task의 실행자(executorPreference)를 조정해보기
작은 코드부터 하나씩 실험해 보면서 경험을 많이 쌓아가요! 💪
💭 다음 글은?
다음 글에서는 async/await에 대해 정리해보겠습니다!
또한, 번외편으로 Detached Task와 Task 그룹(TaskGroup, ThrowingTaskGroup) 도 따로 다뤄볼 예정입니다.
혹시 틀린 부분이 있거나 궁금한 점이 있다면 언제든 알려주세요!
긴 글 읽으시느라 고생하셨습니다!! 🙇🏻♀️
참고 문서
https://developer.apple.com/documentation/swift/task
'Swift' 카테고리의 다른 글
[Concurrency] 천천히 알아보는 Task.detached(), 언제 어떻게 써야 할까? (0) | 2025.03.11 |
---|---|
[Swift] Split과 Components (0) | 2023.01.29 |
[Swift] Method 랑 Computed Property 중 어떤걸 사용해야할까? (0) | 2023.01.29 |
[Swift] 생성자(initializer) (1) | 2023.01.29 |
[Swift] Method (메소드) (0) | 2023.01.29 |