Swift6.2에서 달라진내용과 앱의 응답성을 효율적으로 만드는 내용입니다
Concurrency를 이용해서 UI업데이트의 시점과 큰 리소스가 들어가는 작업을 효율적으로 관리하기위한 방법을 제시합니다
Swift 6.2에는 새 언어 모드가 도입됐습니다

모듈의 모든 타입에 암묵적이 @MainActor주석을 표시
투어에서 배우는 모든 내용은 새 모드와 상관없이 적용됩니다
투어에는 3가지 명소가 등장합니다(WWDC에서 각 목차를 투어에 비유함)
메인 액터의 아름다운 메도우에서 시작하여 SwiftUI가 메인 액터를 애플리케이션 컴파일 시점과 런타임 기본값으로 처리하는 방법을 살펴봅니다
그런 다음 동시성 절벽을 방문해 SwiftUI가 메인 스레드에서 작업을 분리하여 앱 UI 지연을 방지하는 방법과 데이터 레이스 버그 방지 방법을 살펴봅니다
마지막에는 캠프에 도착해 동시 코드와 SwiftUI API 간의 관계에 대해 생각해 봅니다
Main-actor Meadows

View는 @MainActor에 격리되며 구조체를 View에 맞게 조정함
따라서 ColorExtractorView는 @MainActor로 격리됨
이 점선은 추론된 격리를 나타냄
즉 이 주석은 컴파일 시점에 암시적으로 적용됨
@MainActor에 격리된 전체 타입은 모든 멤버도 암시적으로 격리됩니다
View의 요구 사항을 구현하는 본문 프로퍼티뿐만 아니라
@State 변수 같은 선언한 다른 멤버도 포함됩니다
모델의 스키마나 colorCount에 바인딩된 속성 같은 다른 멤버 속성을 참조시 공유된 @MainActor 격리가 액세스 안전을 보장하기 때문에 컴파일러에서 허용
@MainActor는 SwiftUI의 컴파일 타임 기본값입니다
따라서 대부분의 시간을 앱 기능 개발에 집중하고 동시성 문제를 고민할 필요가 없습니다

색상 추출 함수는 비동기식이므로 비동기 컨텍스트로 전환하기 위해 Task로 함수를 호출합니다
뷰 body가 @MainActor로 격리되기 때문에 이 Task에 전달한 클로저도 메인 스레드에서 실행됩니다
[훨씬 더 실용적인 이유]
- AppKit 및 UIKit의 API는 @MainActor에만 격리됨
- SwiftUI는 이러한 프레임워크와 원활하게 상호작용함
@MainActor로 주석을 달지 않아도 됩니다
SwiftUI는 API에 @MainActor 주석을 답니다 SwiftUI가 구현하는 기본 런타임 동작을 반영하기 때문

SwiftUI의 동시성 주석은 런타임 의미론을 표현합니다
Concurrency Cliffs
메인 스레드가 처리해야 할 작업이 너무 많으면 앱 프레임 드롭이나 지연이 발생할 수 있습니다 작업과 구조화된 동시성을 이용해 메인 스레드에서 컴퓨팅 작업을 분리할 수 있습니다

isLoading 속성에 반응하는 scaleEffect를 사용하고 있습니다
애니메이션 값에는 복잡한 수학이 동반됩니다
프레임 단위로 계산하면 비용이 많이 들 수 있습니다
SwiftUI는 이 계산을 백그라운드 스레드에서 수행하여 메인 스레드가 다른 작업에 더 집중하게 합니다

SwiftUI는 선언형입니다
UIView와 달리 View 프로토콜을 준수하는 구조체는 메모리에서 고정된 위치를 차지하는 객체가 아닙니다
런타임에 SwiftUI는 View에 대한 별도의 표현을 만듭니다

이 표현은 다양한 최적화 기회를 제공합니다
중요한 점은 뷰 표현의 일부를 백그라운드 스레드에서 평가하는 것
SwiftUI는 사용자를 대신해 많은 계산을 수행할 때 이 기법을 사용함
예를들어 대부분의 경우에 고빈도 기하 계산이 동반됩니다
Shape 프로토콜이 대표적인 예입니다

웨지 모양이 움직이면 작성한 path 메서드는 백그라운드 스레드에서 호출을 받습니다
뷰 수정자 visualEffect는 대상 뷰(즉 텍스트)에 적용될 효과를 정의하는 클로저를 허용합니다
시각 효과는 화려하지만 렌더링 비용은 비쌉니다 SwiftUI는 백그라운드 스레드에서 이 클로저를 호출함

Layout 프로토콜은 메인 스레드 에서 요구 사항 메서드를 호출
visualEffect처럼 onGeometryChange의 최초 인수는 백그라운드 스레드에서도 호출될 수 있는 클로저입니다
SwiftUI는 Sendable 주석을 사용하여 이 런타임 동작 또는 의미론을 컴파일러와 사용자에게 전달합니다
여기서도 SwiftUI의 동시성 주석은 런타임 의미론을 표현합니다
코드를 별도의 스레드에서 실행하면 메인 스레드가 해제되어 앱의 응답 속도가 개선됨

Sendable 키워드는 @MainActor에서 데이터를 공유할 때 발생하는 데이터 레이스 조건을 알려 줍니다
Sendable은 절벽길 경고 표지판과 같습니다
Swift는 코드 내의 레이스 조건을 안정적으로 탐지하고 컴파일러 오류로 이를 알립니다
데이터 레이스 조건을 방지하는 최고의 전략은 동시 실행되는 작업 간에 데이터를 공유하지 않는 것입니다
샌더블함수 밖에서 변수에 접근해야할때를 알아보자

SchemeContentView에서 상태 pulse 가 이 visualEffect에 필요함
Swift는 데이터 레이스 조건이 있다고 에러뜸
pulse 변수는 self.pulse의 약자입니다 @MainActor 격리 변수를 전송 가능 클로저에서 공유할 때 자주 발생하는 상황임
Self는 뷰입니다 메인 액터에 격리되어 있습니다


최종 목표는 전송 가능 클로저에서 pulse 변수에 접근하는 것입니다 이렇게 하려면 두 가지 일이 일어나야 합니다
첫째, 격리된 속성읽기를 방지해야함

값 자체가 메인 액터에서 배경 스레드 코드 영역으로 넘어가야 합니다
Swift에서는 이를 변수 자체의 백그라운드 스레드 전송이라 부릅니다
이렇게 하려면 self의 유형이 Sendable이어야 합니다

self가 적절한 위치에 있으니 이 비격리 영역에서 관련 속성 pulse를 읽겠습니다
컴파일러는 속성 pulse가 어떤 액터에도 격리되지 않은 경우에만 이를 허용합니다

코드를 다시 살펴보면 self는 View이므로 @MainActor로 보호됩니다
그래서 컴파일러는 이를 Sendable로 간주합니다
그래서 Swift는 이 self 참조가 @MainActor 격리 영역에서 Sendable 클로저로 넘어가도록 허용함

컴파일러는 self 전달은 가능하지만 @MainActor에 격리된 속성 pulse 접근은 안전하지 않다고 말합니다
컴파일 오류를 수정하려면 View를 참조하여 속성 읽기를 방지해야 합니다
작성 중인 visualEffect에는 뷰의 전체 값이 필요 없습니다
pulse가 true인지 false인지 알기만 하면 됩니다
클로저의 캡처 목록에 있는 pulse 변수의 복사본을 만들어 대신 참조할 수 있습니다
이렇게 하면 self가 클로저에 전송되지 않습니다
Bool은 단순 값 유형이므로 전송 가능 pulse의 복사본을 전송함

이 복사본은 이 함수 범위 내에서만 존재하므로 접근해도 데이터 레이스 문제가 발생하지 않습니다

이 작업을 위한 두번째 다른 전략은 우리가 읽는 모든 내용이 격리되지 않게 하는 것입니다
Code Camp
왜 Button은 비동기 클로저를 받지 않을까요?
동기식 업데이트는 좋은 사용자 경험의 필수 요소입니다
앱에 장기 실행 작업이 있으며 사용자가 결과물을 기다려야 할 때는 더 중요하죠
SwiftUI의 액션 콜백은 제 로딩 상태 같은 UI 업데이트 설정에 필요한 동기식 클로저를 허용함

반면 비동기 함수는 특히 애니메이션을 다룰 때 추가 고려사항이 있습니다
UI 프레임워크로서 프레임마다 부드러운 상호작용을 구현하려면 SwiftUI는 기기가 요구하는 특정 화면 재생 빈도를 고려해야 합니다
스크롤 같은 연속 제스처에 반응하는 코드를 작성할 때는 이 맥락을 반드시 고려해야 합니다 코드를 타임라인에 넣어 보겠습니다

녹색 삼각형을 이용해 SwiftUI가 onScrollVisibilityChange를 호출하는 시점을 표시하겠습니다
파란색 원은 상태 변환을 통해 애니메이션을 트리거하는 순간을 표시합니다
이 설정에서는 변환이 제스처 콜백과 동일한 프레임에서 발생하느냐에 따라 시각적으로 완전히 달라질 수 있습니다
이제 애니메이션 변환 전에 비동기 작업을 추가하려 합니다 비동기 작업이 시작되는 시점을 주황 선으로 표시하고 대기합니다

Task는 async 함수를 인수로 허용합니다
await를 목격하면 컴파일러는 비동기 함수를 두 개로 나눕니다

첫 번째 부분을 실행하면 Swift 런타임은 함수를 일시 중지하고 CPU에서 다른 작업을 수행할 수 있습니다
이 과정은 임의의 시간 동안 지속될 수 있습니다
이후 런타임은 원래 비동기 함수로 돌아가 두 번째 부분을 실행합니다

함수에서 await가 발생할 때마다 반복됨

타임라인으로 돌아가면 이 중단은 작업 완료가 기기에서 지정된 새로 고침 마감 시간 이후에도 재개되지 않을 수 있음을 의미합니다
그래서 애니메이션이 버벅거리는 것처럼 보입니다
따라서 비동기 함수 변형은 목표 달성에 도움이 되지 않죠
애니메이션 같은 시간에 민감한 로직은 SwiftUI의 입력과 출력이 동기화되어야 합니다
Observable 속성의 동기화된 변환과 동기화된 콜백은 프레임워크와의 상호작용에서 가장 자연스러운 타입입니다

앱이 많은 동시 작업을 수행한다면 UI 코드와 비 UI 코드 사이의 경계를 찾아보세요
비동기 작업의 로직을 뷰 로직에서 분리하세요
상태 일부를 브리지로 사용할 수 있습니다 상태는 UI 코드와 비동기 코드를 분리합니다
비동기 작업이 완료되면 상태에 동기식 변경을 수행하여 UI가 이 변경에 반응해 업데이트되게 합니다
이런 방식으로 UI 로직은 대부분 동기식이 됩니다
또한, 비동기 코드에 대한 테스트 작성이 더 쉬워집니다 UI 로직과 독립적으로 작동하기 때문이죠

뷰는 여전히 Task를 이용해 비동기 컨텍스트로 전환 가능하죠
하지만 코드를 최대한 간단하게 유지해야 합니다 UI 이벤트를 모델에 알리는 역할을 합니다
UI 코드 중 시간에 민감한 변경이 많이 필요한 부분과 장기 실행되는 비동기 로직 간의 경계를 찾으면 앱 구조를 개선할 수 있습니다
뷰의 동기성과 반응성 유지에도 도움이 됩니다 UI가 아닌 코드를 잘 정리하는 것도 중요합니다
Swift 6.2는 우수한 기본 액터 격리 설정을 제공합니다 기존 앱이 있다면 시도해 보세요
대부분의 @MainActor 주석을 삭제할 수 있습니다
Mutex는 클래스를 전송 가능으로 만드는 중요 도구입니다
'iyOmSd > Title: Swift' 카테고리의 다른 글
| [Swift] Screen Time API(DeviceActivity) (0) | 2025.10.27 |
|---|---|
| [Swift] Screen Time API(FamilyControls, ManagedSettings) (0) | 2025.09.26 |
| [Swift] WWDC25 Wake up to the AlarmKit API (3) | 2025.07.22 |
| [Swift] Vision OCR 인식 (0) | 2025.06.25 |
| [Swift] Task Cancel (0) | 2025.05.23 |