티스토리 뷰
iOS로 개발을 어느 정도 해보았다면 DispatchQueue에 대해서 잘 알고 있을 것이다.
@MainActor이랑 DispatchQueue모두 메인 스레드에서 작업을 하도록 하는 건데, 둘의 개념을 간단하게 살펴보고
차이를 알아보고자 한다.
📚 들어가기 전
iOS에서는 UI 관련 이벤트나, 주요 작업이 항상 메인 스레드에서 실행되어야 한다. 왜냐하면 UIKit이나 SwiftUI가 싱글스레드 환경에서 작동하도록 설계되었기 때문이다!
그럼에도 불구하고 여러 스레드에서 UI관련 이벤트들을 실행했을 땐 싱글 스레드 환경이 아닌 멀티 스레드 환경에서 동시에 접속을 하는 것이기 때문에 데이터가 충돌하여 제대로 된 데이터가 표시되지 않거나, UI가 깨져 원하는 UI가 표시되지 않을 수도 있다.
따라서 어떤 작업을 백그라운드에서 실행하고 메인스레드에서 실행할지 정확히 구분해야 한다.
그럼 메인스레드에서 실행되어야 하는 주요 작업들이 뭔지에 대해서 살펴보자
- UI 관련 이벤트 (터치이벤트, 애니메이션, UI 업데이트..)
- App LifeCycle 이벤트 (SceneDelegate, AppDelegate, SwiftUI App)
- NotificationCenter에서 UI 관련 notification 수신
아무튼 이런 이유로 인해서 작업을 메인스레드에서 실행할 수 있도록 하는 것이 DispatchQueue와 MainActor이다!
DispatchQueue

apple 공식문서에 따르면 DispatchQueue는 앱의 메인스레드나 백그라운드 스레드에서 직렬이나 동시에 작업을 관리한다고 설명되어 있다.
(MainActor와 비교하여 메인스레드에서 작업하는 DispatchQueue.main에 대한 설명을 주로 할 예정이기 때문에 DispatchQueue.global에 대한 설명은 생략하도록 하겠다)
DispatchQueue.main.async { ... }
보통 DispatchQueue를 사용할 때 위와 같이 사용하는 게 제일 일반적인 사용법이며 메인 스레드에서 작업을 실행시킬 때 사용한다.
위 코드를 조금 뜯어보면 아래와 같이 3단계로 나눌 수 있다.
- DispatchQueue
- main
- async
즉 DispatchQueue를 이용해서 사용하지만 메인스레드에서 사용할 거고, 비동기로 실행하여 작업을 수행한다는 의미이다!
메인스레드에서 작업을 하는 DispatchQueue.main은 UI 업데이트를 안전하게 보장하기 위해 따로 작업을 빼내는 건데 비동기가 아닌 동시에 실행하게 된다면 UI업데이트가 동시에 발생하게 되어 원하는 동작이 나오지 않을 수 있으며 데드락에 빠질 수 있다.
따라서 반드시 메인스레드에서 작업을 할 때는 Serial Queue 방식으로 동작해야 한다!
하지만 DispatchQueue의 비동기 작업이 너무 많아지거나 서버 응답을 느리게 받아왔을 때 UI 업데이트에 지연시간이 꽤 생겨서 작업을 적절하게 효율적으로 분리하는 게 중요할 거 같다.
func handleDeleteUserAccount() {
viewModelWrapper.deleteUserViewModel.deleteUserAccount { success in
DispatchQueue.main.async {
if success {
showDeleteUserPopUp = false
navigateCompleteView = true
} else {
Log.error("Fail delete UserAccount")
}
}
}
}
실제 코드에서 DispatchQueue를 사용한 모습이다.
위의 코드는 뷰에서 프로필 사진을 삭제하기 위한 메서드이며, 뷰모델에 있는 deleteUserAccount의 클로저를 통해 DispatchQueue를 걸어주고 있는 모습이다.
프로필 사진을 삭제하여 기본이미지로 업데이트하는 과정은 UI를 업데이트해야 하는 과정이기 때문에 메인스레드에서 실행해주어야 했다. 따라서 DispatchQueue를 통해 메인스레드에서 작업을 수행하도록 한 모습이다!
@MainActor

MainActor은 Swift Concurrency에서 메인스레드에서 실행하도록 보장하는 전역 actor이다.
MainActor를 이해하려면 Actor에 대해서 알고 있으면 이해가 더 편할 것이다!
Actor 란? 자신의 내부 상태를 보호하고 외부에서의 접근으로부터 격리하여, 데이터를 안전하게 공유할 수 있게 해주는 도구
따라서 Actor는 자신의 데이터를 보호하고, 외부에서 접근할 때는 비동기적으로 처리하며 한 번에 하나의 작업만 처리하기 때문에 동시에 같은 데이터에 접근할 때 발생가능한 동시성 문제를 해결할 수 있다.
MainActor는 이름에서부터 Actor 개념을 기반으로 동작한다는 것을 확인할 수 있고, 특정 코드가 항상 메인스레드에서 실행됨을 보장하며 전체 프로그램에 걸쳐 공유되는 전역 actor라고 이해하면 된다.
여기서 보장한다는 것은 Swift가 강제적으로 해당 코드가 메인 스레드에서 실행되도록 관리해 주기 때문에 @MainActor가 적용된 함수나 프로퍼티에 접근하는 경우 자동으로 메인 스레드에서 실행되도록 동작하게 된다.
@MainActor func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
var scannedImages: [UIImage] = []
for i in 0..<scan.pageCount {
scannedImages.append(scan.imageOfPage(at: i))
}
parent.onScanCompleted(scannedImages)
parent.presentationMode.wrappedValue.dismiss()
}
위 코드는 visionkit로 문서를 스캔완료 했을 때에 대한 동작을 MainActor로 구현한 코드이다.
동작과정은 스캔 완료 시 스캔한 모든 페이지를 UIImage 타입의 배열로 저장하며 스캔된 이미지를 상위 뷰인 ScanView로 전달하도록 하며 동작이 다 끝나면 상위뷰로 스캔한 이미지를 넘겨주고, 현재 창을 닫도록 하는 UI 업데이트 관련 로직이 있기 때문에 이 또한 메인스레드에서 실행해야 했다.
따라서 함수 전체를 MainActor를 적용하여 메인스레드에서 적용해 줌으로써 보장하도록 하였다.
(DispatchQueue를 사용해도 되긴 했지만 개인적으로 DispatchQueue를 함수 블록 안에서 사용하면 코드가 길어질수록 가독성도 좋지 않고, 별로 깔끔하지 못하다고 느꼈다.)
✏️ DispatchQueue vs MainActor
| DispatchQueue | MainActor | |
| 개념 | GCD(Grand Central Dispatch)의 작업 대기열 | Swift Concurrency의 actor 타입 |
| UI 업데이트 안정성 | ❌ | 🅾️ |
| 비동기 처리 | 클로저와 콜백 사용 | async/await 키워드 사용 |
| 메인스레드 보장 | 🅾️ | 🅾️ |
| 스레드 안전성 | 직접관리 | 컴파일 타임에 data race 방지 |
| 에러처리 | 클로저와 콜백 사용 | try/catch 사용 |
MainActor를 사용하면 좋은 경우
- Published로 구성된 viewModel 함수인 경우
- async/await을 사용하는 비동기 함수
- UI 상태를 변경하는 함수
- 에러 처리가 중요한 경우
MainActor는 try/catch문을 사용하여 에러처리를 하여 DispatchQueue보다 훨씬 안정적이고 확실하게 에러처리를 할 수 있다는 장점이 있다. 따라서 에러 처리가 중요한 경우와 UI 상태를 변경해야 하는 경우, DispatchQueue의 경우는 비동기처리를 클로저와 콜백을 사용하여 할 수 있는 반면에 MainActor은 Swift의 Concurrency와 호환되기 때문에 async/await를 사용한 코드의 경우 사용하기 유용하다.
또한 Published로 구성된 viewModel 함수인 경우는 @Published 프로퍼티 자체가 직접 UI 업데이트를 수행하도록 한다. 이것 또한 UI 업데이트이기 때문에 메인스레드에서 실행돼야 하는데 viewModel에서 이러한 프로퍼티가 많고 viewModel의 주요 내용이 뷰에 어떤 화면을 보여줄지, 뷰에 대한 업데이트에 관한 내용이라면 ViewModel 전체를 MainActor을 사용하여 메인스레드에서 간편하게 작업을 수행할 수 있도록 할 수 있기 때문이다.
하지만 ViewModel이 뷰를 업데이트하는 것 말고도 다른 역할(데이터 처리, 네트워크 요청 등..)을 한다면 오히려 성능에 문제가 생길 수 있기 때문에 이런 경우는 해당 함수에만 MainActor를 사용하거나, 부분적으로 DispatchQueue를 사용하는 방식으로 구현해야 된다.
DispatchQueue를 사용하면 좋은 경우
- UI 업데이트 시점만 명확하게 메인 스레드에서 실행하고 싶을 때
- iOS 13 이전 버전을 지원해야 하는 경우
- 특정 시간 지연이 필요한 경우
- 성능 최적화가 중요한 경우
Swift Concurrency와 호환되는 MainActor는 Swift 5.5 이상부터 사용이 가능하다. 따라서 이미 오래전부터 개발해 왔거나, iOS 13 이하 버전을 지원해야 하는 경우는 MainActor를 사용했다가 뷰 업데이트가 되지 않을 수도 있기 때문에 DispatchQueue를 사용하는 것이 좋다! (DispatchQueue가 훨씬 오래전부터 사용되고 있었음)
또한 DispatchQueue.main.async는 아래와 같이 지연시간을 포함해서도 사용할 수 있다.
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
if showCompleteToastPopup {
showCompleteToastPopup = false
}
}
이 경우는 지금으로부터 2초 뒤에 블록 안의 작업을 수행하도록 하는 것이다. 일정 시간의 지연시간을 줘야 하는 경우나 , 스플래쉬 화면 등등 되게 자주 쓰인다.
위의 경우 말고도 개인적으로 DispatchQueue를 사용하면 좋을 때는 네트워크 요청을 할 때인 것 같다. 네트워크 요청을 하면서 받아온 응답으로 특정 뷰를 업데이트해 줄 때 뷰에 변경되는 부분만 DispatchQueue를 사용해 주면 깔끔하고 작업도 효과적으로 분배되기 때문이다.
🧐 느낀 점
이때까지 DispatchQueue를 통해 수동으로 메인스레드에서 작업을 수행하도록 지정해 주는 방식으로 개발을 진행해 왔었다. 하지만 이번 블로그를 통해 MainActor에 대해서 공부하고 이때까지 작업했던 프로젝트 코드를 다시 확인해 보니 불필요한 UI 업데이트가 과도하게 일어나고 있었던 것을 확인할 수 있었다..!!!
그래서 다음 프로젝트나 개인 프로젝트를 하게 될 때는 MainActor를 사용해서 불필요한 UI 업데이트를 최소화하며 어떤 방식으로 코드를 구성해야 하는지 조금 더 배운 거 같다.
위 블로그 포스팅은 개인적으로 분석한 것이므로 틀린 점이나 개선할 부분이 있다면 댓글 부탁드립니다!
참고
https://mechanicdong.tistory.com/54
https://developer.apple.com/documentation/dispatch/dispatchqueue
'iOS > Swift' 카테고리의 다른 글
| [swift] Swift Concurrency - Task (1) (0) | 2025.06.18 |
|---|---|
| [iOS] print 대신 Logger를 사용해서 디버깅하기 (1) | 2025.05.11 |
| [iOS/Swift] @escaping closure (1) | 2024.09.26 |
| [iOS] lazy var에 대해 알아보기 (0) | 2024.09.21 |
| [iOS/Swift] Closure (0) | 2024.05.08 |
- Total
- Today
- Yesterday
- Fastlane
- unstructed task
- internal Combine
- Swift Format
- 백준
- ObservableObject
- XCTest
- closure
- combine
- 프로그래머스
- Swift Concurrency
- asyne-let
- UIKit
- Task
- UITest
- CoreData
- foundation models
- 스위프트
- 클로저
- awakeFromNib
- swiftUI
- SnapshotTest
- 코딩테스트
- rxswift
- detached task
- prepareForReuse
- SWIFT
- group tasks
- ios
- Xcode
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |