문제 상황

문서 스캔 앱을 만들면서 Inbox/Archive 목록을 구현하고 있었는데요. SwiftData @Query로 문서 상태별 필터링을 하려던 순간, 예상치 못한 컴파일 에러 두 개를 연달아 만났습니다.

함정 1: #Predicate에서 enum 비교가 안 된다

처음 시도한 코드

자연스럽게 이렇게 작성했거든요.

@Model
class Document {
    var status: DocumentStatus  // enum 저장 프로퍼티
    // ...
}

enum DocumentStatus: String, Codable {
    case inbox, archived
}

그리고 @Query에서 필터링:

@Query(
    filter: #Predicate<Document> { $0.status == .inbox },
    sort: \Document.createdAt,
    order: .reverse
) private var documents: [Document]

깔끔해 보이죠? 그런데 빌드하면 컴파일 에러가 나요.

왜 안 되는가

#Predicate는 Swift 매크로인데, 내부적으로 predicate expression을 Core Data의 NSPredicate로 변환해야 하거든요. 이 과정에서 enum 타입은 직접 비교 대상으로 지원되지 않아요. #Predicate가 이해할 수 있는 타입은 String, Int, Bool 같은 기본 타입뿐이에요.

SwiftData가 내부적으로 enum을 rawValue로 저장하긴 하지만, #Predicate 매크로 레벨에서는 그 변환을 자동으로 해주지 않는 거죠.

해결: rawValue 저장 프로퍼티 패턴

@Model
class Document {
    var statusRawValue: String   // #Predicate 호환
    
    var status: DocumentStatus {  // 편의용 computed
        get { DocumentStatus(rawValue: statusRawValue) ?? .inbox }
        set { statusRawValue = newValue.rawValue }
    }
    
    init(title: String, imageData: Data) {
        self.statusRawValue = DocumentStatus.inbox.rawValue
        // ...
    }
}

이제 #Predicate에서 String 비교로 필터링할 수 있어요.

@Query(
    filter: #Predicate<Document> { $0.statusRawValue == "inbox" },
    sort: \Document.createdAt,
    order: .reverse
) private var documents: [Document]

앱 코드에서는 여전히 document.status = .archived 같은 enum 인터페이스를 쓸 수 있고, 저장과 쿼리만 rawValue String으로 처리하는 구조예요.

함정 2: 빈 @Query에서 modelContext가 nil

처음 시도한 코드

InboxView에서 스캔한 문서를 저장해야 하니까, modelContext가 필요했는데요. 처음엔 이렇게 접근했어요.

struct InboxView: View {
    @Query(...) private var documents: [Document]
    
    // documents에서 context 가져오기
    func saveDocument() {
        if let context = documents.first?.modelContext {
            // 문서 저장...
        }
    }
}

Inbox가 비어있을 때 — 그러니까 앱을 처음 설치했을 때 — documents가 빈 배열이니까 documents.firstnil이고, modelContext에 접근할 수 없어요.

처음 스캔하는 사용자가 문서를 저장할 수 없는 상황이 되는 거죠.

해결: @Environment

struct InboxView: View {
    @Query(...) private var documents: [Document]
    @Environment(\.modelContext) private var modelContext
    
    func saveDocument() {
        // modelContext는 항상 사용 가능
        let document = Document(title: "New", imageData: data)
        modelContext.insert(document)
    }
}

@Environment(\.modelContext)는 SwiftData의 modelContainer가 뷰 계층에 주입되어 있으면 항상 접근 가능해요. 쿼리 결과와 무관하게요.

배운 점

  • #Predicate는 기본 타입만 비교 가능하다. enum을 필터링하려면 rawValue 저장 프로퍼티를 별도로 두고, computed property로 enum 인터페이스를 유지하는 패턴이 필요해요.
  • @Query 결과에 의존해서 modelContext를 얻지 마세요. 빈 목록에서는 접근 불가. @Environment(\.modelContext)가 항상 정답이에요.

두 가지 다 SwiftData 문서에 명확하게 경고되어 있지 않아서, 직접 빌드 에러를 만나봐야 알게 되는 함정이더라고요.