← 모든 글

SwiftData의 진짜 비용은 스키마 규율입니다

장르: shipped-code. 이 글은 Get Bananas, Return, Reps에 걸친 SwiftData 스키마 결정 사항을 기록합니다. 세 앱 모두 스키마가 깔끔한 마이그레이션을 통과했거나, 마이그레이션을 계획하지 않은 대가를 치렀습니다. Get Bananas의 ShoppingItem이 대표적인 예시입니다. 원래 스키마에는 lastModified 타임스탬프가 포함되어 있지 않았고, 나중에 추가하려면 특정한 마이그레이션 형태가 필요했습니다. 기존 데이터가 이미 디스크에 있었기 때문이며, 이 필드는 처음 비옵셔널로 추가되었을 때 발생한 마이그레이션 충돌을 수정하기 위해 옵셔널로 만들어졌습니다.1

SwiftData의 API는 두 개의 매크로입니다. 클래스에 @Model을 붙이면 영구 타입이 됩니다. 프로퍼티에 @Attribute(.unique)를 붙이면 고유성 제약이 부여됩니다. 프레임워크는 Core Data의 스택 관리, 값 변환기 처리, NSManagedObjectContext 보일러플레이트를 숨겨줍니다. 프레임워크가 숨기지 않는 것은 스키마 마이그레이션입니다. 단지 마이그레이션을 명령형이 아닌 선언형으로 만들 뿐입니다. 마이그레이션에 주의를 기울이지 않은 대가는 일상적인 업데이트에서 사용자의 데이터를 지워버리는 버그입니다.

이 글의 주제는 다음과 같습니다. SwiftData는 시작은 저렴하지만 허술하게 마이그레이션하면 비싸집니다. 규율은 명명, 옵셔널 여부, 그리고 VersionedSchema를 첫날부터 적용하는 것입니다. 적용해야 한다는 것을 깨달은 그날부터가 아닙니다.

TL;DR

  • @Model 매크로는 클래스를 영구 SwiftData 타입으로 변환합니다. 프레임워크는 컴파일 타임에 프로퍼티 선언으로부터 스키마를 생성합니다.
  • 새 옵셔널 프로퍼티 추가는 무동작 마이그레이션입니다. SwiftData의 경량 마이그레이션이 처리합니다. 기존 스키마에 비옵셔널 프로퍼티를 추가하려면 VersionedSchema와 함께 기존 행에 새 필드를 어떻게 채울지 프레임워크에 알려주는 MigrationPlan이 필요합니다.
  • 첫날부터 VersionedSchema를 건너뛰는 비용은 어떤 사소하지 않은 v2 스키마 변경이든 사용자의 데이터베이스를 잃을 위험을 감수해야 한다는 것입니다. 경량 경로는 보수적이며, 마이그레이션을 추론할 수 없으면 중단되기 때문입니다.
  • @Attribute(.unique)는 자연 키(직접 생성한 UUID, 가져온 외부 ID)에 적합한 도구입니다. @Relationship은 부모/자식 참조에 적합한 도구입니다. 둘 다 내부적으로 올바른 Core Data 배관을 생성하는 매크로입니다.2

@Model이 실제로 하는 일

SwiftData 타입은 @Model 매크로가 적용된 Swift 클래스입니다. Get Bananas의 ShoppingItem이 대표적인 형태입니다.

import Foundation
import SwiftData

@Model
final class ShoppingItem {
    @Attribute(.unique) var id: UUID
    var name: String
    var amount: String
    var section: String
    var isChecked: Bool
    var isOptional: Bool
    var sortOrder: Int
    var lastModified: Date?

    init(id: UUID = UUID(), name: String, amount: String, section: String,
         isOptional: Bool = false, sortOrder: Int = 0) {
        self.id = id
        self.name = name
        self.amount = amount
        self.section = section
        self.isChecked = false
        self.isOptional = isOptional
        self.sortOrder = sortOrder
        self.lastModified = Date()
    }
}

이 형태에서 API가 숨기는 세 가지 세부 사항입니다.

@Model은 별도의 영구 저장소 스키마 선언이 필요하지 않습니다. SwiftData는 컴파일 타임에 클래스 정의를 읽어 스키마를 합성합니다. 클래스의 프로퍼티는 모델의 속성이 되고, Swift 타입은 컬럼 타입이 됩니다. 유지 관리할 .xcdatamodeld 파일이 없습니다(Core Data의 기본 NSManagedObjectModel은 여전히 존재하며 런타임에 스키마를 뒷받침합니다).2

@Attribute(.unique)는 단일 컬럼에 대한 제약이지 PRIMARY KEY 선언이 아닙니다. SwiftData의 영구 식별자는 PersistentIdentifier로, 행마다 자동으로 생성됩니다. @Attribute(.unique) 선언은 프레임워크에 “이 컬럼은 값당 최대 한 행만 저장한다”고 알립니다. 이미 존재하는 .unique 값을 가진 모델을 삽입하면 SwiftData는 upsert를 수행합니다. 기존 행이 거부되지 않고 업데이트됩니다. 이 의미론은 제품 코드에서 중요합니다. .unique는 중복 제출을 방지하는 UI 수준의 검증이 아니라, 조용히 병합하는 최대 한 개 저장 보장입니다. 위의 id: UUID 패턴은 프로세스 간 동기화에 권장되는 형태입니다(프로세스 내 PersistentIdentifier가 사라져도 살아남는 안정적인 식별자가 필요한 경우). upsert 동작은 동일한 UUID가 두 동기화 경로에서 도착할 때 정확히 원하는 동작입니다.

@Model 클래스는 값 타입이 아닌 참조 타입입니다. ShoppingItem 인스턴스의 프로퍼티를 변경하면 SwiftData의 변경 추적이 작동합니다. 프레임워크는 변경을 등록하고 다음 컨텍스트 저장 시 영속화합니다. @Query를 통한 SwiftUI 통합은 일치하는 술어를 관찰하는 모든 뷰를 다시 렌더링합니다. 이 패턴은 @Observable(What SwiftUI Is Made Of에서 다룸)과 유사하며, 그 위에 영속성이 계층화된 형태입니다.

옵셔널 필드는 저렴한 마이그레이션입니다

ShoppingItemlastModified: Date? 필드는 옵셔널이며, 이 옵셔널 여부는 핵심적인 역할을 합니다. 이 필드는 v1 출시 이후에 크로스 디바이스 동기화와 충돌 해결을 지원하기 위해 추가되었습니다. 사용자 기기의 기존 행에는 lastModified 값이 없었습니다. 기본값이 없는 옵셔널 필드는 SwiftData의 경량 마이그레이션이 마이그레이션 코드 없이 추가를 처리할 수 있게 합니다. 기존 행은 nil을 가지고, 새 행은 init이 설정한 값을 가집니다.3

경량 마이그레이션 경로는 프레임워크의 정중한 경로입니다. SwiftData는 새 스키마와 영구 저장소를 검사하고, 가장 작은 호환 변경을 추론하여 적용합니다. 마이그레이션은 자동으로 이루어지며, 사용자는 아무것도 보지 못하고, 앱은 기존 데이터에서 정상적으로 실행됩니다. 경량 경로가 깔끔하게 처리하는 경우는 다음과 같습니다.

  • 옵셔널 프로퍼티 추가
  • 프로퍼티 제거(데이터는 삭제되며, 기존 읽기는 더 이상 컬럼을 보지 못합니다)
  • 프레임워크가 힌트로 일치시킬 수 있는 속성 이름 변경(@Attribute(originalName: ...) 사용)
  • 프레임워크가 일치시킬 수 있는 @Model 클래스 이름 변경(@Model.originalName 또는 힌트 사용)

경량 경로가 중단되는 경우는 다음과 같습니다.

  • 기본값이 없는 비옵셔널 프로퍼티를 기존 스키마에 추가(기존 행에는 채울 값이 없습니다)
  • 프로퍼티의 타입 변경(예: IntString)
  • 모델을 두 개로 분할하거나 두 개를 하나로 병합
  • 마이그레이션에 사용자 정의 로직이 필요한 모든 작업

경량 경로가 중단될 때 안전한 동작은 마이그레이션을 실패시키는 것입니다. 안전하지 않은 동작은 데이터베이스를 삭제하고 처음부터 시작하는 것입니다. 프레임워크는 보수적이어서 그것을 조용히 하지 않습니다. 사용자는 마이그레이션 오류로 앱이 실행 시 충돌하는 것을 봅니다. 개발자는 스키마 불일치를 가리키는 스택 추적을 봅니다. 아무도 데이터를 잃지 않지만, 모두가 신뢰를 잃습니다.

첫날부터 VersionedSchema를 건너뛴 비용은 v2 → v3 경계에서 나타납니다. 경량 경로가 처리하는 범위를 초과하는 스키마 변경을 가진 세 번째 기능을 추가할 때입니다.

VersionedSchema와 MigrationPlan: 첫날부터의 규율

VersionedSchema는 모델 스키마의 특정 버전을 선언합니다. MigrationPlan은 한 버전에서 다음 버전으로 어떻게 마이그레이션할지 선언합니다.4 형태는 다음과 같습니다.

import SwiftData

enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] = [ShoppingItemV1.self]
}

enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] = [ShoppingItemV2.self]
}

enum AppMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] = [
        SchemaV1.self,
        SchemaV2.self,
    ]

    static var stages: [MigrationStage] = [
        MigrationStage.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
    ]
}

모델 클래스 자체는 버전 스키마 네임스페이스로 이동합니다.

extension SchemaV1 {
    @Model
    final class ShoppingItemV1 { /* v1 fields */ }
}

extension SchemaV2 {
    @Model
    final class ShoppingItemV2 { /* v2 fields, including lastModified */ }
}

ModelContainer는 마이그레이션 계획과 함께 구성됩니다.

let container = try ModelContainer(
    for: ShoppingItemV2.self,
    migrationPlan: AppMigrationPlan.self,
    configurations: ModelConfiguration("ShoppingList")
)

마이그레이션 계획은 프레임워크에 스키마가 어떻게 진화하는지에 대한 타입화된 그래프를 제공합니다. v2 출시 앱이 v1 데이터베이스에 대해 실행될 때, 프레임워크는 마이그레이션 계획을 따라가며 명명된 단계를 적용하고 데이터베이스를 v2로 가져옵니다. v3를 출시할 때는 schemasSchemaV3.self를 추가하고 v2와 v3 사이에 새 MigrationStage를 추가합니다.

규율은 단 하나의 버전만 있을 때조차 v1에서 VersionedSchema를 출시하는 것입니다. 그렇게 하는 비용은 파일 하나와 enum 선언 하나가 더 추가되는 것입니다. 그렇게 하지 않는 비용은 v2의 첫 번째 사소하지 않은 스키마 변경이 v1을 VersionedSchema로 소급 래핑해야 한다는 것입니다. 가능하지만, 프레임워크가 기존 데이터를 SchemaV1으로 식별할 수 있도록 정확한 v1 형태와 일치시키는 데 주의가 필요합니다. v2 작업을 하는 미래의 당신이 그 세금을 낼 것입니다. 현재의 당신은 한 번 내고 잊어버릴 수 있습니다.

어려운 경우를 위한 사용자 정의 MigrationStage

경량 마이그레이션은 대부분의 추가 변경을 다룹니다. 타입 변경, 분할, 병합, 조건부 채우기에는 MigrationStage.custom이 필요합니다.

static var stages: [MigrationStage] = [
    MigrationStage.custom(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self,
        willMigrate: { context in
            // Read v1 rows; stage any derived state to a transient store
            // (UserDefaults / temp file) since the v1 and v2 contexts do
            // not share state, and didMigrate cannot read v1.
            let v1Items = try context.fetch(FetchDescriptor<ShoppingItemV1>())
            stageDerivedState(from: v1Items)
        },
        didMigrate: { context in
            // Populate v2-only fields on existing rows
            let v2Items = try context.fetch(FetchDescriptor<ShoppingItemV2>())
            for item in v2Items where item.lastModified == nil {
                item.lastModified = Date()
            }
            try context.save()
        }
    )
]

두 클로저는 프레임워크가 구조적 마이그레이션을 적용하기 전과 후에 실행됩니다. willMigrate는 v1 스키마에 대해 실행되고, didMigrate는 v2 스키마에 대해 실행됩니다. 클로저 본문은 일반 SwiftData 코드(페치 디스크립터, 모델 컨텍스트 저장, 실행 중인 앱에서 사용되는 동일한 API)이며, 일시적인 마이그레이션 중 컨텍스트에 대해 작동합니다.

프로덕션에서 살아남는 패턴은 willMigrate를 비워 두고 모든 채우기 로직을 didMigrate에 넣는 것입니다. willMigrate 내부에서 v1 데이터를 읽는 것은 허용되지만, 프레임워크 관점에서 v2 스키마는 아직 존재하지 않으므로 모든 계산은 didMigrate 클로저가 읽을 수 있는 일시적 저장소에 단계적으로 배치되어야 합니다. 더 간단한 규칙은 다음과 같습니다. 구조적 마이그레이션은 프레임워크의 일이고, 기존 행에 v2 전용 필드를 채우는 것은 didMigrate의 일입니다.

@Attribute@Relationship이 그 이름값을 할 때

@Model 클래스에서 두 매크로가 대부분의 스키마 데코레이션 작업을 수행합니다.

@Attribute는 단일 프로퍼티를 제약 또는 힌트로 데코레이션합니다.

  • @Attribute(.unique)ShoppingItem.id처럼 고유성을 강제합니다
  • @Attribute(.externalStorage)는 큰 Data blob(이미지 데이터, 오디오 버퍼)을 데이터베이스 외부에 저장합니다
  • @Attribute(originalName: "old_field_name")은 마이그레이션 중에 프로퍼티를 이름이 변경된 컬럼과 일치시킵니다
  • @Attribute(.transformable(by: ...))은 Codable이 아닌 타입에 ValueTransformer를 적용합니다

올바른 규율은 다음과 같습니다. 진정으로 고유해야 하는 필드(직접 생성한 UUID, 외부 ID)에 .unique를 사용하고, 몇 KB를 초과하는 모든 blob에 .externalStorage를 사용하며, 프로퍼티의 v2 이름 변경이 v1 데이터를 잃을 수 있을 때 originalName을 사용합니다.

@Relationship은 다른 @Model 클래스 또는 그 컬렉션을 가리키는 프로퍼티를 데코레이션합니다.

@Model
final class List {
    var name: String

    @Relationship(deleteRule: .cascade, inverse: \ShoppingItem.list)
    var items: [ShoppingItem] = []
}

@Model
final class ShoppingItem {
    var name: String
    var list: List?
}

deleteRule: .cascade는 부모 List를 삭제하면 모든 자식 ShoppingItem 행이 삭제된다는 의미입니다. inverse: 매개변수는 자식의 어느 프로퍼티가 부모를 다시 가리키는지 프레임워크에 알립니다. 프레임워크는 예측 가능한 양방향 유지 관리에 이를 사용합니다. SwiftData는 때때로 역방향을 자동으로 추론할 수 있고, inverse: nil은 명시적으로 단방향 관계에 대해 지원되지만, 안전한 기본값은 추론이 모호할 수 있을 때마다 inverse:를 선언하는 것입니다.5

올바른 규율은 다음과 같습니다. 명시적인 deleteRule로 관계를 선언하고(기본값은 .nullify이며, 이는 거의 원하는 것이 아닙니다), 관계가 양방향일 때마다 inverse:를 선언합니다(프레임워크의 추론에 의존하기보다는). 암묵적 기본값은 보통 잘못되어 있습니다. 명시적 형태는 매개변수 하나를 더 추가하지만, 영원히 저장된 버그가 됩니다.

다르게 만들었다면

이 클러스터의 앱들이 출시했거나 출시했더라면 좋았을 세 가지 패턴입니다.

v1부터 VersionedSchema를 출시하세요. 모든 출시 @Model 클래스는 첫날부터 VersionedSchema 안에 살아야 합니다. 비용은 스키마 버전당 래핑 enum 하나입니다. 이점은 v2의 첫 번째 사소하지 않은 변경이 이틀 동안의 소급 리팩토링이 아닌 MigrationPlan.schemas에 한 줄을 추가하는 것이라는 점입니다.

모든 타임스탬프를 옵셔널로 만드세요. 크로스 디바이스 동기화 또는 충돌 해결을 위해 존재하는 lastModified, createdAt, updatedAt과 같은 필드는 v1 제품이 필요로 하지 않으면 v1에서 옵셔널이어야 합니다. 옵셔널은 v2로의 마이그레이션(필요해질 때)을 저렴하게 유지합니다. didMigrate 중에 기존 행에 채우는 것은 루프 하나입니다. v1부터 비옵셔널로 만드는 것은 사용자 데이터의 백필을 깨뜨릴 수 있는 제약입니다.

PersistentIdentifier가 아닌 UUID를 자연 키로 사용하세요. SwiftData의 PersistentIdentifier는 프로세스 내부적입니다. 크로스 디바이스 동기화, MCP 통합(Two Agent Ecosystems, One Shopping List에서 다룸), 그리고 모든 프로세스 외부 참조에는 안정적인 식별자가 필요합니다. @Attribute(.unique)가 있는 UUID가 올바른 형태입니다. 프로세스 내 PersistentIdentifier는 프로세스 경계를 넘는 어떤 것에도 잘못된 형태입니다.

@Model이 잘못된 답인 경우

SwiftData가 올바른 도구가 아닌 세 가지 경우입니다.

단일 레코드 키/값 상태. 앱 설정, 사용자가 선택한 언어, 마지막 동기화 타임스탬프. UserDefaults 또는 NSUbiquitousKeyValueStore(Five Apple Platforms, Three Shared Files에서 다룸)을 사용하세요. 단일 행에 대한 SwiftData의 오버헤드는 낭비된 의식입니다. 키-값 저장소가 올바른 기반입니다.

오프라인 쓰기가 없는 서버 권한 데이터. REST API에서 가져와 읽기 전용으로 표시되는 목록. 진실의 원천이 서버이고 로컬 캐시가 단지 캐시일 뿐이라면 SwiftData는 과합니다. Documents/에 있는 간단한 Codable 스냅샷과 메모리 캐시된 배열로 충분합니다. 데이터가 하드 리셋에서 살아남지 않는다면 SwiftData 마이그레이션 세금을 지불할 가치가 없습니다.

다중 프로세스 조정. SwiftData는 프로세스 내부에서 작동합니다. iOS 앱 외부에서 실행되는 MCP 서버는 앱의 SwiftData 컨테이너를 읽거나 쓸 수 없습니다. 프로세스 간 상태에는 다른 형태가 필요합니다. iCloud Drive JSON 파일, 공유 App Group 컨테이너, 또는 프로세스를 연결하는 명시적 동기화 계층입니다. (Get Bananas는 정확히 이 이유로 SwiftData를 iCloud Drive JSON와 결합합니다.)6

데이터가 거의 변경되지 않는 큰 blob인 경우. 10MB 오디오 파일, 50MB 이미지 데이터셋. blob이 SwiftData 행 안에 있으면 @Attribute(.externalStorage)를 사용하세요. 그렇지 않으면 SwiftData가 파일 URL을 가리키는 메타데이터와 함께 파일 시스템을 직접 사용하세요.

iOS 26+에서 출시되는 앱에 이 패턴이 의미하는 바

세 가지 핵심입니다.

  1. 매크로는 쉬운 부분입니다. 마이그레이션이 비용입니다. @Model@Attribute는 많은 Core Data 배관을 숨기는 두 줄 선언입니다. 마이그레이션 규율은 앱의 수명 동안 실제로 지불하는 비용입니다. v2를 염두에 두고 v1을 설계하세요.

  2. 첫날부터의 VersionedSchema는 출시 앱에 협상 불가입니다. 래핑 enum은 추가 파일 하나입니다. 나중에 추가하는 소급 비용은 훨씬 더 높습니다.

  3. 옵셔널 필드와 명시적 관계는 저렴한 보험입니다. 동기화 메타데이터를 위한 옵셔널 타임스탬프, 관계에 대한 명시적 deleteRuleinverse:. 둘 다 많은 v2 유연성을 사주는 작은 선언입니다.

전체 Apple Ecosystem 클러스터: Apple Intelligence를 위한 타입화된 App Intents; 크로스 LLM 에이전트를 위한 MCP 서버; 둘 사이의 라우팅 질문; 온디바이스 LLM과 Tool 프로토콜을 위한 Foundation Models; iOS의 잠금 화면 상태 머신을 위한 Live Activities; Apple Watch의 watchOS 런타임 계약; 프레임워크 기반을 위한 SwiftUI 내부; visionOS 장면을 위한 RealityKit의 공간 정신 모델; 시각 계층을 위한 Liquid Glass 패턴; 크로스 디바이스 도달을 위한 멀티 플랫폼 출시. 허브는 Apple Ecosystem Series에 있습니다. 더 광범위한 iOS와 AI 에이전트 컨텍스트는 iOS Agent Development guide를 참조하세요.

FAQ

@Model과 Core Data의 NSManagedObject의 차이점은 무엇인가요?

@Model은 내부적으로 NSManagedObject 배관을 생성하는 Swift 매크로입니다. SwiftData는 Core Data를 백킹 저장소로 사용하므로 런타임 모델은 동일합니다. 차이점은 표면입니다. @Model.xcdatamodeld 파일, 값 변환기 의식, 그리고 NSManagedObjectContext 수명 주기 관리를 제거합니다. Swift 형태의 API로 동일한 영구 저장소를 얻습니다.

스키마를 변경할 계획이 전혀 없다면 VersionedSchema가 필요한가요?

앱이 v2를 출시할 가능성이 있다면, 네. 일회성 데모라면, 아니오. v1부터 VersionedSchema의 비용은 추가 enum 선언 하나입니다. v2에서 소급 추가하는 비용은 프레임워크가 기존 데이터를 인식하도록 정확한 v1 스키마 형태와 일치시키는 것입니다. 가능하지만 오류가 발생하기 쉽습니다. 대부분의 출시 앱은 결국 스키마 변경이 필요합니다. v1에서 그것을 위한 예산을 잡으세요.

@Attribute(.unique)는 언제 사용해야 하나요?

필드가 행의 자연 키일 때입니다. 직접 생성한 UUID, 가져온 외부 ID, 할당한 슬러그. SwiftData는 .unique를 upsert로 처리합니다. 이미 존재하는 .unique 값을 가진 모델을 삽입하면 새 행이 추가되는 대신 기존 행이 업데이트됩니다. 그 의미론은 upsert 스타일 동기화 경로(두 디바이스에서 오는 동일한 UUID)를 안전하게 만드는 것입니다. 또한 title과 같은 표시 이름 필드에 .unique가 잘못된 도구인 이유이기도 합니다. 두 사용자가 같은 제목을 입력하면 두 개의 별개 레코드를 만드는 대신 행을 조용히 병합하기 때문입니다.

기존 스키마에 추가된 비옵셔널 필드를 어떻게 처리하나요?

기존 행에 필드를 채우는 didMigrate 클로저가 있는 MigrationStage.custom을 사용하세요. 또는 더 쉽게는 새 스키마 버전에서 필드를 옵셔널로 선언하고 접근 시 지연 채우기를 하세요. 옵셔널이 더 저렴한 마이그레이션입니다. 비옵셔널 추가에는 명시적 채우기 로직이 필요합니다.

PersistentIdentifier와 직접 만든 UUID의 차이는 무엇인가요?

PersistentIdentifier는 SwiftData의 프로세스 내 행 ID입니다. 자동으로 생성되며 실행 중인 프로세스의 수명 동안 유지됩니다. @Attribute(.unique)가 있는 직접 만든 UUID는 안정적인 크로스 프로세스, 크로스 디바이스 식별자입니다. 앱 내부의 프로세스 내 참조에는 PersistentIdentifier를 사용하세요. 프로세스 경계를 넘는 모든 것(크로스 디바이스 동기화, 외부 통합, MCP 도구, 네트워크 호출)에는 UUID를 사용하세요.

References


  1. 저자의 Get Bananas, iCloud Drive JSON 동기화와 MCP 서버를 결합한 SwiftUI 쇼핑 목록 앱. ShoppingItem 모델은 초기 개발 주기 전반에 걸쳐 진화했으며, lastModified: Date? 필드는 초기 스키마 이후에 추가되었습니다(2025-12-01의 커밋 268a00d, “Make lastModified optional to fix migration crash”). 비옵셔널로 만들면 기존 행에 채울 값이 없을 때 마이그레이션이 깨졌기 때문입니다. 

  2. Apple Developer, “SwiftData”“Adding and editing persistent data in your app”. @Model 매크로, @Attribute 제약 표면, 그리고 Core Data의 NSManagedObjectModel과의 관계. 

  3. Apple Developer, “Preserving your app’s model data across launches”“Adopting SwiftData for a Core Data app”. 경량 마이그레이션 의미론과 프레임워크가 중단되는 트리거. 

  4. Apple Developer, “VersionedSchema”“SchemaMigrationPlan”. 버전 스키마 선언, 마이그레이션 단계 정의, 그리고 마이그레이션 계획을 받는 ModelContainer 생성자. 

  5. Apple Developer, “Defining data relationships with enumerations and model classes”“Schema.Relationship”. @Relationship 매크로, deleteRule 옵션(.cascade, .nullify, .deny, .noAction), 그리고 양방향 관계 유지 관리에서 inverse: 매개변수의 역할. 

  6. 저자의 Two Agent Ecosystems, One Shopping List 분석, 2026년 4월 29일, 그리고 Five Apple Platforms, Three Shared Files. 다중 프로세스 워크플로우 내에서 SwiftData를 보완(때로는 대체)하는 Get Bananas + Return 크로스 프로세스 및 크로스 디바이스 동기화 패턴. 

관련 게시물

Two Agent Ecosystems, One Shopping List: An MCP Server Living Alongside an iOS App

Get Bananas runs on iOS, macOS, watchOS, visionOS. It also lives inside Claude Desktop as an MCP server. The bridge is i…

19 분 소요

Foundation Models On-Device LLM: The Tool Protocol

iOS 26's Foundation Models framework puts a 3B-parameter LLM on every Apple Intelligence device. The Tool protocol is th…

15 분 소요

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 분 소요