@MainActor — Swift 비동기 코드에서의 개념, 적용 및 특징

저자: IT Sectr 게시일: 2026-03-19 읽는 시간: 8 분

@MainActor는 메인 스레드에서 코드 실행을 보장하는 Swift 언어의 전역 액터입니다. Apple Developer, 2024에 따르면, @MainActor는 UI 작업 시 메인 스레드로의 전환을 자동화하여 개발자가 수동으로 DispatchQueue.main.async를 호출할 필요를 없앱니다. 이 어노테이션은 Swift 5.5에서 async/await 시스템과 함께 등장했습니다.

핵심 사항

  • @MainActor — 메인 스레드에서의 실행을 보장하는 Swift의 전역 액터입니다.
  • async/await 시스템 — @MainActor가 구축된 기반입니다.
  • 클래스 어노테이션은 모든 메서드를 자동으로 메인 스레드에서 실행합니다.
  • DispatchQueue.main과 달리, @MainActor는 컴파일러 수준에서 스레드를 확인합니다.
  • UI 업데이트 — iOS 개발에서 @MainActor의 주요 적용 영역입니다.

@MainActor란?

@MainActor는 액터의 속성과 애플리케이션 메인 스레드에서의 실행 보장을 결합한 Swift의 전역 액터입니다. Swift 5.5에서 async/await 및 구조적 동시성과 함께 도입된 Swift 동시성 시스템의 일부입니다. 이 어노테이션을 사용하면 개발자가 수동으로 스레드를 전환하는 것에 대해 걱정할 필요가 없으며 UI 버그 수가 줄어듭니다.

Swift Concurrency에서의 정의와 위치

Swift에서 액터는 자신의 상태를 격리하고 하나의 스레드만 수정할 수 있도록 보장하는 참조 유형입니다. @MainActor는 실행자가 메인 스레드인 특별한 전역 액터입니다. @MainActor로 표시된 모든 코드는 백그라운드 작업에서 호출된 경우에도 메인 스레드에서 실행됩니다.

@MainActor 이전에는 개발자가 DispatchQueue.main.async를 사용하여 수동으로 메인 스레드로 전환했습니다. 이는 빈번한 오류의 원인이었으며, 개발자가 전환을 잊어버려 메인 스레드가 아닌 곳에서 UI를 업데이트하여 충돌이 발생했습니다. @MainActor는 이 문제를 유형 시스템 수준에서 해결합니다.

생성 배경

iOS 애플리케이션의 대부분 버그 원인은 UI 안전성 문제, 즉 백그라운드 스레드에서 인터페이스를 업데이트하는 것입니다. Apple은 @MainActor를 Swift Concurrency에 내장하여 메인 스레드로의 전환을 자동화하고 컴파일러가 확인할 수 있게 하여 런타임 오류의 전체 클래스를 제거했습니다.

@MainActor의 작동 방식

@MainActor의 작동 원리는 Swift Concurrency 실행 시스템을 기반으로 합니다. 스레드가 @MainActor로 표시된 함수를 호출하면 스케줄러가 현재 실행기에서 이를 일시 중단하고 메인 스레드에서 재개합니다. 컴파일러는 호출 경계를 추적하고 안전성을 보장합니다.

메인 스레드 실행기

@MainActor의 실행은 MainActor.shared에 의해 관리됩니다. 이는 애플리케이션의 메인 스레드와 연결된 실행기입니다. 비동기 함수가 @MainActor로 표시된 경우, 원래 작업이 시작된 스레드와 관계없이 항상 이 실행기에서 재개됩니다.

swift
import SwiftUI

class ViewModel: ObservableObject {
    @Published var items: [String] = []

    @MainActor
    func loadData() async {
        let result = await fetchRemoteData()
        items = result  // 안전하게, MainActor가 메인 스레드를 보장합니다
    }
}

액터 컨텍스트 상속

@MainActor로 표시된 함수가 다른 비동기 함수를 호출하는 경우, 기본적으로 액터 컨텍스트를 상속합니다. 즉, 달리 지정되지 않는 한 모든 중첩 호출도 메인 스레드에서 실행됩니다. 컴파일러는 이를 추적하고 일관되지 않은 클로저를 전달하려고 하면 오류를 발생시킵니다.

@MainActor vs DispatchQueue.main

@MainActor와 DispatchQueue.main의 비교는 두 가지 모두 메인 스레드에서 코드를 실행하는 동일한 작업을 해결하지만, 새로운 메커니즘이 왜 더 안전하고 편리한지 이해하는 데 도움이 됩니다.

유형 수준의 안전성

@MainActor는 컴파일러 수준에서의 검사입니다. 안전하지 않은 컨텍스트에서 @MainActor 함수를 호출하려고 하면 컴파일러가 경고 또는 오류를 발생시킵니다. DispatchQueue.main.async는 런타임 호출로, 코드는 컴파일되지만 백그라운드 스레드에서 UI를 업데이트하려고 하면 런타임에 충돌할 수 있습니다.

성능 및 오버헤드

DispatchQueue.main.async는 지연되어 실행될 수 있는 블록을 대기열에 추가합니다. @MainActor는 async/await와 함께 불필요한 클로저를 생성하지 않고 직접 실행기를 전환합니다. 이는 오버헤드를 줄이고 실행 시간을 더 예측 가능하게 만듭니다.

swift
// 이전 방식
DispatchQueue.main.async {
    self.updateUI()
}

// @MainActor를 사용한 새로운 방식
@MainActor
func updateUI() {
    // 메인 스레드에서 실행
    self.label.text = "업데이트됨"
}
기준@MainActorDispatchQueue.main
검사컴파일러런타임
구문어노테이션 (선언형)호출 (명령형)
오버헤드낮음 (실행기 전환)중간 (클로저 + 대기열)
테스트 용이성높음 (MainActor.shared 교체 가능)낮음 (모킹 어려움)

iOS 프로젝트에서 @MainActor 사용

실제 iOS 프로젝트에서 @MainActor는 ViewModel 계층, SwiftUI 뷰 및 UIKit 컨트롤러에서 사용됩니다. 어노테이션은 개별 메서드와 전체 유형 모두에 적용할 수 있습니다.

클래스 또는 구조체 어노테이션

클래스를 @MainActor로 표시하면 모든 메서드와 속성이 메인 스레드에서만 접근 가능함을 보장합니다. 이는 SwiftUI 뷰와 ObservableObject 클래스에 특히 편리합니다. class 앞에 @MainActor를 추가하기만 하면 모든 @Published 속성이 안전하게 업데이트됩니다.

swift
@MainActor
final class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false

    func fetchUsers() async {
        isLoading = true
        users = await api.getUsers()
        isLoading = false
    }
}

레거시 코드 래핑

스레드 전환이 수동이었던 기존 UIKit 코드로 작업할 때 명시적 전환을 위해 MainActor.run을 사용할 수 있습니다. 이는 전체 코드베이스를 다시 작성하지 않고 Swift Concurrency로 점진적으로 마이그레이션하는 데 편리합니다.

swift
await MainActor.run {
    self.tableView.reloadData()
}

@MainActor의 제한 사항

모든 장점에도 불구하고 @MainActor에는 애플리케이션 아키텍처를 설계할 때 고려해야 할 몇 가지 제한 사항이 있습니다. 적용 가능성의 경계를 이해하면 잘못된 사용을 방지하는 데 도움이 됩니다.

집중 사용 시 성능

전체 호출 체인이 @MainActor로 표시된 경우 모든 무거운 작업이 메인 스레드에서 수행되어 UI가 멈춥니다. UI 계층만 @MainActor로 표시하고 비즈니스 로직과 네트워크 요청은 백그라운드 액터나 전역 실행기에 두는 것이 좋습니다.

일부 API와의 비호환성

오래된 콜백 기반 API(예: async/await 없는 URLSession)는 액터 컨텍스트를 지원하지 않습니다. 통합을 위해서는 CheckedContinuation을 사용한 래퍼가 필요합니다. 또한 @MainActor는 performSelector, target-action 및 기타 비동기식이 아닌 UIKit 패턴과 호환되지 않습니다.

멀티스레드 디버깅

@MainActor를 사용한 애플리케이션 디버깅 시 컴파일러가 런타임 대신 빌드 시간에 많은 경합 상태를 방지하므로 레이스 컨디션을 재현하기 어렵습니다. 그러나 이는 잘못된 안전감을 만들 수 있습니다. 공유 가변 객체(NSCache 또는 공유 전역 변수 등)가 @MainActor로 표시되지 않고 명시적 동기화 없이 사용되는 경우 잘못된 작업이 여전히 발생할 수 있습니다.

@MainActor 테스트

@MainActor는 테스트에서 수동 스레드 전환의 필요성을 없애 UI 로직 테스트를 크게 단순화합니다. 그러나 단위 테스트와 UI 테스트를 작성할 때 고려해야 할 특이사항이 있습니다.

MainActor를 사용한 단위 테스트

XCTest에서 테스트 환경은 자동으로 메인 스레드 실행기를 설정합니다. 테스트 메서드가 메인 스레드에서 실행될 때 @MainActor 함수 호출에 추가 설정이 필요하지 않으며 동일한 컨텍스트에서 실행됩니다. 백그라운드 시나리오를 테스트하려면 명시적 우선순위와 실행기로 Task 내에서 MainActor.run을 사용하고, 백그라운드에서 호출할 때 코드가 올바르게 작동하는지 별도로 확인합니다.

일반적인 접근 방식은 @MainActor를 사용한 ViewModel 테스트로, 비동기 작업 후 @Published 속성이 올바르게 업데이트되는지 확인합니다. 액터 컨텍스트 상속 덕분에 테스트 내에서 await를 호출하면 추가 DispatchQueue 보장이나 수동 컨텍스트 전환 없이 메인 스레드에서 실행이 보장되어 테스트 작성이 간소화됩니다.

리팩토링 시 격리 확인

기존 코드를 Swift Concurrency로 리팩토링할 때 컴파일러를 통해 @MainActor 격리를 확인합니다. @MainActor 컨텍스트에서 @MainActor 없는 동기 메서드를 호출하면 오류로 표시됩니다. 이 속성은 프로젝트를 점진적으로 async/await로 마이그레이션하는 데 사용됩니다. ViewModel 계층을 @MainActor로 표시하면 컴파일러가 백그라운드 액터로 이동해야 하는 모든 안전하지 않은 호출을 강조 표시합니다.

모킹 및 액터 컨텍스트

@MainActor 종속성을 위한 모킹을 만들 때 반환 유형이 있는 비동기 함수를 선언하는 async 메서드가 있는 프로토콜을 사용합니다. 이를 통해 액터 격리를 깨지 않고 네트워크 서비스, 데이터베이스 및 기타 외부 종속성을 교체할 수 있습니다. 컴파일러는 모의 객체가 모든 격리 요구사항을 구현하는지 확인하여 백그라운드 테스트 스레드에서 @MainActor 코드에 우발적으로 접근하는 것을 방지합니다.

비동기 작업 대기

@MainActor 코드를 동기적으로 테스트할 때 XCTestExpectation을 사용하여 비동기 작업 완료를 기다립니다. 테스트에서 기대치를 설정하고 메인 스레드에서 실행되는 클로저 내에서 fulfillment를 호출합니다. 테스트가 무기한 중단되면 메인 스레드에서 호출이 발생하지 않을 가능성이 높으며 액터 격리를 확인해야 합니다. 실행 컨텍스트를 디버깅하려면 테스트 코드 내에 Thread.isMainThread 검사를 추가하는 것이 유용합니다.

자주 묻는 질문

클래스 전체를 @MainActor로 표시해야 하나요?

아니요, UI를 업데이트하는 메서드만 표시하면 충분합니다. 그러나 클래스에 이러한 메서드가 여러 개 있는 경우 전체 클래스에 @MainActor를 추가하는 것이 더 간단합니다. 이렇게 하면 모든 멤버가 메인 스레드에서 실행되고 코드 유지 관리가 간소화됩니다.

@MainActor와 @globalActor의 차이점은 무엇인가요?

@MainActor는 메인 스레드에 연결된 전역 액터의 특정 인스턴스입니다. @globalActor는 자체 전역 액터를 생성하기 위한 프로토콜입니다. 예를 들어 프로젝트 아키텍처에서 필요한 경우 백그라운드 스레드에서 코드를 실행하는 @BackgroundActor를 만들 수 있습니다.

@MainActor를 async/await 없이 사용할 수 있나요?

네, @MainActor를 사용한 동기 함수도 메인 스레드에서 실행됩니다. 그러나 @MainActor의 주요 가치는 async/await와 함께 사용될 때 드러나며, 비동기 함수가 DispatchQueue.main을 통한 수동 전환 없이 자동으로 메인 스레드에서 재개됩니다.

@MainActor 작업을 취소하는 방법은?

Task.cancel()은 일반 작업과 동일하게 @MainActor 작업에서도 작동합니다. @MainActor 작업은 Task.isCancelled를 확인하거나 CancellationError를 던질 수 있습니다. 취소 시 메인 스레드는 차단되지 않으며 작업은 가장 가까운 일시 중단 지점에서 실행을 중지합니다.

@MainActor를 백그라운드 스레드에서 호출하면 어떻게 되나요?

컴파일러가 안전성을 보장합니다. 백그라운드 컨텍스트에서 @MainActor 함수를 호출하면 컴파일러가 오류를 지적합니다. 비동기 호출의 경우 호출 코드를 await로 표시하기만 하면 실행기가 메인 스레드로 전환됩니다. 동기 호출의 경우 MainActor.run을 통한 명시적 전환이 필요합니다.

요약

  • @MainActor — 메인 스레드에서 실행을 보장하는 Swift의 전역 액터입니다.
  • 컴파일러 검사는 UI 안전성 오류의 전체 클래스를 제거합니다.
  • 전체 클래스 어노테이션은 모든 메서드를 자동으로 메인 스레드에서 실행합니다.
  • MainActor.run — 레거시 코드 및 동기 컨텍스트를 위한 명시적 전환입니다.
  • DispatchQueue.main과 달리, @MainActor는 클로저를 생성하지 않고 실행기 전환을 사용합니다.
  • 무거운 계산은 UI 멈춤을 방지하기 위해 @MainActor 아래에서 수행하지 않아야 합니다.
  • 액터 컨텍스트 상속은 비동기 호출 체인을 단순화하고 코드를 일관되고 예측 가능하며 UI에 안전하게 만듭니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기