UICollectionViewDiffableDataSource와 Sendable 에러 해결기

피터·2025년 10월 29일
post-thumbnail

문제 상황

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>!
    // ❌ 여기서 에러 발생!
}

이상한 점들

1. 모든 프로토콜을 채택했는데도 에러가 남

  • BookSearchItem: ✅ Hashable, ✅ Sendable
  • BookSearchSection: ✅ Hashable, ✅ Sendable
  • Book: ✅ Hashable, ✅ Sendable

모든 타입이 요구사항을 만족하는데 왜 에러가 날까?

2. 별도 파일로 분리해도 에러

처음에는 "UIKit을 import한 파일에 있어서 그런가?"라고 생각했습니다.

// BookSearchModels.swift (새 파일)
import Foundation // UIKit 아님!

enum BookSearchItem: Hashable, Sendable {
    case recentBook(Book)
    case searchBook(Book)
    case empty
}

하지만 여전히 같은 에러가 발생했습니다.

3. UUID로 하면 에러가 사라짐

신기하게도 다음과 같이 변경하면 에러가 사라졌습니다.

// ✅ 에러 없음
private var dataSource: UICollectionViewDiffableDataSource<BookSearchSection, BookSearchItem.ID>!

BookSearchItem.IDUUID 타입인데, 왜 이건 되고 BookSearchItem 자체는 안 될까?

4. Apple 샘플 코드는 문제없음

더 이상한 건, Apple의 공식 샘플 코드(Implementing Modern Collection Views)에서는 똑같은 패턴을 사용하는데 에러가 없었습니다.

// Apple 샘플 코드 - MountainsViewController.swift
var dataSource: UICollectionViewDiffableDataSource<Section, MountainsController.Mountain>!
// ✅ 에러 없음!

문제 원인 파악

결정적 단서: Build Settings

프로젝트의 Build Settings를 확인하던 중, 결정적인 설정을 발견했습니다.

Swift Compiler - Concurrency → Default Actor Isolation = MainActor

이 설정이 MainActor로 되어 있었습니다!

Default Actor Isolation이란?

이 설정은 "명시적으로 actor를 지정하지 않은 타입들의 기본 격리 방식"을 결정합니다.

설정 A: Default Actor Isolation = MainActor

// 내가 작성한 코드
enum BookSearchItem: Hashable, Sendable {
    case recentBook(Book)
}

// 컴파일러가 실제로 보는 코드
@MainActor // 자동으로 추가됨!
enum BookSearchItem: Hashable, Sendable {
    case recentBook(Book)
}

모든 타입이 자동으로 MainActor에 격리됩니다.

설정 B: Default Actor Isolation = nonisolated

// 내가 작성한 코드
enum BookSearchItem: Hashable, Sendable {
    case recentBook(Book)
}

// 컴파일러가 보는 코드 (그대로)
enum BookSearchItem: Hashable, Sendable {
    case recentBook(Book)
}

명시적으로 @MainActor를 붙인 것만 격리됩니다.

왜 MainActor 격리가 문제인가?

핵심은 MainActor에 격리된 타입은 Sendable이 될 수 없다는 것입니다.

Sendable의 의미

protocol Sendable {}

"여러 concurrency domain(actor) 간에 안전하게 전달할 수 있는 타입"

@MainActor의 의미

@MainActor
class MyData {
    var value: Int = 0
}

"반드시 MainActor에서만 접근 가능한 타입"

둘의 모순

@MainActor
enum BookSearchItem: Sendable { // ❌ 모순!
    case recentBook(Book)
}
  • Sendable: "여러 actor 간에 전달 가능해야 함"
  • @MainActor: "MainActor에만 있어야 함"
  • 모순: MainActor에만 있어야 하는데 어떻게 다른 곳으로 전달?

비유로 설명하면:

  • MainActor 격리 = 은행 금고 안
  • Sendable = 밖으로 가지고 나갈 수 있는 것
  • 금고 안에만 있어야 하는 물건은 밖으로 못 나감!

Actor 격리와 Sendable의 관계

[MainActor 영역]
│
├─ 보호막 (Actor Isolation)
│
└─ @MainActor 타입들
    - 보호막 안에서만 접근 가능
    - 보호막 밖으로 못 나감
    - = Sendable 불가능

Actor 격리는 데이터 보호를 위한 것입니다:

  • 격리된 데이터는 해당 actor 안에서만 접근 가능
  • 다른 actor로 전달하면 data race 발생 가능
  • 따라서 격리된 타입은 Sendable이 될 수 없음

UICollectionViewDiffableDataSource의 요구사항

@MainActor @preconcurrency
open class UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>
    where ItemIdentifierType: Hashable

Swift 6의 strict concurrency 모드에서는 암묵적으로:

where ItemIdentifierType: Hashable & Sendable

ItemIdentifierTypeSendable이어야 합니다!

에러 발생 과정

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 샘플 코드는 왜 문제없었나?

Apple 샘플 코드의 프로젝트 설정을 확인해보니:

SWIFT_VERSION = 5.0
SWIFT_STRICT_CONCURRENCY = (설정 없음, 기본값 minimal)
  • Swift 5 언어 모드
  • Strict concurrency 체크 최소화
  • Default Actor Isolation이 기본값 (nonisolated)

반면 우리 프로젝트는:

  • Xcode 26 최신 버전 사용
  • Swift 6 concurrency 체크 활성화
  • Default Actor Isolation = MainActor로 변경됨

해결 방법

Default Actor Isolation 변경 (권장 ⭐)

Build Settings → Swift Compiler - Concurrency → Default Actor Isolation = nonisolated

이 방법이 가장 근본적이고 권장되는 해결책입니다.

장점:

  • Swift의 표준 동작 방식
  • 명시적으로 @MainActor가 필요한 곳에만 붙임
  • DiffableDataSource 같은 표준 API와 잘 동작
  • 코드가 더 명확해짐

변경 후 필요한 작업:

// 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에서 동작하므로 명시적으로 표시하는 것이 맞습니다.

왜 Xcode는 @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에 대해서 많이 공부했다고 했는데.. 또 공부할게 생겼습니다....

참고 자료

profile
iOS 개발자입니다.

0개의 댓글