
이번에 SwiftUI를 본격적으로 사용하면서 개발한 PR에 대해 코드리뷰를 진행하면서
await MainActor.run 구문이 있는 곳과 없는 곳이 있더라구요
문득 그러면 @Published 프로퍼티를 set 할 때 항상 써야하나? 라는 생각이 들어서
GPT를 토대로 좀 확실하게 정리된 내용을 블로그로 남기고자 합니다.
GPT 채팅내용 기반 GPT가 정리한 내용입니다.
SwiftUI에서 ObservableObject로 상태를 관리할 때
비동기 로직(async/await)과 Combine(Publisher)를 어떻게 MainActor와 함께 안전하게 다루는지
그리고 성능을 위해 Actor context를 어떻게 분리해야 하는지
실전 패턴을 예시 코드와 함께 정리합니다.
@MainActor를 붙이는 이유receive(on:)는 꼭 필요할까?Task {} vs Task.detached {} 언제 쓰나?await suspension과 Actor context 복귀nonisolated는 언제 쓰는가?@MainActor를 붙이는 이유SwiftUI는 @Published로 상태 변화를 감지하고 objectWillChange 퍼블리셔를 통해 View를 업데이트합니다.@MainActor를 붙여서 모든 상태 변경과 관련 메서드가 메인 스레드에서 안전하게 실행되도록 보장합니다.@MainActor
final class MyViewModel: ObservableObject {
@Published var text: String = ""
func updateText(_ value: String) {
text = value
}
}
func bindPublisher(_ publisher: AnyPublisher<String, Never>) {
publisher
.sink { [weak self] value in
self?.text = value // ✅ MainActor isolation 덕분에 안전
}
.store(in: &cancellables)
}
| 상황 | 추천 |
|---|---|
| 네트워크 호출, 간단한 I/O | Task {} |
| CPU-heavy 연산, 파일 I/O, DB 쓰기 등 무거운 작업 | Task.detached {} |
✔️ Task {}
Task {
let data = await api.fetch()
self.text = data // MainActor isolation 보장
}
✔️ Task.detached {}
Task.detached {
let result = heavyCalculation()
await MainActor.run {
self.text = result
}
}
@MainActor
func fetchData() async {
let value = await fetchRemote() // 백그라운드 I/O
self.text = value // MainActor로 복귀
}
@MainActor
class Example {
var counter: Int = 0
nonisolated func getStaticInfo() -> String {
return "Swift Concurrency Rules"
}
}
| 개념 | 설명 | 팁 |
|---|---|---|
| @MainActor ViewModel | 상태 변경은 항상 메인 스레드에서 안전 | ViewModel 기본값으로 쓰기 |
| Publisher 수신 | ViewModel isolation 따르므로 receive(on:) 필요 없음 | |
| Task {} | 상위 Actor 계승, 간단한 I/O | |
| Task.detached {} | Actor context 분리, 무거운 작업 | await MainActor.run으로 복귀 |
| suspension | I/O는 background → 복귀는 Actor isolation | |
| nonisolated | 상태 변경 없는 getter 전용 |
@MainActor
final class LogDailyViewModel: ObservableObject {
@Published var text: String = ""
let api: SomeAPIProtocol
init(api: SomeAPIProtocol) {
self.api = api
}
func sendNetworkRequest() {
Task {
let data = await api.fetch() // 간단한 네트워크 I/O
self.text = data
}
}
func saveFileHeavyTask() {
Task.detached {
let result = try await HeavyFileIO().process()
await MainActor.run {
self.text = result
}
}
}
func bindPublisher(_ publisher: AnyPublisher<String, Never>) {
publisher
.sink { [weak self] value in
self?.text = value // MainActor isolation 덕분에 안전
}
.store(in: &cancellables)
}
nonisolated func staticInfo() -> String {
"This is static, no state mutation"
}
}
Swift Concurrency에서 Actor isolation을 정확히 이해하면
⚡️ UI 상태는 안전하게 보호하고
⚡️ 무거운 연산은 백그라운드로 보내 성능도 잡을 수 있습니다.
ViewModel을 @MainActor로 선언하고
Task와 Task.detached, await MainActor.run을 상황에 맞게 조합하세요!
이렇게 하면 스레드 안전과 효율을 한 번에 챙길 수 있습니다.