Coaspe

Adopting Swift concurrency 본문

카테고리 없음

Adopting Swift concurrency

Coaspe 2023. 8. 1. 15:02

비동기 코드를 간단하게 작성하기

SwiftUI 앱에서는 메인 스레드가 모든 UI 작업을 수행합니다. 또한, 탭이나 스와이프 같은 유저 이벤트도 메인 스레드가 처리합니다. 이 모든 작업들이 올바르게 작동하기 위해서는 앱의 뷰 업데이트와 이벤트 핸들러는 모두 메인 스레드에서 수행되어야 합니다.

 

그러나, 너무 많은 작업이 메인 스레드로 몰리면 앱의 반응성이 떨어집니다. 메인 스레드가 어떤 코드의 완료를 기다린다면 그 동안에 수행되어야할 뷰 업데이트 또는 이벤트가 지연되고 사용자로 하여금 결국 앱이 느리고 가만히 있는 것 처럼 느껴지게 합니다. 균형을 맞춰야 합니다. 필요할 때는 메인 스레드에서 실행하지만 가능할 때는 백그라운드 스레드에서 실행합니다.

 

다음 튜토리얼에서는 데이터를 읽고 디스크에 데이터를 쓸 코드를 작성합니다. 사용자 인터페이스의 응답성을 유지하기 위해 이 작업을 비동기적으로 수행합니다. 이 문서에서는 코드를 시작하기 전에 스위프트 동시성 기능에 대한 개요를 제공합니다.

스위프트 동시성의 세 가지 기능, 즉 비동기 기능, 작업 유형 및 @MainActor 어노테이션을 사용해봅시다.

비동기 함수 정의

매개 변수 목록 뒤에 그리고 함수가 값을 반환하는 경우 반환 화살표(->) 앞에 async 키워드를 추가하여 비동기 함수 또는 메서드를 정의합니다. 아래 예에서 UserStore에는 Participant 배열을 반환하는 fetchParticipants()라는 비동기 함수가 있습니다:

final class UserStore {
    func fetchParticipants() async -> [Participant] {...}
}

비동기 함수 호출

await 키워드를 사용하여 비동기 함수를 호출할 수 있습니다. 비동기 함수는 실행을 일시 중단할 수 있기 때문에 다른 비동기 함수의 본문 내부와 같은 비동기 컨텍스트에서만 대기를 사용할 수 있습니다. 아래 예에서 UserStore는 fetchParticipants()를 호출하는 refresh()라는 새 비동기 함수를 포함합니다:

final class UserStore {


   func refresh() async -> [UserRecord] {
      let participants = await fetchParticipants()
      let records = await fetchRecords(participants: participants)
      return records
   }
   func fetchParticipants() async -> [Participant] {...}
   func fetchRecords(participants: [Participant]) async -> [UserRecord] {...}
}

 

wait 키워드는 fetchParticipants()가 완료될 때까지 시스템이 함수의 실행을 일시 중지할 수 있도록 합니다. 함수가 일시 중지된 동안 함수를 실행하는 스레드는 다른 작업을 자유롭게 수행할 수 있습니다. fetchParticipants()가 완료되면 시스템은 refresh() 함수의 다음 행으로 실행을 재개합니다. 다음 함수 호출인 fetchRecords(participants:)는 fetchParticipants()의 반환 값을 사용할 수 있습니다. 비동기 함수의 경우 함수가 코드에 나타나는 순서대로 실행됩니다.

비동기 컨텍스트 생성

비동기 함수를 호출하려면 함수 호출이 비동기 컨텍스트에 있어야 합니다. 대부분의 코드에서 비동기 컨텍스트는 비동기 함수 또는 닫기의 본문이 됩니다. 위의 refresh() 함수는 refresh()를 비동기 함수로 선언했기 때문에 두 개의 비동기 함수를 호출할 수 있습니다.

종종 동기 컨텍스트가 필요한 API로 작업할 때 비동기 함수를 호출합니다. 예를 들어 Swift UI Button 이니셜라이저는 동기 종료를 사용합니다. 동기 컨텍스트에서 비동기 함수를 호출하려면 Task를 사용하여 새 비동기 컨텍스트를 만들 수 있습니다. 아래 예와 같이 버튼 작업에서 refresh() 기능을 사용하려면 Task를 만들고 Task 내부의 비동기 함수를 호출합니다:

struct RefreshButton: View {
   @Binding var model: ViewModel
 
   var body: some View {
      Button("Refresh") {
        // Can't call asynchronous functions directly inside the button action.
        Task {
            // Task provides an asynchronous context inside the closure.
            await model.refresh()
        }
      }
   }

위의 패턴은 refresh()가 아무것도 반환하지 않고 오류를 발생시키지 않기 때문에 잘 작동합니다. 반환된 값이나 작업에서 발생한 오류를 수동으로 처리해야 합니다. 작업 사용에 대한 자세한 내용은 작업 설명서를 참조하십시오.

뷰 수식자 onApare(perform:)는 동기적 클로저의 또 다른 예입니다. SwiftUI는 뷰가 나타날 때 비동기 기능을 실행하는 데 사용할 수 있는 task(priority:_:) 식별자를 제공합니다. 태스크의 수명은 뷰의 수명과 일치합니다. 뷰가 사라지면 실행 중인 모든 작업을 취소합니다.

 

UI 업데이트

지금까지 @State@Binding 속성을 사용하여 Scrumdinger UI가 데이터 모델과 동기화되도록 유지했습니다. 이러한 속성을 변경하면 뷰가 업데이트되므로 이를 변경하는 코드는 메인 스레드에서 실행되어야 합니다. 하지만 비동기 함수는 백그라운드 스레드에서 실행될 수 있으므로 이러한 함수에서 이러한 속성을 수정하는 것은 문제가 있습니다.

Swift는 @MainActor 어노테이션를 제공하여 메인 스레드와 상호 작용할 수 있도록 도와줍니다. 클래스 선언에 @MainActor를 적용하여 클래스의 모든 속성 돌연변이가 메인 스레드에서 처리되도록 할 수 있습니다. 아래 예에서 UserStoreObservableObject이고 사용자는 게시된 속성입니다. @MainActor 어노테이션은 게시된 사용자 속성의 수정이 메인 스레드에서 발생함을 보장하므로 @Binding과 함께 속성을 안전하게 사용할 수 있습니다.

@MainActor // Mutations of the users property will occur on the main actor.
class UserStore: ObservableObject {
    @Published var users: [UserRecord] = []


    func refresh() async {
        let participants = await fetchParticipants()
        let records = await fetchRecords(participants: participants)
        self.users = records
    }
    func fetchParticipants() async -> [Participant] { return [] }
    func fetchRecords(participants: [Participant]) async -> [UserRecord] { return [] }
}

 

Comments