Swift - MainActor

이재원·2025년 1월 16일
0

Swift

목록 보기
16/17
post-thumbnail

Swift는 동시성(Concurency)을 도입하면서 비동기 작업을 효율적으로 처리할 수 있는 강력한 도구들을 제공합니다. 하지만 UI 업데이트와 같이 항상 메인 스레드에서 실행되어야 하는 작업도 여전히 존재합니다. 이런 작업에서 동시성 환경에서 발생할 수 있는 문제를 안전하고 간단하게 해결하기 위해 Swift는 @MainActor라는 속성을 제공합니다.

이번 글에서는 @MainActor가 무엇인지, 왜 필요한지, 그리고 어떻게 사용하는지 자세히 알아보겠습니다.

@MainActor란

@globalActor
final actor MainActor

@MainActor는 특정 코드(함수, 메서드, 프로퍼티, 클래스 등)가 항상 메인 스레드에서 실행되도록 보장하는 Swift 속성입니다.

UI와 관련된 작업은 반드시 메인 스레드에서 실행되어야 합니다. 기존에는 DispatchQueue.main.async를 통해 직접 메인 스레드에서 실행되도록 명시적으로 작성해야 했지만, @MainActor를 사용하면 더 선언적이고 간결하게 메인 스레드 작업을 지정할 수 있습니다.

@MainActor가 필요한 이유

동시성 모델에서 작업이 여러 스레드에서 실행되다 보면, UI와 관련된 작업이 메인 스레드가 아닌 다른 스레드에서 실행될 위험이 있습니다. 이 경우 앱이 비정상적으로 동작하거나 크래시가 발생할 수 있습니다.

기존 방식으로 아래와 같이 DispatchQueue.main.async를 사용하여 명시적으로 처리해야 했습니다.

DispatchQueue.main.async {
	// UI 업데이트
	self.label.text = "Updated Text"
}

이 방식은 메인 스레드에서 실행된다는 점을 명확히 하지만, 반복적으로 사용해야 하거나 비동기 코드가 복잡해질수록 관리하기 어려워집니다.

@MainActor는 이런 문제를 해결하기 위해 등장했습니다. 이 속성을 사용하면 메인 스레드에서 작업이 실행된다는 것을 컴파일러가 보장하며, 코드를 더 선언적이고 안전하게 작성할 수 있습니다.

@MainActor의 사용 방법

@MainActor를 함수나 메서드에 적용하면, 해당 함수는 항상 메인 스레드에서 실행됩니다. @MainActor가 적용된 함수는 비동기 컨텍스트에서 호출될 때 반드시 await 키워드를 사용해야 합니다.

@MainActor
func updateUI() {
	// 항상 메인 스레드에서 실행
	label.text = "Updated Text"
}

Task {
	await updateUI()
}

타입(클래스, 구조체, 열거형)에 @MainActor를 적용하면, 해당 타입의 모든 메서드와 프로퍼티가 메인 스레드에서 실행됩니다.

@MainActor
class ViewModel {
    var text: String = ""
    
    func updateText() {
        text = "Updated Text"
        print("Text updated on main thread")
    }
}

Task {
    let viewModel = ViewModel()
    await viewModel.updateText()
}

만약 모든 코드에 @MainActor 를 적용하기 어려운 경우, MainActor.run을 사용하여 특정 코드 블록만 메인 스레드에서 실행되도록 지정할 수 있습니다.

Task {
    await MainActor.run {
        // 메인 스레드에서 실행되는 코드
        label.text = "Updated Text"
    }
}

@MainActor의 실제 활용 예제

UI 업데이트를 안전하게 처리하기

@MainActor는 UI 업데이트가 포함된 코드에서 특히 유용합니다.

@MainActor
class ViewModel: ObservableObject {
    @Published var text: String = ""
    
    func fetchAndUpdateText() async {
        let fetchedText = await fetchTextFromServer()
        self.text = fetchedText // 항상 메인 스레드에서 실행
    }
    
    private func fetchTextFromServer() async -> String {
        // 서버에서 데이터 가져오기
        return "Fetched Text"
    }
}

Swift에서 @Published 프로퍼티를 사용하면 UI의 업데이트가 항상 메인 스레드에서 안전하게 수행됩니다.

비동기 작업 이후 UI 업데이트

비동기 작업을 처리한 후 UI를 업데이트 할 때도 @MainActor는 유용합니다.

func fetchData() async {
    let data = await performBackgroundTask()
    
    await MainActor.run {
        // 메인 스레드에서 UI 업데이트
        label.text = "Data: \(data)"
    }
}

func performBackgroundTask() async -> String {
    // 백그라운드 작업 수행
    return "Some Data"
}

SwiftUI와 UIKit의 View에서 MainAcotr 적용

기본적으로 SwiftUI와 UIKit은 UI 작업이 항상 메인 스레드에서 실행되어야 한다는 규칙을 강제하고 있습니다. 따라서 평소에 UI 작업을 할 때는 굳이 명시하지 않아도 되지만, 만약 UI 작업이 백그라운드 스레드에서 실행된다면 명시적으로 메인 스레드로 전환해야 합니다. 그렇지 않으면 런타임 오류나 예상치 못한 동작이 발생할 수 있습니다.

UIView에서의 UI 업데이트

UIKit에서 UI 업데이트는 기본적으로 메인 스레드에서 처리됩니다.

class CustomView: UIView {
    func updateLabelText(_ text: String) {
        self.label.text = text
    }
}

이 코드는 메인 스레드에서 호출된 경우 자동으로 메인 스레드에서 실행됩니다. 하지만 백그라운드 스레드에서 호출된다면 문제가 발생합니다.

let view = CustomView()
DispatchQueue.global().async {
    view.updateLabelText("New Text") // 문제 발생 가능: 백그라운드에서 UI 업데이트 시도
}

이런 경우 메인 스레드로 전환해야 합니다.

DispatchQueue.global().async {
    let result = performBackgroundTask()
    DispatchQueue.main.async {
        view.updateLabelText(result) // 메인 스레드에서 실행
    }
}

이렇게 작성할 경우 신경써서 메인 스레드로 전환해야 하는 단점이 있습니다. 이 경우 MainActor를 함수에 명시하면 더 간결하고 안전해집니다.

class CustomView: UIView {
    @MainActor
    func updateLabelText(_ text: String) {
        self.label.text = text
    }
}

let view = CustomView()
Task {
    await view.updateLabelText("Updated Text")
}

UIKit과 SwiftUI의 차이

UIKit

메인 스레드에서 동작해야 한다는 규칙이 암묵적으로 강제되며, 개발자가 백그라운드에서 작업할 경우 직접 메인 스레드로 전환해야 합니다. 일반적으로는 MainActor를 명시할 필요가 없습니다. 뷰와 뷰컨트롤러가 메인 스레드에서 동작하도록 설계되어 있으므로 UI 업데이트가 문제 없이 작동하기 때문입니다.

SwiftUI

SwiftUI는 백그라운드 작업 중에 UI 업데이트가 필요하면, UI 업데이트가 자동으로 메인 스레드에서 처리됩니다. 이는 SwiftUI가 내부적으로 데이터 바인딩(State, Binding, ObservedObject, Published)과 관련된 UI 업데이트를 항상 메인 스레드에서 실행하도록 설계되어 있기 때문입니다.

import SwiftUI

class ViewModel: ObservableObject {
    @Published var text: String = ""
    
    func fetchData() {
        DispatchQueue.global().async {
            let result = "Fetched Data"
            self.text = result // 자동으로 메인 스레드에서 처리됨
        }
    }
}

주의할 점은 @Published 와 같은 속성이 아닌, UI와 직접적인 상호작용이 필요하다면 명시적으로 메인 스레드로 전환해야 합니다.

DispatchQueue.global().async {
    let result = "Data"
    DispatchQueue.main.async {
        // SwiftUI View 외부에서 UI 업데이트
        someUILabel.text = result
    }
}

또한 아주 가끔씩 복잡한 동작이나 백그라운드 동시성 작업 문제가 발생한다면 SwiftUI에서 자동으로 처리하지 못할 수도 있습니다. 이때는 @MainActorMainActor.run 을 사용해 명시적으로 메인 스레드에서 실행되도록 지정해야 합니다.

마무리

@MainActor 는 Swift 동시성 모델에서 메인 스레드 작업을 선언적으로 처리할 수 있는 강력한 도구입니다. 기존의 DispatchQueue.main.async 를 사용하는 방식보다 안전하고 간결하며, 특히 UI 업데이트와 관련된 코드에서 유용하게 사용할 수 있습니다.

따라서 동시성과 메인 스레드 작업이 포함된 코드를 작성할 때, @MainActor 를 사용하면 코드의 안정성과 가독성을 크게 향상시킬 수 있습니다.

참고자료
https://developer.apple.com/documentation/swift/mainactor
https://green1229.tistory.com/343
https://www.swiftbysundell.com/articles/the-main-actor-attribute/

profile
20학번 새내기^^(였음..)

0개의 댓글

관련 채용 정보