Swift Concurrency 1탄: async/await, Task, TaskGroup개념 정리
Swift는 Concurrency(동시성) 프로그래밍을 지원하기 위해 async/await, Task, TaskGroup등의 기능을 제공합니다. 동시성 프로그래밍을 올바르게 사용하면, 앱의 성능을 최적화하고 데이터 경쟁 상태를 방지할 수 있습니다.
이번 글에서는 Swift의 동시성 개념을 정리하고, 실전에서 어떻게 활용할 수 있는지를 알아보겠습니다.
async/await란?
Swift 5.5부터 도입된 async/await는 비동기 작업을 동기 코드처럼 읽기 쉽게 작성할 수 있도록 해줍니다. 기존의 completion handler 방식보다 훨씬 직관적이며, 코드의 가독성을 높입니다.
기존의 Completion Handler 방식
func fetchData(completion: @escaping (String) -> Void) {
DispatchQueue.global().async {
sleep(2) // 네트워크 요청을 가정
completion("데이터 로드 완료")
}
}
fetchData { result in
print(result)
}
이로 인해서 우리는 몇가지 문제점들이 발생합니다.
[ex) 콜백 지옥, 에러 처리의 어려움, 코드의 비직관적인 흐름, 메모리 관리 이슈 (Strong Reference Cycle)]
하지만 이 글에서는 중요하지 않으므로 넘어가겠습니다. 그리고 swift는 이러한 문제들을 해결하기 위해 async/await 을 도입하게 되었죠.
async/await를 활용한 방식
func fetchData() async -> String {
await Task.sleep(2 * 1_000_000_000) // 2초 대기
return "데이터 로드 완료"
}
Task {
let result = await fetchData()
print(result)
}
async 키워드를 사용하면 비동기 함수임을 명시할 수 있고, await를 통해 결과를 기다릴 수 있습니다. 이를 통해 코드가 깔끔해지고, 여러 개의 비동기 호출을 연쇄적으로 실행할 때 더 효율적입니다.
Task: 새로운 비동기 작업 시작하기
Task는 새로운 비동기 컨텍스트에서 작업을 실행하는 기본적인 방법입니다. Task {} 블록 내부에서 await를 사용하여 비동기 작업을 실행할 수 있습니다.
Task {
let message = await fetchData()
print(message)
}
Task의 주요 특징
Task를 생성하면 자동으로 백그라운드에서 실행됩니다.await를 사용하여 비동기 작업을 실행할 수 있습니다.Task는 취소 가능하며, 취소 요청이 들어오면 실행 중인 작업을 중단할 수 있습니다.
let task = Task {
let data = await fetchData()
print(data)
}
task.cancel() // 실행 중인 Task를 취소할 수 있음
SwiftUI + ViewModel에서 Task 사용하기
SwiftUI에서 Task를 사용할 때 주의해야 할 점은, View가 사라질 때 실행 중인 Task도 함께 해제해야 한다는 것입니다. 그렇지 않으면 불필요한 리소스 소비가 발생할 수 있습니다.
View + ViewModel에서 Task 사용하기
@MainActor
class MyViewModel: ObservableObject {
@Published var data: String = ""
private var tasks: [Task<Void, Never>] = [] // 실행 중인 여러 개의 Task를 저장
func fetchData() {
let task = Task {
let result = await fetchData()
await MainActor.run { self.data = result }
}
tasks.append(task)
}
func cancelTasks() {
tasks.forEach { $0.cancel() }
tasks.removeAll()
}
}
struct ContentView: View {
@StateObject private var viewModel = MyViewModel()
var body: some View {
VStack {
Text(viewModel.data)
Button("Load Data") {
viewModel.fetchData()
}
}
.onDisappear {
viewModel.cancelTasks() // View가 사라질 때 Task 취소
}
}
}
Task 사용 시 주의할 점
1. View가 사라질 때 Task를 취소해야 함onDisappear에서 Task.cancel()을 호출해 리소스 낭비 방지
2. **@MainActor**를 사용하여 UI 업데이트 보장Task 내부에서 MainActor.run {}을 사용해 UI 업데이트 실행
3. Task를 property로 저장해 관리
Task를 변수에 저장하여 필요할 때 취소할 수 있도록 함
TCA에서 Task 사용하기
TCA에서도 Task를 활용해 비동기 작업을 처리할 수 있습니다. 아래는 fetchData 작업을 수행하는 간단한 예제입니다.
struct AppState: Equatable {
var data: String = ""
}
enum AppAction: Equatable {
case fetchData
case fetchDataResponse(String)
}
struct AppEnvironment {
var fetchData: () async -> String = {
await Task.sleep(2 * 1_000_000_000) // 2초 대기
return "데이터 로드 완료"
}
}
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
switch action {
case .fetchData:
return .run { send in
let result = await environment.fetchData()
await send(.fetchDataResponse(result))
}
case .fetchDataResponse(let result):
state.data = result
return .none
}
}
struct ContentView: View {
let store: Store<AppState, AppAction>
var body: some View {
WithViewStore(store) { viewStore in
VStack {
Text(viewStore.data)
Button("Load Data") {
viewStore.send(.fetchData)
}
}
}
}
}
TCA에서 Task를 사용할 때 주의할 점
Effect.run {}을 활용: 비동기 코드를Effect.run내부에서 실행해야 합니다.- State를 직접 변경하지 않기: 상태 업데이트는
Action을 통해 이루어져야 합니다. - 작업 취소 처리: 필요에 따라
.cancellable(id:)을 사용하여 Task를 취소할 수 있습니다.
TaskGroup: 여러 개의 비동기 작업 관리
TaskGroup을 사용하면 여러 개의 비동기 작업을 그룹으로 묶어 관리할 수 있습니다. 예를 들어, 여러 개의 API 요청을 동시에 실행하고 모든 결과를 기다린 후 합치는 경우에 유용합니다.
func fetchMultipleData() async -> [String] {
await withTaskGroup(of: String.self) { group in
var results: [String] = []
group.addTask { await fetchData() }
group.addTask { await fetchData() }
for await result in group {
results.append(result)
}
return results
}
}
Task {
let data = await fetchMultipleData()
print(data)
}
TaskGroup의 주요 특징
- 여러 개의 Task를 병렬로 실행할 수 있음
- 개별 Task가 완료될 때까지 기다릴 필요 없이, 완료된 순서대로 결과를 받아 처리할 수 있음
- 내부적으로 최적화된 동시 실행 모델을 제공하여 성능을 극대화할 수 있음
Task.detached: 독립적인 비동기 작업 실행
기본적으로 Task는 부모 Task의 컨텍스트를 상속받습니다. 하지만 Task.detached를 사용하면 독립적인 Task를 실행할 수 있습니다.
Task.detached {
let data = await fetchData()
print("Detached Task: \(data)")
}
Task.detached의 주요 특징
Task {
let priority = Task.currentPriority
print("부모 Task의 우선순위: \(priority)")
Task.detached {
let detachedPriority = Task.currentPriority
print("Detached Task의 우선순위: \(detachedPriority)")
}
}
- 부모 Task의 컨텍스트(우선순위, 로컬 스토리지)를 상속받지 않음
- 완전히 새로운 실행 컨텍스트에서 동작
- 부모 Task의 취소 상태에 영향을 받지 않음
언제 Task.detached를 사용해야 할까?
- 독립적인 백그라운드 작업을 수행할 때
- 부모 Task와 무관하게 실행해야 하는 경우 (예: 백그라운드 데이터 처리, 네트워크 요청 등)
⚠️ 하지만 Task.detached는 남용하면 위험할 수 있음. 부모 Task의 취소 상태를 고려하지 않고 실행되므로, 불필요한 리소스 사용을 초래할 수 있음.
Swift Concurrency 개념 정리
async/await: 비동기 함수 실행을 보다 직관적으로 표현하는 방식Task:독립적인 비동기 실행 단위를 생성하는 컨텍스트TaskGroup :여러 개의 비동기 작업을 관리하고 결과를 수집할 수 있음Task.detached :부모 컨텍스트와 독립적으로 실행되는 비동기 Task
마무리
Swift Concurrency는 async/await, Task, TaskGroup, Task.detached등의 기능을 활용하여 동시성 프로그래밍을 쉽게 만들었습니다. 이를 적절히 사용하면 코드의 가독성과 안전성을 높일 수 있습니다.
다음 글에서는 Sendable을 활용하여 데이터 공유의 안전성을 보장하는 방법을 자세히 다뤄보겠습니다. 🚀
⏰ 딜라이트룸에서 알라미와 함께 아침을 바꿀 분들을 모십니다 🙌
- 딜라이트룸 채용 공고 살펴보기 ⏰
(+ 여기서 지원하면 입사 축하금 100만원💸)