-
Swift Concurrency - @MainActor 사용하기Concurrency 2023. 3. 27. 10:47
안녕하세요. 그린입니다🍏
이번 포스팅에서는 @MainActor를 사용해 메인 큐에서 UI 업데이트를 자동으로 전달하는 방법에 대해 학습해보겠습니다🙋🏻
☝️ UI 업데이트는 꼭 메인 스레드에서 진행되어야 한다
우선 다들 알다시피 iOS에서 UI 업데이트에 관한건 모두 메인 스레드에서만 업데이트 해야한다는 점입니다.
만약 메인 스레드가 아닌 타 백그라운드 같은 스레드에서 UI 업데이트를 친다면 예기치 못한 동작이 발생할 수 있고 또한 경고를 내보내죠!
따라서 백그라운드 스레드에서 직접 혹은 간접적으로 작업을 수행할 때마다 UI 렌더링과 관련한 속성 및 메서드에 접근하기 전에 꼭 메인 스레드로 이동시켜야 합니다.
물론 당연한거 아니야? 라고 생각될 수 있습니다.
그러나 실제 작업을 하다보면 백그라운드 스레드에서 UI 업데이트를 치는 일이 흔하더라구요.
한번쯤은 다들 경험해보셨을거라 생각합니다😭
이런 실수는 결국 앱의 상태 전환 시 충돌 및 기타 오류를 발생할 여지가 너무 넘쳐납니다.
자 그럼 기존에는 일반적으로 어떻게 UI 업데이트에 관한것을 메인 스레드로 넘겨줬을까요?
수동으로 지정하는 메인 큐 디스패치
만약 수동으로 진행한다면 아래와 같이 진행할 수 있습니다.
class ProfileViewController: UIViewController { private let userID: User.ID private let loader: UserLoader private lazy var nameLabel = UILabel() private lazy var biographyLabel = UILabel() ... private func loadUser() { loader.loadUser { [weak self] result in DispatchQueue.main.async { switch result { case .success(let user): self?.nameLabel.text = user.name self?.biographyLabel.text = user.biography case .failure(let error): self?.showError(error) } } } } }
일반적으로는 DispatchQueue.main.async을 이용해 UI 관련 업데이트를 비동기 호출로 래핑할 수 있었습니다.
위 코드는 당연히 정상적으로 돌아가지만 단점은 매번 DispatchQueue.main 코드를 넣어 신경써야 한다는 점입니다.
실제 Combine Publisher를 관찰하거나 특정 딜리게이트 메서드를 구현할 때와 같은 경우 백그라운드 큐에서 실행될 수 있다는 점이 있기에 오류가 발생하기 쉽습니다.
즉, 완전히 명확하지 않은 상황에선 조금 위험할 수 있다는것이죠.
그렇기에 저번 포스팅에서 다뤄봤던 Actor를 기억하시나요?
못보셨다면 아래 포스팅을 먼저 봐주시고 해당 포스팅을 봐주시면 더 이해가 빠르실거에요🙌
https://green1229.tistory.com/341
Main Actor
Swift 5.5에서 main actor를 통해 많은 문제를 해결할 수 있습니다.
핵심만 우선 짚어보자면 Actor 내 구현이 실행중인 모든 작업은 항상 메인 큐에서 수행하게 된다는 점입니다.
그럼 우리는 어떻게 이 메인 액터에게 코드를 정확히 실행할 수 있게 할까요?
가장 먼저 처리해야할 일은 비동기 코드가 새로운 async/await 패턴을 따르도록 만들어야 합니다.
아래 코드를 보시죠!
extension UserLoader { func loadUser() async throws -> User { try await withCheckedThrowingContinuation { continuation in loadUser { result in switch result { case .success(let user): continuation.resume(returning: user) case .failure(let error): continuation.resume(throwing: error) } } } } }
loadUser의 경우 VC가 호출하는 메서드의 async/await 패턴 기반으로 생성하여 수행할 수 있습니다.
즉 아래와 같이 VC에서 사용될 것입니다.
class ProfileViewController: UIViewController { ... private func loadUser() { Task { do { let user = try await loader.loadUser() nameLabel.text = user.name biographyLabel.text = user.biography } catch { showError(error) } } } }
사용을 보시면 do, try catch와 같은 오류 처리 매커니즘과 함께 Task를 통해 비동기 작업을 해줄 수 있습니다.
즉, VC 내에서 loadUser의 새로운 async/await API를 호출할 수 있죠.
그런데 여기서 의문이 되는점은 DispatchQueue.main.async에 대한 호출이 없는데 어떻게 가능할까요?
loadUser가 백그라운드 스레드에서 작업을 해버리면 UI도 백그라운드 스레드에서 업데이트되는게 아닐까요!?여기서 바로 Main Actor가 등장하게 됩니다.
UIKit 기반의 UILabel이나 UIView, UIViewController를 까보시면 아래와 같이 @MainActor 어노테이션이 붙은걸 확인할 수 있습니다.
@MainActor class UILabel: UIView @MainActor class UIViewController: UIResponder
즉 Swift의 Concurrency를 사용할 때 해당 클래스의 모든 속성과 메서드가 메인 큐에서 자동으로 설정 및 호출/접근 된다는것을 의미합니다.
MainActor가 이러한 모든 호출을 항상 메인 스레드에서 작업을 수행하도록 자동으로 라우팅됨으로 위에서 썼던 기본적인 DisapatchQueue.main.async과 같은 수동 호출이 이제 더이상 필요하지 않게 되었습니다🎉
커스텀한 UI 연관 클래스
그렇다면 ViewModel과 같은 완전히 커스텀한 타입에 대해서 작업하는 경우는 어떻게 해야할까요?
SwiftUI를 구성하면서 ViewModel을 ObservableObject를 채택해 클래스를 구성하게 되죠.
이때 메인 큐에만 @Published 속성을 할당해야 할때도 메인 액터를 활용할 수 있습니다.
@MainActor class ListViewModel: ObservableObject { @Published private(set) var result: Result<[Item], Error>? private let loader: ItemLoader ... func load() { Task { do { let items = try await loader.loadItems() result = .success(items) } catch { result = .failure(error) } } } }
이런식으로 말이죠.
그렇다면 해당 class도 메인액터의 속성을 가지게 됩니다.
이제 자동으로 해당 타입을 사용하면 메인 스레드 디스패치를 해주게 됩니다.
다만 하나 유의할 부분은 이런 편리한 속성들이 모두 Swift Concurrency를 사용할때만 수행한다는 점입니다.
결국 기존 Completion Handler난 Combine과 같은 다른 동시성 패턴을 사용한다면 @MainActor 속성은 전혀 아무 의미가 없습니다.
예를 들어 아래와 같이 컴플리션 핸들러를 사용한 메서드라면 @MainActor 속성이 전혀 먹지 않고 해당 코드는 백그라운드 스레드에 할당되어 오류를 범하게 됩니다.
@MainActor class ListViewModel: ObservableObject { ... func load() { loader.loadItems { [weak self] result in self?.result = result } } }
이런 의도는 새로운 Swift Concurrency 시스템을 사용해 액터에 비동기적으로만 접근할 수 있다는 점을 고려한다면 이해할 수 있습니다.
따라서 완전하게 동기적인 컨텍스트에서 동작하는 경우에는 시스템이 자동으로 코드를 메인액터로 디스패치할 수 있진 않습니다👏
(위 컴플리션 핸들러가 실제 백그라운드 스레드에서 호출되는 경우처럼)
결론
점차 Swift가 발전해나감에 따라 대부분의 비동기 코드가 Swift Concurrency로 마이그레이션 된다면 더이상 메인큐에서 UI 업데이트를 수동으로 디스패치할 경우는 사라질것 같습니다!
당연하겠지만 API 호출이나 다른 스레딩 및 기타 동시성 문제를 고려하지 않아도 되는건 아니지만 최소한 백그라운드 큐에서 실수로 UI 업데이트를 수행하는 문제는 이제 신경쓰지 않아도 될 수 있겠네요😄
[참고 자료]
https://www.swiftbysundell.com/articles/the-main-actor-attribute/
'Concurrency' 카테고리의 다른 글
Swift Concurrency - Actor (8) 2023.03.20 Swift Concurrency - 이전 버전에서 비동기 시스템 API 사용하기 (3) 2023.03.16 Swift Concurrency - Throwing & 비동기 Swift 프로퍼티 (5) 2023.03.14 Swift Concurrency - Async sequence & stream (9) 2023.03.09 Swift Concurrency - map & forEach (14) 2023.03.09