logo
|
Blog
  • 회사 소개
  • Alarmy
  • DARO
  • Delighthub
딜라이트룸 채용
엔지니어링

Swift Concurrency 1탄: async/await, Task, TaskGroup개념 정리

async/await, Task, TaskGroup
DelightRoom's avatar
DelightRoom
Feb 14, 2025
Swift Concurrency 1탄: async/await, Task, TaskGroup개념 정리
Contents
async/await란?기존의 Completion Handler 방식async/await를 활용한 방식Task: 새로운 비동기 작업 시작하기Task의 주요 특징SwiftUI + ViewModel에서 Task 사용하기View + ViewModel에서 Task 사용하기Task 사용 시 주의할 점TCA에서 Task 사용하기TCA에서 Task를 사용할 때 주의할 점TaskGroup: 여러 개의 비동기 작업 관리TaskGroup의 주요 특징Task.detached: 독립적인 비동기 작업 실행Task.detached의 주요 특징언제 Task.detached를 사용해야 할까?Swift Concurrency 개념 정리마무리

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를 사용할 때 주의할 점

  1. Effect.run {}을 활용: 비동기 코드를 Effect.run 내부에서 실행해야 합니다.
  2. State를 직접 변경하지 않기: 상태 업데이트는 Action을 통해 이루어져야 합니다.
  3. 작업 취소 처리: 필요에 따라 .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만원💸)
Share article
Contents
async/await란?기존의 Completion Handler 방식async/await를 활용한 방식Task: 새로운 비동기 작업 시작하기Task의 주요 특징SwiftUI + ViewModel에서 Task 사용하기View + ViewModel에서 Task 사용하기Task 사용 시 주의할 점TCA에서 Task 사용하기TCA에서 Task를 사용할 때 주의할 점TaskGroup: 여러 개의 비동기 작업 관리TaskGroup의 주요 특징Task.detached: 독립적인 비동기 작업 실행Task.detached의 주요 특징언제 Task.detached를 사용해야 할까?Swift Concurrency 개념 정리마무리

Delightroom

RSS·Powered by Inblog