회사에서 대망의 첫 MR을 작성하게 됐습니다.(Gitlab은 MR이라고 하더군요,,, Github의 PR) 작업 상 크게 어려운 부분은 없었지만, Swift Concurrency를 이용한 코드 작성 중 관련 리뷰를 받게 되어 간단한 실험을 해볼까 합니다.
어떤 상황?
간단히 말하면 Task로 비동기 작업을 생성할 때, 내부 동작은 항상 부모 Context의 메타데이터를 상속 받을까? 입니다.
final class SomeViewController: UIViewController {
// ...
func someFunction() {
Task {
// ⭐️ Task 내부 클로저는 어느 스레드에서 실행?
}
}
}
정확한 상황을 표현하자면, UIKit의 ViewController는 @MainActor로 마킹되어 있기 때문에 항상 메인스레드에서의 동작을 보장하게 되며, 내부 함수 등에서 생성한 Task는 이에 따라 메인스레드에서 동작하게 된다! 입니다.
// UIViewController 공식 문서
@available(iOS 2.0, *)
@MainActor open class UIViewController : UIResponder ... {
하지만 GPT에게 언제나 상속 받는 것을 보장할 수 없다,,,? 와 같은 이야기가 들려와서 혼란이 와버렸고, 어느정도 직접 실험해봐야 신뢰할 수 있는 코드를 작성할 수 있겠다 판단했습니다.
간단한 실험 해보기
첫 번째 실험은 함수 내부에 하나의 Task를 생성 후 메인스레드 여부를 검증하는 것입니다. 모든 실험은 SwiftUI의 View, UIKit의 UIViewController 내부에서 진행했습니다. 예상할 수 있듯, 첫 번째 테스트는 무난히 통과했습니다.
struct ContentView: View {
var body: some View {
Text("실험실")
.onAppear {
test()
}
}
func test() {
Task {
if !Thread.isMainThread {
fatalError("메인스레드가 아니잖아?!")
}
}
}
}
두 번째 실험은 여러 개의 Task를 생성 후 검증하는 것입니다. 이 또한 통과했습니다.
struct ContentView: View {
var body: some View {
Text("실험실")
.onAppear {
test()
}
}
func test() {
(1...10000).forEach { _ in
Task {
if !Thread.isMainThread {
fatalError("메인스레드가 아니잖아?!")
}
}
}
}
}
세 번째 실험은 자식 Task를 생성 후 검증하는 것입니다. 요상하게 생기긴 했지만 결국 가장 마지막 Task까지 메인스레드에서 동작함을 확인할 수 있었습니다.
struct ContentView: View {
var body: some View {
Text("실험실")
.onAppear {
test()
}
}
func test() {
Task {
Task {
Task {
Task {
Task {
if !Thread.isMainThread {
fatalError("메인스레드가 아니잖아?!")
}
}
}
}
}
}
}
}
마지막으로 어떻게든 실패 케이스를 찾고 싶어 이것저것 넣어본 코드입니다. 비동기 작업 여러 개 생성 + 스레드 제어권 양보(yield) 까지 진행해봤지만, 역시 메인스레드의 동작을 보장합니다.
struct ContentView: View {
var body: some View {
Text("실험실")
.onAppear {
test()
}
}
func test() {
Task {
(1...10000).forEach { i in
Task {
try await Task.sleep(for: .seconds(5))
if !Thread.isMainThread {
fatalError("메인스레드가 아니잖아?!")
}
}
}
}
}
}
이로써 혹여나 싶은 마음에 @MainActor 어노테이션을 붙여준다거나, await MainActor.run {} 과 같은 코드들 없이도 메인스레드에서의 동작을 보장할 수 있음을 어느정도 확신할 수 있었습니다.
결론 정리!
- @MainActor 어노테이션이 적용된(SwiftUI View, UIKit UIViewController) 곳 내부에서 기본적으로 생성한 Task는 메인스레드에서의 동작을 보장한다.
- 즉, 부모 Context가 MainActor의 격리되어 있고, 자식 Task가 분리(detached) 혹은 다른 actor에서 실행되지 않는 한 메인스레드에서의 동작을 보장한다.
실패 케이스
마지막으로 실패할 수 밖에 없던 케이스 몇 개를 남겨놓습니다!
struct ContentView: View {
var body: some View {
Text("실험실")
.onAppear {
test()
}
}
func test() {
Task.detached { // detached는 메타데이터를 상속 받지 않음.
if !Thread.isMainThread {
fatalError("메인스레드가 아니잖아?!")
}
}
}
}
struct ContentView: View {
var body: some View {
Text("실험실")
.onAppear {
test()
}
}
func test() {
Task { @TestActor in // 다른 Actor를 마킹했으므로 당연히 메인스레드가 아님.
if !Thread.isMainThread {
fatalError("메인스레드가 아니잖아?!")
}
}
}
}
struct ContentView: View {
@State private var viewModel = ViewModel()
var body: some View {
Text("실험실")
.onAppear {
viewModel.test()
}
}
}
@Observable
final class ViewModel {
func test() {
Task { // Task가 생성된 ViewModel은 따로 actor가 지정되어 있지 않기 때문에 메인스레드 아님.
if !Thread.isMainThread {
fatalError("TestActor는 메인 스레드에서만 실행되어야 합니다.")
}
}
}
}
'내게 필요한 개발 공부' 카테고리의 다른 글
함수와 메서드는 다르다. (4) | 2025.08.01 |
---|---|
멀티태스킹과 멀티프로세싱, 비슷한듯 다른 두 개념 (0) | 2025.03.17 |
안전한 놀이터 샌드박스 알아보기 (2) | 2025.02.26 |
iOS에서 OS 뜯어보기 (0) | 2025.02.26 |
스마트폰의 CPU, AP 알아보기 (1) | 2025.02.24 |