Adopting Swift concurrency
Swift에서 동시성(Concurrency) 기능을 사용하면 비동기적이고 올바른 비동기 코드를 더 쉽게 작성할 수 있습니다
현재 이 글에서 비동기 함수를 정의하고 호출하는 방법과 스위프트의 동시성 기능을 어떻게 간소화하는지 설명하고 있습니다
Simplifying asynchronous code
SwiftUI 앱에서는 모든 UI 작업을 메인 스레드에서 실행합니다
또한 모든 사용자 이벤트(Tap, Swipe...)도 메인 스레드에서 전달됩니다
앱이 올바르게 작동하려면 모든 뷰 업데이트와 이벤트 핸들러를 메인 스레드에서 실행해야 합니다
하지만 메인 스레드에서 너무 많은 작업을 수행하면 앱 전체가 반응이 느려집니다
메인 스레드가 작업물을 완료하기 위해서 대기하면 뷰 업데이트와 이벤트 처리가 지연되어 앱이 느리거나 멈춘 것처럼 보일 수 있습니다
그래서 필요한 경우에는 메인 스레드에서 작업을 실행하고 가능한 경우에는 백그라운드 스레드에서 실행을 해야합니다
사용자 인터페이스를 유지하기 위해 이러한 작업들을 비동기로 수행합니다
그리고 코드를 작성하기 전에 스위프트 동시성 기능을 제공합니다
스위프트 동시성 세 가지 주요 키워드가 있습니다
비동기 함수 / Task Type / @MainActor annotation
Defining an asynchronous function
비동기 함수(메서드)를 정의하려면 매개변수 목록 뒤에 'async' 키워드를 추가합니다
함수가 값을 반환하는 경우에 -> 기호 앞에 async 키워드를 추가합니다
아래 예제에서 비동기 함수를 사용하고 배열을 반환하는걸 볼 수 있습니다
final class UserStore {
func fetchParticipants() async -> [Participant] {
// 비동기 작업을 수행하는 코드
}
}
Calling an asynchronous function
비동기 함수는 await를 사용해서 호출할 수 있습니다
비동기 함수는 실행을 일시 중지할 수 있기 때문에 '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] {
// 비동기 작업을 수행하는 코드
}
}
'await' 키워드는 'fetchParticipants()' 함수가 완료될 때까지 함수 실행을 일시 중지할 수 있게 합니다
함수가 일시 중지된 동안 함수를 실행하는 스레드는 다른 작업을 수행할 수 있습니다
'fetchParticipants()'함수가 완료되면 시스템은 'refresh()' 함수의 다음 줄에서 실행을 재개합니다
다음 함수 호출인 'fetchRecords(participants:)'는 'fetchParticipants()'의 반환 값을 사용할 수 있습니다
비동기 함수 덕분에 함수는 코드에 나타난 순서대로 실행이 되고 있습니다
Creating an asynchronous context
비동기 함수를 호출하려면 함수 호출이 비동기 컨텍스트 내에 있어야 하는데 대부분의 코드에서는 비동기 컨텍스트가 비동기 함수나 클로저의 본문이 되는데 위의 'refresh()'함수는 비동기 함수로 선언되었기 때문에 두 개의 비동기 함수를 호출할 수 있습니다
동기 컨텍스트를 요구하는 API를 사용할 때 비동기 함수를 호출해야합니다
예를 들어 SwiftUI 'Button' 초기화는 동기 클로저를 받는데 동기 컨텍스트에서 비동기 함수를 호출하려면 'Task'를 생성하여 새로운 비동기 컨텍스트를 만들 수 있습니다. 'Button'액션에 'refresh()'함수를 사용하려면 다음 예제처럼 'Task'를 생성하고 비동기 함수를 'Task' 내에서 호출합니다
struct RefreshButton: View {
@Binding var model: ViewModel
var body: some View {
Button("Refresh") {
// 버튼 액션 내부에서 비동기 함수를 직접 호출할 수 없습니다.
Task {
// Task는 클로저 내부에 비동기 컨텍스트를 제공합니다.
await model.refresh()
}
}
}
}
위의 패턴은 'refresh()'가 값을 반환하지 않으며 오류를 던지지 않기 때문에 잘 작동합니다
'Task'반환된 값이나 던져진 오류는 수동으로 처리해야 하며 그렇지 않으면 손실됩니다
'onAppear(perform:)' 뷰 수정자는 또 다른 동기 클로저의 예인데 SwiftUI는 뷰가 나타날 때 비동기 함수를 실행할 수 있는 'task(priority:_:)' 수정사를 제공합니다. 작어브이 수명은 뷰의 수명과 일치하고 뷰가 사라지면 실행 중인 작업은 취소 됩니다
Updating the user interface
@State @Binding 속성을 사용하여 데이터 모델과 Scrumdinger 사용자 인터페이스를 동기화하는데 이러한 속성을 변경하면 뷰가 업데이트되므로 이를 변경하는 코드는 메인 스레드에서 실행되어야 합니다
비동기 함수는 백그라운드 스레드에서 실행될 수 있기 때문에 이러한 속성을 변경하는 것은 문제가 될 수 있습니다
스위프트 메인 스레드와 상호 작용할 수 있도록 '@MainActor'애노테이션을 제공하고 클래스 선언에 '@MainActor'를 적용하면 클래스의 모든 속성 변경이 메인 스레드에서 처리됩니다. 아래 예제에서는 'UserStore'가 'ObservableObject'이고 'users'는 게시된 속성이고
'@MainActor'애노테이션은 게시된 'users'속성의 수정이 메인 스레드에서 발생하도록 보장하여 이 속성을 '@Binding'과 안전하게 사용할 수 있게 합니당
@MainActor // users 속성의 수정이 메인 액터에서 발생합니다.
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 []
}
}
파일 시스템에서 읽고 쓰는 클래스를 만드는데 비동기 함수를 사용해서 사용자 인터페이스를 반응적으로 유지할 것인데 뷰 수정자에서 비동기 함수를 호출하기 위해 'Task'를 사용하는데 마지막으로 새로운 모델 클래스에 @MainActor를 주석 달아 뷰가 올바르게 메인스레드에서 업데이트 되도록 합니다
'SwiftUI' 카테고리의 다른 글
SwiftUI | Examining data flow in Scrumdinger (0) | 2024.08.06 |
---|---|
SwiftUI | Adopting new API features (0) | 2024.08.06 |
SwiftUI | Responding to events (0) | 2024.08.02 |
Making classes observable (0) | 2024.08.02 |
SwiftUI | Managing data flow between views(@State, @Binding) (0) | 2024.08.01 |