
Swift로 iOS 앱을 개발하던 중, UICollectionViewDiffableDataSource를 사용하려고 하니 다음과 같은 에러가 발생했습니다.
Main actor-isolated conformance of 'BookSearchItem' to 'Hashable'
cannot satisfy conformance requirement for a 'Sendable' type parameter 'ItemIdentifierType'
Requirement specified as 'ItemIdentifierType' : 'Hashable'
[with ItemIdentifierType = BookSearchItem]
// BookSearchItem.swift
enum BookSearchItem: Hashable, Sendable {
case recentBook(Book)
case searchBook(Book)
case empty
}
enum BookSearchSection: Int, CaseIterable, Hashable, Sendable {
case recent
case search
}
// BookSearchViewController.swift
final class BookSearchViewController: UIViewController {
private var dataSource: UICollectionViewDiffableDataSource<BookSearchSection, BookSearchItem>!
// ❌ 여기서 에러 발생!
}
BookSearchItem: ✅ Hashable, ✅ SendableBookSearchSection: ✅ Hashable, ✅ SendableBook: ✅ Hashable, ✅ Sendable모든 타입이 요구사항을 만족하는데 왜 에러가 날까?
처음에는 "UIKit을 import한 파일에 있어서 그런가?"라고 생각했습니다.
// BookSearchModels.swift (새 파일)
import Foundation // UIKit 아님!
enum BookSearchItem: Hashable, Sendable {
case recentBook(Book)
case searchBook(Book)
case empty
}
하지만 여전히 같은 에러가 발생했습니다.
신기하게도 다음과 같이 변경하면 에러가 사라졌습니다.
// ✅ 에러 없음
private var dataSource: UICollectionViewDiffableDataSource<BookSearchSection, BookSearchItem.ID>!
BookSearchItem.ID는 UUID 타입인데, 왜 이건 되고 BookSearchItem 자체는 안 될까?
더 이상한 건, Apple의 공식 샘플 코드(Implementing Modern Collection Views)에서는 똑같은 패턴을 사용하는데 에러가 없었습니다.
// Apple 샘플 코드 - MountainsViewController.swift
var dataSource: UICollectionViewDiffableDataSource<Section, MountainsController.Mountain>!
// ✅ 에러 없음!
프로젝트의 Build Settings를 확인하던 중, 결정적인 설정을 발견했습니다.
Swift Compiler - Concurrency → Default Actor Isolation = MainActor
이 설정이 MainActor로 되어 있었습니다!
이 설정은 "명시적으로 actor를 지정하지 않은 타입들의 기본 격리 방식"을 결정합니다.
Default Actor Isolation = MainActor// 내가 작성한 코드
enum BookSearchItem: Hashable, Sendable {
case recentBook(Book)
}
// 컴파일러가 실제로 보는 코드
@MainActor // 자동으로 추가됨!
enum BookSearchItem: Hashable, Sendable {
case recentBook(Book)
}
모든 타입이 자동으로 MainActor에 격리됩니다.
Default Actor Isolation = nonisolated// 내가 작성한 코드
enum BookSearchItem: Hashable, Sendable {
case recentBook(Book)
}
// 컴파일러가 보는 코드 (그대로)
enum BookSearchItem: Hashable, Sendable {
case recentBook(Book)
}
명시적으로 @MainActor를 붙인 것만 격리됩니다.
핵심은 MainActor에 격리된 타입은 Sendable이 될 수 없다는 것입니다.
protocol Sendable {}
"여러 concurrency domain(actor) 간에 안전하게 전달할 수 있는 타입"
@MainActor
class MyData {
var value: Int = 0
}
"반드시 MainActor에서만 접근 가능한 타입"
@MainActor
enum BookSearchItem: Sendable { // ❌ 모순!
case recentBook(Book)
}
비유로 설명하면:
[MainActor 영역]
│
├─ 보호막 (Actor Isolation)
│
└─ @MainActor 타입들
- 보호막 안에서만 접근 가능
- 보호막 밖으로 못 나감
- = Sendable 불가능
Actor 격리는 데이터 보호를 위한 것입니다:
@MainActor @preconcurrency
open class UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>
where ItemIdentifierType: Hashable
Swift 6의 strict concurrency 모드에서는 암묵적으로:
where ItemIdentifierType: Hashable & Sendable
ItemIdentifierType이 Sendable이어야 합니다!
1. Build Settings에서 Default Actor Isolation = MainActor
↓
2. BookSearchItem이 자동으로 @MainActor에 격리됨
↓
3. @MainActor 타입은 Sendable이 될 수 없음 (actor 영역 밖으로 못 나감)
↓
4. 하지만 DiffableDataSource는 ItemIdentifierType이 Sendable을 요구함
↓
5. ❌ 에러: "Main actor-isolated conformance of 'BookSearchItem' to 'Hashable'
cannot satisfy conformance requirement for a 'Sendable' type parameter"
Apple 샘플 코드의 프로젝트 설정을 확인해보니:
SWIFT_VERSION = 5.0
SWIFT_STRICT_CONCURRENCY = (설정 없음, 기본값 minimal)
반면 우리 프로젝트는:
Build Settings → Swift Compiler - Concurrency → Default Actor Isolation = nonisolated
이 방법이 가장 근본적이고 권장되는 해결책입니다.
장점:
@MainActor가 필요한 곳에만 붙임변경 후 필요한 작업:
// ViewController는 명시적으로 표시
@MainActor
final class BookSearchViewController: UIViewController {
private var dataSource: UICollectionViewDiffableDataSource<BookSearchSection, BookSearchItem>!
// ✅ 이제 에러 없음!
}
// Data 타입은 격리 없음 (그대로 사용)
enum BookSearchItem: Hashable, Sendable {
case recentBook(Book)
case searchBook(Book)
case empty
}
UIViewController는 원래 MainActor에서 동작하므로 명시적으로 표시하는 것이 맞습니다.
그렇다면 Swift 팀은 왜 이 설정을 기본값으로 했을까요? 해당 근거는 Swift Evolution Proposal 0466 (SE-0466): Control default actor isolation inference에서 찾을 수 있습니다.
내용이 매우 심도 있지만, 핵심 동기를 요약하자면 다음과 같습니다.
개발자가 의도하지 않은 동시성 오류를 제거하여, Swift 동시성을 더 쉽게 만들고 마이그레이션 부담을 줄인다.**
이 기본 액터 격리 설정은 "모든 코드를 @MainActor로 강제하는 것"이 아닙니다.
대신, "개발자가 명시적으로 격리하지 않은 코드만" 앱의 일반적인 실행 환경인 @MainActor로 기본 추론함으로써 동시성 문제를 완화하는 방식입니다.
궁극적으로는, 기존처럼 비격리(nonisolated)로 가정되어 발생하던 불필요한 경고들을 없애는 것이 핵심 목표입니다. 이 추론의 결과, 해당 코드는 런타임에도 실제로 MainActor의 격리를 받게 됩니다.
"쉽게" 만들고 마이그레이션 부담을 줄인다... 저 역시 이 버그로 인해 내용을 파헤치지 않았다면 "오, 그렇구나" 하고 넘어갔을 것입니다.
하지만 직접 문제를 겪고 나니, 오히려
"기본값이 되면서 예상치 못한 곳에서 더 복잡한 충돌이 발생할 수 있구나"라는 생각이 들었습니다. 하하.
처음에는 단순히 "Sendable을 채택했는데 왜 에러가 나지?"라는 의문에서 시작했지만, 결국 Swift의 actor isolation과 concurrency 모델에 대한 깊은 이해가 필요한 문제였습니다.
핵심 요약:
1. MainActor에 격리된 타입은 Sendable이 될 수 없다
2. Default Actor Isolation 설정이 모든 타입의 기본 격리 방식을 결정한다
3. UICollectionViewDiffableDataSource는 Sendable 타입을 요구한다
4. 해결책: Default Actor Isolation을 nonisolated로 변경하고 필요한 곳에만 @MainActor를 명시
Swift Concurrency에 대해서 많이 공부했다고 했는데.. 또 공부할게 생겼습니다....