[Swift] @MainActor란?
안녕하세요. 1000JI 입니다 :)
프로젝트 코드를 보던 중 @MainActor라는 것을 보게 되어서..!!!
궁금해서 블로그를 쓰게 되었습니다.
MainActor에 대해 설명하기 앞서 기존 저희가 UI 업데이트를 어떤 스레드에서 돌렸었는지 기억나시나요?
왜 당연한걸 물어..? 라고 생각하실 수도 있습니다...ㅎㅎ
메인 스레드에서 돌렸었죠? 왜 그런 걸까요? 이 질문에 대답을 못 하실 수도 있을거라 생각해요!
왜냐! 그냥 당연하게 메인스레드에서 도는거 아니야? 라고 생각이 드셨을 것 같아요(제가 그랬습니다).
이유를 살펴보겠습니다.
첫 번째, UIKit은 메인 스레드에서 작동하도록 설계됨
- UIKit은 사용자 인터페이스(UI)와 관련된 작업을 메인 스레드에서 수행하도록 설계되었습니다.
이는 Apple의 프레임워크에서 기본적으로 요구하는 규칙입니다. - 메인 스레드는 애플리케이션의 주요 이벤트 루프를 관리하며, UI 업데이트, 터치 이벤트, 제스처 인식, 화면 렌더링 등 UI와 직접적으로 관련된 모든 작업을 처리합니다.
- 사용자는 앱이 즉각적이고 일관되게 반응 할 것으로 기대하며, 메인 스레드에서 UI 업데이트를 처리하면 이러한 응답성을 보장 할 수 있습니다.
두 번째, UI 상태의 일관성 유지
- 여러 스레드가 동시에 UI를 업데이트하면 UI 상태가 일관성을 잃을 수 있으며, 이는 애플리케이션의 크래시나 예기치 않은 동작을 초래할 수 있습니다.
- 예를 들어, 백그라운드 스레드에서 버튼의 텍스트를 변경하거나, UI 요소를 추가/제거하는 등의 작업을 수행하면,
UI가 예측할 수 없는 상태로 변경될 수 있습니다. 이렇게 된다면 사용자 경험에 심각한 문제를 야기할 수 있습니다.
세 번째, 레이스 컨디션(Race Condition)과 데이터 손상
- 두 번째 문제와 이어지는 내용인데, 백그라운드 여러 스레드가 동시에 UI요소에 접근해 변경하려고 할 때,
데이터 손상이나 비정상적인 UI 상태가 발생 할 수 있습니다. - 예를 들어, 하나의 스레드가 UI 요소를 변경하는 동안 다른 스레드가 동일한 UI 요소를 참조하여 다른 작업을 수행한다면,
결과적으로 UI가 불안정해지고 크래시가 발생할 가능성이 높아집니다.
위와 같은 이유들로 우리는 UI를 메인 스레드에서 처리하고 있었던 거 였습니다 :)
그렇다면 우리는 기존에 어떻게 메인 스레드에서 UI 업데이트를 하고 있었을까요?
수동으로 지정하는 메인 큐 디스패치
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 업데이트를 비동기 호출로 동작 시킬 수 있었습니다.
이 방법이 제일 대표적이며, RxSwift & RxCocoa를 사용하셨다면 .observeOn 연산자를 통해 메인스레드로 변경했거나,
메인 스레드에서 돈다는 것을 보장하는 Drive를 사용 했을 확률이 높습니다 :)
그렇다면 이번 블로그에서 다루게 될 MainActor는 도대체 무엇일까요?
우선 Actor가 무엇인지 간단하게 살펴보고 오셔도 좋을 것 같습니다.
링크 참고 부탁드리구요!
MainActor
Swift 5.5에서는 MainActor를 통해 많은 문제를 해결 할 수 있게 되었습니다.
핵심은 Actor 내 구현이 실행 중인 모든 작업은 항상 메인 큐에서 수행하게 된다는 점 입니다.
1. @MainActor 어노테이션 사용하기
함수 또는 프로퍼티에 @MainActor를 적용한다면, 메인 스레드에서 실행 될 것을 보장하게 됩니다.
데이터를 비동기적으로 불러온 후 UI를 업데이트 하는 곳에 사용한다면 많이 유용하겠죠? :)
class ViewModel: ObservableObject {
// 백그라운드 스레드에서 데이터 로딩
func loadData() async {
// 데이터 로딩 로직
// ...
await updateUI()
}
// UI 업데이트를 위한 @MainActor 함수
@MainActor
func updateUI() {
// UI 업데이트 로직
// ...
}
}
이런 식으로 활용 할 수 있겠습니다.
2. async/await와 함께 사용하기
class ViewModel: ObservableObject {
func loadData() async {
// 백그라운드 작업 수행
// ...
await MainActor.run {
// 메인 스레드에서 UI 업데이트 수행
// ...
}
}
}
이런 식으로 await에 MainActor.run를 활용하여 동작 시킬 수 있습니다.
간단하죠? :)
3. Actor 기반 동시성 모델
Class나 구조체 전체를 @MainActor로 표시하여, 내부의 모든 작업을 메인 스레드에서 실행하도록 할 수 있다.
특히 ViewModel 또는 Controller 같은 클래스에서 메인 스레드에서 동작해야하는 많은 메소드 또는 프로퍼티가 있을 경우 간단하게 사용 할 수 있는 방법입니다.
@MainActor
class ViewModel: ObservableObject {
// 모든 메서드와 프로퍼티는 메인 스레드에서 실행됨
func loadData() {
// 메인 스레드에서 안전하게 데이터 로딩 및 UI 업데이트 수행
// ...
}
}
여기 까지 봤을 때 "그냥 편하게 1번 처럼 사용하고자 하는 메소드 또는 프로퍼티에만 @MainActor 어노테이션을 붙이지말고,
전체에 어노테이션을 붙이는게 편하지 않나?" 라는 생각이 들 수 있습니다.
하지만 아래의 표를 통해 비교해보겠습니다.
함수에 @MainActor 사용 시 고려 사항 | 전체에 @MainActor 사용 시 고려사항 |
- 단일 함수 또는 특정 함수에만 메인 스레드 실행을 제한하고 싶을 때 사용 - 클래스 또는 구조체 내 다른 메서드와 프로퍼티에 영향을 주지 않음 - 다양한 컨텍스트에서 실행되는 메서드가 있을 경우, 함수 수준에서 적용하므로 유연성을 유지 할 수 있음 |
- 객체의 모든 기능이 메인 스레드에서 실행되어야 할 때 사용함(ViewModel, UIViewController) - 객체 또는 구조체의 모든 작업이 메인 스레드에 바인딩되므로, 백그라운드에서 실행되어야 하는 작업이 있다면 적합하지 않음 |
아하, 그러니까
"특정 함수에만 사용 할 경우랑 백그라운드로 돌려야 하는 특정 메소드가 없다면 전체적으로 사용하는 경우 이렇게 나눠서 설계하면 된다!"
라고 이해 하시면 되겠습니다 :)
4. 다른 동시성 패턴과 같이 사용하기
만약 비동기처리와 UI 업데이트를 동시에 활용해야 한다면 다음과 같은 코드로 활용 할 수 있겠습니다.
@MainActor
func updateUI() {
// 이 코드는 메인 스레드에서 안전하게 실행됩니다.
someLabel.text = "Updated"
}
func performTaskInBackground() {
DispatchQueue.global().async {
// 백그라운드 스레드에서 수행되는 작업
let result = heavyCalculation()
// 메인 스레드에서 UI 업데이트
Task {
await updateUI()
}
}
}
정말 간단하죠? :)
정리하면 MainActor는 앱의 안정성과 사용자 경험을 크게 향상 시키는데,
이유는 메인 스레드에서 수행되어야 할 작어보가 백그라운드 스레드에서 수행되어야 할 작업 간의 동기화를 올바르게 관리한다는 점과
앱이 더 반응적이고 부드럽게 동작하게 만들어준다는 점에서 효율적으로 사용 할 수 있다고 생각 할 수 있습니다.
도움이 되셨을까요? :)
궁금한 점이나 틀린 부분이 있다면 댓글로 알려주세요.
감사합니다!
참고 사이트
https://green1229.tistory.com/343
Swift Concurrency - @MainActor 사용하기
안녕하세요. 그린입니다🍏 이번 포스팅에서는 @MainActor를 사용해 메인 큐에서 UI 업데이트를 자동으로 전달하는 방법에 대해 학습해보겠습니다🙋🏻 ☝️ UI 업데이트는 꼭 메인 스레드에서 진
green1229.tistory.com
[iOS] 동시성 해결사 MainActor 활용 가이드
SwiftUI 개발을 하다 보면, 메인 스레드에서 UI를 업데이트하는 것의 중요성을 강조받게 된다. 이것은 앱의 반응성과 성능에 핵심적인 영향을 미친다. 여기서 MainActor가 중요한 역할을 한다고 하는
velog.io
https://mechanicdong.tistory.com/54
[Swift] Async Await와 MainActor의 관계에 대한 고찰
[Swift] Async Await와 MainActor의 관계에 대한 고찰 Swift에서 Concurrency 환경을 만들어야할 때 다양한 사용법이 있다. 특히 비동기로 데이터를 처리한 후 UI를 업데이트할 땐 MainThread에서 처리해야 한다.
mechanicdong.tistory.com