← 모든 글

SwiftData 마이그레이션: Lightweight vs Custom, 그리고 V2가 필요 없을 때

SwiftData의 스키마 마이그레이션 방식은 Core Data에 비해 구조적으로 개선되었지만, 팀들이 계속 빠지는 함정이 하나 있습니다. SwiftData가 inline default를 통해 자동으로 처리했을 변경에 대해 새로운 VersionedSchema를 선언하는 것입니다. 그 결과 코드가 올바르게 보이고 깔끔하게 빌드되었는데도 디바이스에서 “Duplicate version checksums across stages detected” 크래시가 발생합니다. 프레임워크의 실제 마이그레이션 모델은 세 가지 요소(VersionedSchema, MigrationStage, SchemaMigrationPlan)와 세 가지 마이그레이션 유형(automatic lightweight, declared lightweight, custom)을 사용합니다1. 대부분의 스키마 변경은 automatic입니다. 일부는 declared lightweight stage가 필요합니다. 소수의 경우 willMigratedidMigrate 클로저를 사용하는 custom stage가 필요합니다.

이 글에서는 Apple 문서를 따라가며 마이그레이션 모델을 살펴보고, 각 마이그레이션 유형이 처리하는 경우를 짚어내고, iOS 26의 새로운 클래스 상속 지원을 다룹니다. 핵심 틀은 “무엇을 내가 선언하고, 무엇을 SwiftData가 대신 처리하는가”입니다. 이 결정이 마이그레이션이 깔끔하게 출시될지, 아니면 첫 실행에서 크래시가 날지를 좌우하기 때문입니다.

TL;DR

  • SwiftData 마이그레이션은 세 가지 프로토콜로 구성됩니다. VersionedSchema(특정 버전에서의 모델 타입 스냅샷), MigrationStage(.lightweight 또는 .custom case를 가진 fromVersion-to-toVersion 단일 전환), SchemaMigrationPlan(stage의 순서가 있는 목록)입니다1.
  • inline default(var foo: Bool = false)와 함께 새로운 @Model 프로퍼티를 추가하는 것은 새로운 VersionedSchema를 필요로 하지 않습니다. SwiftData가 이 추가를 lightweight 마이그레이션으로 자동 처리합니다. 이를 위해 V2를 선언하면 “Duplicate version checksums across stages detected” 크래시가 발생합니다.
  • Lightweight 마이그레이션이 처리하는 것: entity, attribute, relationship의 추가/이름 변경/삭제, relationship 타입 변경, 이름 변경을 추적하기 위한 @Attribute(originalName:) 선언, delete rule 지정입니다. 대부분의 스키마 변경이 여기에 해당합니다.
  • Custom 마이그레이션(MigrationStage.custom(fromVersion:toVersion:willMigrate:didMigrate:))은 데이터 변환을 처리합니다. 하나의 컬럼을 둘로 나누기, 파생 필드 계산하기, 모델 간 데이터 이동하기 등입니다. willMigrate는 이전 context를, didMigrate는 새 context를 가집니다.
  • iOS 26은 @Model 타입에 대한 클래스 상속을 추가합니다2. 상속을 도입하는 스키마는 이전의 flat-model 버전에서 시작하는 lightweight stage와 함께 새 버전으로 올라갑니다.

세 가지 요소로 이루어진 모델

SwiftData 마이그레이션은 세 가지 요소로 구성됩니다.

VersionedSchema

특정 스키마 버전에서의 모델 타입 스냅샷입니다1. 이 프로토콜은 다음을 요구합니다.

  • static var versionIdentifier: Schema.Version. 시맨틱 버전 3요소(Schema.Version(1, 0, 0))입니다.
  • static var models: [any PersistentModel.Type]. 이 버전에 포함된 @Model 타입의 배열입니다.
enum SchemaV1: VersionedSchema {
    static let versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Item.self]
    }

    @Model
    final class Item {
        var name: String
        var createdAt: Date
        init(name: String, createdAt: Date) {
            self.name = name
            self.createdAt = createdAt
        }
    }
}

enum 안에 타입을 중첩하는 패턴이 관례입니다. 각 VersionedSchema는 자신의 모델 클래스를 네임스페이스로 감싸므로, 같은 모델 이름을 가진 여러 스키마가 마이그레이션 중에 코드베이스에서 공존할 수 있습니다.

MigrationStage

VersionedSchema 타입 사이의 단일 전환입니다3. 두 가지 case가 있습니다.

  • .lightweight(fromVersion: any VersionedSchema.Type, toVersion: any VersionedSchema.Type). SwiftData가 앱 코드 없이 처리하는 전환을 선언합니다. 파라미터는 raw Schema.Version 값이 아니라 VersionedSchema 타입 그 자체(예: SchemaV1.self)입니다.
  • .custom(fromVersion:toVersion:willMigrate:didMigrate:). 데이터 마이그레이션 전후에 실행되는 코드를 포함한 전환을 선언합니다. 버전 인자의 파라미터 타입은 .lightweight과 동일합니다.

SchemaMigrationPlan

이전 버전에서 현재 버전으로 스키마를 가져가는, 순서가 있는 stage 목록입니다1.

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

    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }

    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self
    )

    static let migrateV2toV3 = MigrationStage.custom(
        fromVersion: SchemaV2.self,
        toVersion: SchemaV3.self,
        willMigrate: { context in
            // Pre-migration: read old data, prepare it
            try context.save()
        },
        didMigrate: { context in
            // Post-migration: backfill new fields
            let descriptor = FetchDescriptor<SchemaV3.Item>()
            let items = try context.fetch(descriptor)
            for item in items {
                item.computedField = computeFromExisting(item)
            }
            try context.save()
        }
    )
}

ModelContainer는 현재 스키마와 마이그레이션 플랜을 모두 사용해 설정됩니다.

let container = try ModelContainer(
    for: SchemaV3.Item.self,
    migrationPlan: AppMigrationPlan.self,
    configurations: ModelConfiguration(...)
)

SwiftData는 컨테이너 생성 시 persistent store의 현재 스키마 버전을 읽고, 그 버전부터 현재 버전까지 플랜의 stage를 순서대로 따라가며 각 stage를 차례로 적용합니다.

Lightweight 마이그레이션이 자동으로 처리하는 것

대부분의 스키마 변경에는 custom stage가 필요 없습니다1.

  • default 값이 있는 attribute 추가. 기존 @Modelvar foo: Bool = false를 추가하는 것은 자동입니다.
  • 새 entity(모델 클래스) 추가. 새 타입은 해당 VersionedSchema가 현재 버전일 때 나타나며, 기존 데이터는 보존됩니다.
  • attribute 또는 entity 제거. SwiftData가 컬럼이나 테이블을 삭제합니다.
  • attribute 또는 entity 이름 변경. 프로퍼티에 @Attribute(originalName: "oldName")을 추가해 데이터를 보존하세요. SwiftData가 이전 이름을 새 이름으로 매핑합니다.
  • relationship 타입 변경. single-to-many, many-to-many 등입니다.
  • delete rule 지정. @Relationship(deleteRule: .cascade)와 같은 추가는 lightweight입니다.

이 목록에 해당하는 변경의 경우, 모델 타입이 그 외에 변하지 않았다면 새로운 VersionedSchema를 아예 선언하지 않는 것이 올바른 패턴입니다. SwiftData가 기존 스키마에 대해 lightweight 마이그레이션을 자동으로 수행합니다.

함정: 필드를 추가한다고 V2가 필요한 것은 아니다

가장 흔한 SwiftData 마이그레이션 실수는 이렇습니다. 개발자가 inline default(var foo: Bool = false)와 함께 새 프로퍼티를 추가한 뒤, SchemaV1과 동일한 모델 타입을 참조하는 SchemaV2를 선언합니다. 빌드는 깔끔합니다. 기존 V1 데이터가 있는 디바이스에서 처음 실행하면 Duplicate version checksums across stages detected로 크래시가 납니다. SchemaV1SchemaV2가 동일한 checksum으로 해석되기 때문입니다(SwiftData가 다르다고 인식할 만한 방식으로 모델 타입이 변하지 않았으므로).

올바른 패턴은 이렇습니다. 기존 VersionedSchema는 그대로 두고, 새 프로퍼티를 inline default와 함께 모델에 추가한 뒤, SwiftData의 automatic lightweight 마이그레이션이 처리하도록 맡기는 것입니다. MigrationPlan도, MigrationStage도, V2도 필요 없습니다.

// V1 schema
enum SchemaV1: VersionedSchema {
    @Model
    final class Item {
        var name: String
        // BEFORE: just these two properties
        var createdAt: Date
        // AFTER: add a third with inline default
        var isFavorite: Bool = false   // Lightweight, automatic
    }
}

var isFavorite: Bool = false 변경은 어떤 MigrationStage 선언 없이도 출시됩니다. migrationPlan:을 전달하지 않는 ModelContainer 이니셜라이저가 동작합니다.

let container = try ModelContainer(
    for: SchemaV1.Item.self,
    configurations: ModelConfiguration(...)
)

V2 스키마는 변경이 lightweight일 수 없을 때에만 필요합니다(데이터 변환, 모델 분할, custom 로직이 필요한 상속 구조 재편 등). 이런 경우에는 V2가 실재하며, SchemaMigrationPlan이 전환을 조율합니다.

Custom 마이그레이션이 필요할 때

Custom 마이그레이션은 다음 세 가지 경우에 그 복잡성을 정당화합니다.

1. 하나의 필드를 여러 개로 분할할 때. "Last, First"를 담고 있는 String 필드가 firstNamelastName 두 필드로 나뉩니다. 마이그레이션은 이전 값을 읽어 파싱하고, 새 필드에 써야 합니다.

static let migrateV1toV2 = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: nil,
    didMigrate: { context in
        let descriptor = FetchDescriptor<SchemaV2.Person>()
        let people = try context.fetch(descriptor)
        for person in people {
            let parts = person.fullName.split(separator: ", ", maxSplits: 1)
            person.lastName = String(parts.first ?? "")
            person.firstName = String(parts.dropFirst().first ?? "")
        }
        try context.save()
    }
)

didMigrate 클로저는 새 스키마의 context에 대해 실행되므로 새 필드에 접근할 수 있습니다. 이전 fullName은 새 필드가 채워질 때까지 제거를 미뤄야 할 수도 있습니다. 정리 작업은 후속 V2-to-V3 stage입니다.

2. 파생 필드를 계산할 때. 기존 데이터에 의존하는 새 @Attribute는 마이그레이션 시점에 backfill되어야 합니다.

3. 모델 간 데이터를 이동할 때. Item의 데이터를 Item과 새 Tag 모델로 나누는 재구성은, 이전 데이터로부터 태그를 할당하기 위해 custom 로직을 필요로 합니다.

일반적인 패턴은 이렇습니다. 스키마의 형태가 바뀌면 lightweight, 데이터의 형태가 바뀌면 custom입니다.

willMigrate vs didMigrate

Custom stage에는 서로 다른 시점에 호출되는 두 개의 클로저가 있습니다4.

willMigrate는 SwiftData가 스키마 마이그레이션을 적용하기 전에 실행됩니다. 클로저가 받는 모델 context는 이전 스키마의 context입니다. 스키마가 아래에서 바뀌기 전에 데이터를 캡처하거나, 비정규화하거나, 보조 상태를 준비할 때 사용하세요.

didMigrate는 스키마 마이그레이션 후에 실행됩니다. 모델 context는 스키마의 context입니다. 새 필드를 backfill하거나, 파생 데이터를 계산하거나, 마이그레이션을 마무리할 때 사용하세요.

필요하지 않으면 두 클로저 모두 nil이 될 수 있습니다. 대부분의 custom 마이그레이션은 didMigrate만 사용합니다. willMigrate는 스키마가 바뀐 후에는 접근할 수 없는 이전 데이터를 마이그레이션이 읽어야 할 때 유용합니다.

클로저는 ModelContext를 받아 fetch, 수정, 저장을 할 수 있습니다. 클로저는 throwing이며, 에러는 마이그레이션 밖으로 전파되어 마이그레이션을 중단시킵니다.

iOS 26: @Model에 대한 클래스 상속

iOS 26은 SwiftData 모델에 대한 클래스 상속을 도입합니다2. 이제 모델은 부모-자식 관계를 가질 수 있습니다.

@Model
class Vehicle {
    var make: String
    var year: Int
    init(make: String, year: Int) {
        self.make = make
        self.year = year
    }
}

@Model
final class Car: Vehicle {
    var doorCount: Int
    init(make: String, year: Int, doorCount: Int) {
        self.doorCount = doorCount
        super.init(make: make, year: year)
    }
}

상속을 도입하는 스키마는 이전 flat-model 버전에서 시작하는 lightweight 마이그레이션 stage와 함께 새 버전으로 올라갑니다. 상속이 기존 프로퍼티를 보존한다면 이 전환은 자동입니다. 서브클래스의 새 필드는 표준 inline-default 패턴을 따릅니다.

이 패턴은 여러 @Model 타입이 특성을 공유하는 경우에 잘 맞습니다. Car, Truck, Motorcycle 자식을 가진 Vehicle 부모, CheckingAccount, SavingsAccount 자식을 가진 Account 부모 같은 경우입니다. 공유 프로퍼티는 부모에, 구체적인 것은 자식에 둡니다.

마이그레이션 테스트하기

컴파일되는 마이그레이션이 곧 출시 가능한 마이그레이션은 아닙니다. 릴리스 전에 실행해 볼 만한 세 가지 테스트 패턴이 있습니다.

1. 프로덕션 데이터베이스 복사본에 대한 라운드트립 테스트. 최근 프로덕션 형태의 데이터베이스를 가져오거나(혹은 테스트를 통해 합성 V1 데이터를 생성하고), V2를 인식하는 컨테이너로 열어 데이터가 올바르게 마이그레이션되는지 검증하세요. 이 테스트는 타입 체커가 잡을 수 없는 custom 마이그레이션 버그를 잡아냅니다.

2. 이전 버전이 여전히 실행되는지. 이전 앱 버전을 빌드해 한 번 실행하여 V1 데이터를 만든 다음, 새 앱 버전을 빌드해 크래시 없이 실행되는지 검증하세요. 이 테스트는 “Duplicate version checksums” 함정과 유사한 선언 실수를 잡아냅니다.

3. 마이그레이션 실패 복구. 마이그레이션이 throw하면 어떻게 될까요? SwiftData의 동작은 컨테이너 설정에 따라 다릅니다. 프로덕션 앱에서는 처리되지 않은 마이그레이션 에러가 사용자 데이터를 조용히 삭제해서는 안 됩니다. 실패 경로를 명시적으로 테스트하고 앱이 무엇을 할지 결정하세요(롤백, 프롬프트, 백업으로부터 복구).

이 클러스터의 Single Source of Truth 글은 SwiftData store가 cross-process sync를 통해 교체될 때 어떤 일이 일어나는지에 대한 관련 질문을 다룹니다. 마이그레이션은 그 패턴의 로컬 진화 버전입니다.

프로세스 간 마이그레이션 출시와 진행 상황 표시

문서가 전면에 내세우지는 않지만 SwiftData 팀이 WWDC 2026에서 언급한 두 가지 운영상의 세부 사항이 있습니다5. 앱에 widget이나 extension이 있을 때 마이그레이션이 어디서 실행되는지, 그리고 마이그레이션이 실행될 때 진행 상황 UI를 어떻게 구동하는지입니다.

하나의 프로세스가 마이그레이션을 소유합니다. widget과 extension은 메인 앱과 동일한 런타임 리소스를 받지 못하므로 마이그레이션을 안전하게 수행할 수 없습니다. 권장 사항은 SchemaMigrationPlan1을 widget과 extension 타겟에서 완전히 빼고, 절대 그곳에서 마이그레이션하지 않는 것입니다. 보통 메인 앱인 하나의 프로세스를 데이터베이스의 소유자로 정하세요. widget이 컨테이너를 열었을 때 디스크상의 store가 버전이 없는(더 오래된) 스키마 상태라면 열기가 에러로 실패합니다. 이 에러를 마이그레이션이 필요하다는 신호로 취급하세요. 사용자에게 메인 앱을 열어 달라고 요청하는 UI를 표시하고, 앱이 마이그레이션을 수행하게 하며, 앱이 마이그레이션된 스키마 버전을 공유 UserDefault에 쓰게 하세요. widget은 다음번에 그 값을 읽어, 앱이 이미 마이그레이션한 버전으로 컨테이너를 엽니다. 이 패턴은 단일 writer가 책임을 지게 하여, 두 프로세스가 같은 파일을 진화시키려 경쟁하는 것을 피합니다.

진행 상황은 벽시계 시간이 아니라 stage 개수로 계산합니다. SwiftData는 전용 마이그레이션 진행 상황 API를 제공하지 않습니다5. 진행 표시기를 구동하려면 플랜에 있는 custom 마이그레이션 stage의 총 개수를 세고, stage별 didMigrate 핸들러4를 오버라이드하여 각 stage가 자신의 위치를 “M개 중 N번째 stage”로 보고하게 하세요. 이 숫자는 경과 시간이 아니라 완료된 stage를 반영하므로, 막대는 부드럽게가 아니라 이산적인 단계로 전진합니다. 함께 따라오는 디자인 결정은 마이그레이션 동안 앱이 무엇을 보여주는가입니다. 휑한 spinner는 멈춤처럼 읽혀 사용자가 떠나갑니다. 데이터가 허용하는 한 앱을 부분적으로라도 사용 가능하게 유지하거나, 최소한 각 stage가 무엇을 추가하는지(마이그레이션이 해제하는 새 기능을) 설명하여, 기다림이 죽은 시간이 아니라 무언가를 향한 진전으로 읽히게 하세요.

흔한 실패 양상

SwiftData 실패 로그에서 나타나는 세 가지 패턴입니다.

SwiftData가 자동으로 처리했을 변경에 대해 V2를 선언하기. “Duplicate version checksums” 크래시입니다. 해결책: inline-default 프로퍼티 추가에 대해 새 스키마를 선언하지 말고, SwiftData가 자동으로 처리하게 두세요.

저장하지 않는 custom 마이그레이션 코드. entity를 수정하지만 context.save()를 호출하지 않는 didMigrate 클로저는, 한 번 실행되어 그 작업을 버리고, 매 실행마다 다시 실행되는 마이그레이션을 만듭니다(마이그레이션이 완료되지 않은 것으로 보이기 때문입니다). 해결책: 데이터를 수정하는 모든 클로저는 반환하기 전에 반드시 try context.save()를 호출해야 합니다.

@Attribute(originalName:) 없이 프로퍼티 이름 변경하기. SwiftData는 새 프로퍼티를 새것으로, 이전 것을 삭제된 것으로 취급합니다. 이전 프로퍼티의 기존 데이터는 사라집니다. 해결책: @Attribute(originalName: "oldName") var newName: ...을 선언하여 SwiftData가 이름 변경을 거쳐 데이터를 매핑하게 하세요.

이 패턴이 iOS 26+ 앱에 의미하는 것

세 가지 핵심입니다.

  1. VersionedSchema 사다리는 기본적으로 만들지 마세요. inline default와 함께 프로퍼티 추가하기, 사용하지 않는 필드 삭제하기, @Attribute(originalName:)으로 이름 변경하기. 모두 lightweight이고 자동입니다. VersionedSchema 사다리는 SwiftData가 정말로 자동 처리할 수 없는 변경(데이터 변환, custom 로직, 상속 구조 재편)을 위한 것입니다.

  2. MigrationStage.custom은 데이터 변환에 사용하고, 스키마 형태 변경에는 사용하지 마세요. willMigratedidMigrate 클로저는 데이터에 대해 동작하는 코드를 위한 것이지, 스키마가 바뀌었음을 선언하기 위한 것이 아닙니다. 스키마 형태 변경은 lightweight stage를 통해 흘러갑니다.

  3. 합성 테스트 데이터뿐 아니라 실제 V1 데이터로 마이그레이션을 테스트하세요. 합성 라운드트립에서는 통과하는 마이그레이션이 엣지 케이스가 있는 프로덕션 형태의 데이터에서는 여전히 실패할 수 있습니다(스키마가 다루지 못한 nullable 필드, 타임아웃에 걸리는 대규모 데이터셋 등). 테스트 비용은 적지만, 첫 실행에서 마이그레이션이 크래시하는 비용은 실재합니다.

전체 Apple Ecosystem 클러스터: 타입이 지정된 App Intents; MCP 서버; 라우팅 질문; Foundation Models; 런타임 vs 툴링 LLM 구분; 세 가지 표면; single source of truth 패턴; 두 개의 MCP 서버; Apple 개발을 위한 hooks; Live Activities; watchOS 런타임; SwiftUI 내부 구조; RealityKit의 공간 멘탈 모델; SwiftData 스키마 규율; Liquid Glass 패턴; 멀티 플랫폼 출시; 플랫폼 매트릭스; Vision 프레임워크; Symbol Effects; Core ML 추론; Writing Tools API; Swift Testing; Privacy Manifest; 플랫폼으로서의 접근성; SF Pro 타이포그래피; visionOS 공간 패턴; Speech 프레임워크; 내가 쓰기를 거부하는 것들. 허브는 Apple Ecosystem Series에 있습니다. AI 에이전트와 함께하는 더 넓은 iOS 맥락은 iOS Agent Development 가이드를 참고하세요.

FAQ

SchemaMigrationPlan은 항상 필요한가요?

아니요. 스키마 버전이 하나뿐인 앱(최초 릴리스이거나, lightweight 변경만 해 온 앱)은 SchemaMigrationPlan이 필요 없습니다. ModelContainer 이니셜라이저는 스키마의 모델을 직접 받습니다. migrationPlan: 파라미터는 custom 마이그레이션 stage를 처음 선언할 때(또는 개발자가 명시적인 버전 사다리를 처음 선언하고 싶을 때) 필요해집니다.

내 변경이 lightweight인지 어떻게 알 수 있나요?

Apple의 lightweight 대상 목록1: entity/attribute/relationship 추가, 제거, @Attribute(originalName:)으로 이름 변경, relationship 카디널리티 변경, delete rule 지정입니다. 변경이 이 중 하나에 해당하고 모델 클래스 구조가 그 외에 변하지 않았다면, 마이그레이션은 자동이며 VersionedSchema 사다리가 필요 없습니다. 변경이 데이터 변환(계산, 분할, 데이터 이동)을 요구한다면 custom입니다.

willMigratedidMigrate를 둘 다 설정할 수 있나요?

네. 두 클로저는 개별적으로는 선택 사항이지만 둘 다 제공할 수 있습니다. willMigrate는 SwiftData가 마이그레이션하기 전에 이전 스키마의 context에 대해 실행되고, didMigrate는 후에 새 스키마의 context에 대해 실행됩니다. 둘은 각각 준비와 마무리를 담당합니다.

마이그레이션이 에러를 throw하면 어떻게 되나요?

에러는 ModelContainer 초기화 밖으로 전파됩니다. 컨테이너 열기가 실패합니다. 앱의 동작은 개발자가 에러를 어떻게 처리하느냐에 달려 있습니다. 어떤 앱은 복구 UI를 표시하고, 어떤 앱은 백업으로부터 복원을 시도하며, 어떤 앱은 손상된 store를 삭제하고 새로 시작합니다. SwiftData는 마이그레이션 실패 시 사용자 데이터를 조용히 삭제하지 않습니다. 실패는 앱이 처리해야 할 몫입니다.

프로덕션 데이터에 영향을 주지 않고 마이그레이션을 테스트하려면 어떻게 하나요?

임시 파일 URL을 가리키는 ModelContainer를 만들어 V1 데이터를 채운 다음, 마이그레이션 플랜을 포함한 새 컨테이너로 여는 테스트 타겟을 만드세요. 마이그레이션된 데이터가 기대와 일치하는지 검증하세요. 이 패턴은 단위 테스트와 통합 테스트 모두에서 동작합니다. 가장 현실적인 결과를 얻으려면 실제 프로덕션 형태의 데이터베이스 복사본을 사용하세요.

iOS 26의 클래스 상속이 기존 스키마와 함께 동작하나요?

네, lightweight 마이그레이션과 함께 동작합니다. 상속을 도입하는 앱은 새 스키마 버전(예: V4)으로 올라가 MigrationStage.lightweight(fromVersion: V3.self, toVersion: V4.self)를 선언합니다. flat한 부모 클래스 프로퍼티는 그대로 유지되고, 서브클래스 고유의 프로퍼티는 inline default와 함께 추가됩니다. SwiftData의 lightweight 마이그레이션이 구조적 변경을 처리합니다.

References


  1. Apple Developer Documentation: VersionedSchema and SchemaMigrationPlan protocol references. The migration model. See also the related guide Adopting SwiftData for a Core Data app for the full schema-evolution narrative. 

  2. Apple Developer: SwiftData: Dive into inheritance and schema migration (WWDC 2025 session 291). The introduction of SwiftData class inheritance in iOS 26. 

  3. Apple Developer Documentation: MigrationStage with the .lightweight(fromVersion:toVersion:) and .custom(fromVersion:toVersion:willMigrate:didMigrate:) cases. 

  4. Apple Developer Documentation: MigrationStage.custom(fromVersion:toVersion:willMigrate:didMigrate:) for the case signature. The willMigrate-runs-against-old-context and didMigrate-runs-against-new-context semantics are documented in WWDC 2025 session 291 SwiftData: Dive into inheritance and schema migration, the same session referenced for the iOS 26 inheritance addition. 

  5. WWDC 2026 SwiftData Group Lab (session 8017). Paraphrased from a locally transcribed recording of the WWDC 2026 SwiftData Group Lab; Apple publishes no official captions for the labs. The widget-and-extension migration gating (one process owns the migration, the error path is the migration signal, the migrated version is stored in a UserDefault) and the stage-count progress technique (override the per-stage didMigrate handler to report stage N of M, since no dedicated progress API exists) were described by the SwiftData engineering panel. The SchemaMigrationPlan and MigrationStage.custom didMigrate symbols are confirmed against the Apple Developer documentation cited in 1 and 4; the absence of a dedicated progress API reflects the panel’s own framing during the lab. 

관련 게시물

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

SwiftData의 API는 매크로 두 개입니다. 비용은 출시 이후에 발생합니다. 선택적 필드는 저렴한 마이그레이션이고, 비선택적 추가에는 VersionedSchema가 필요합니다.

12 분 소요

iOS 27의 SwiftData: Observation과 History

iOS 27은 SwiftData에 ResultsObserver를 통한 일급 변경 감지, HistoryObserver를 통한 영구 히스토리 관찰, 그리고 codable 속성 저장을 제공합니다.

8 분 소요

The Skills My Agent Could Not See

Five skills in my Claude Code setup reached the model as bare names. The description budget drops routing information si…

14 분 소요