ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Model your schema with SwiftData (feat. WWDC 2023)
    SwiftData 2023. 6. 13. 12:02

    안녕하세요. 그린입니다🍏

    이번 포스팅에서는 이제 SwiftData의 모델 스키마들에 대해 알아보려 합니다🙋🏻

     

    Utilizing schema macros

    스키마 매크로를 최대한 활용할 수 있는 방법을 볼까요?

    스키마 매크로를 활용하면 앱에서 완벽하게 작동하도록 지속적인 환경의 동작을 커스텀하게 지정할 수 있어요.

    @Model
    final class Trip {
      var name: String
      var destination: String
      var start_date: Date
      var end_date: Date
      
      var bucketList: [BucketListItem]? = []
      var livingAccommodation: LivingAccommodation?
      ...
    }

    기본적으로 해당하는 모델이 있을때 여기서 각 여행의 이름 즉, name이 고유한지 확인하진 않습니다.

    동일한 이름의 Trip 간에는 충돌이 발생하죠.

     

    이를 위해 @Attribute 스키마 매크로를 활용할 수 있어요.

     

    @Attribute

    @Attribute(.unique) var name: String

    해당 스키마 매크로로 유니크 옵션을 사용해 수정할 수 있어요.

    SwiftData는 영구적인 백엔드에 저장하는 모든 Trip이 고유한 이름을 가지도록 보장하여 생성합니다.

    만약 해당 이름의 Trip이 존재하면 뒷단에서 최신 값으로 업데이트합니다.

    그걸 바로 upsert라고 부릅니다.

    upsert는 insert로 부터 시작하여 기존 데이터와 충돌하면 기존 데이터의 속성을 업데이트 합니다.

    숫자, 문자열, UUID와 같은 기본 값 타입이나 일대일 관계를 맺을 수 있는 다른 속성에 대해서도 유니크 제약을 설정할 수 있죠.

     

    그럼 위에서 start/end_date에 언더바를 없애고 싶으면 어떻게 할까요?
    기존 모델은 유지한채 가져가야 한다면 아래와 같이 해줄 수 있어요.

    @Attribute(originalName: "start_date") var startDate: Date
    @Attribute(originalName: "end_date") var endDate: Date

    원래 네이밍에서 매핑하면 데이터 손실을 방지할 수 있죠.

     

    정리해보면 @Attribute 매크로는 대용량 데이터를 외부에 저장하고 변환 가능한 항목을 지원하는 등 많은 작업을 해줍니다.

     

    이제 relationship에 대해 한번 살펴볼께요🕺🏻

     

    @Relationship

    위 코드에서 버킷리스트와 숙소에 대한 정보도 해당 Trip이 사라지면 삭제되길 원할 수 있죠.

    그럴때는 아래 코드처럼 관계 매크로를 이용해 맺어줍니다.

    @Relationship(.cascade)
    var bucketList: [BucketListItem]? = []
    
    @Relationship(.cascade)
    var livingAccommodation: LivingAccommodation?

    이젠 여행을 삭제하면 저 관계들의 데이터도 삭제되죠!

    해당 매크로는 최소 및 최대 관계 개수를 지정하는 기능 등 다양한 기능들이 있습니다.

     

    자 그럼 모델에서 만약 지속적으로 저장되는 속성이 아닌 값을 넣고 싶다면 어떻게 해야 할까요?

     

    @Trasient

    @Transient
    var tripViews: Int = 0

    단순하게 @Transient 매크로를 사용해주면 해당 프로퍼티는 저장되지 않습니다.

    즉, 불필요한 데이터가 지속되는것을 방지해줍니다.

    또한, 임시 프로퍼티에 대한 기본값을 제공해줍니다.

     

    이이서 좀 더 스키마를 발전하는 방법을 살펴볼께요 🏄🏻‍♂️

     

    Evolving schemas

    앱이 변경됨에 따라 스키마 마이그레이션을 통해 스키마를 발전시키는 방법을 보겠습니다.

    프로퍼티 추가 및 제거와 같이 스키마를 변경하면 데이터 마이그레이션이 발생합니다.

    SwiftData를 사용하면 이 마이그레이션이 쉬워집니다😃

     

    VersionedSchema와 SchemaMigrationPlan

    두 기능이 가능하게 해줍니다.

     

    1️⃣ VersionedSchema

    SwiftData 모델을 변경하여 앱의 새 버전을 출시할 때마다 이전에 출시한 스키마를 캡슐화하는 VersionedSchema를 정의할 수 있습니다.

    즉 버전마다 SwiftData의 변경 사항을 확인할 수 있어요.

     

    2️⃣ SchemaMigrationPlan

    VersionedSchemas를 따라 SchemaMigrationPlan을 생성합니다.

     

    이 순서로 마이그레이션을 진행할 수 있습니다.

     

    마이그레이션에도 두가지 방법이 있습니다.

     

    1️⃣ Lightweight

    경량 마이그레이션에는 다음 앱 릴리즈를 위해 기존 데이터를 마이그레이션하기 위한 추가 코드가 필요하지 않습니다.

    단순히 Date와 같은 프로퍼티에 originalName을 추가하거나 관계에 대한 삭제 규칙을 지정하는것과 같은 수정들은 경량 마이그레이션으로 충분합니다.

     

    그러나 Trip의 이름을 고유하게 만드는것과 같은 변경은 경량 마이그레이션에 적합하지 않습니다.
    커스텀 마이그레이션이 필요해요!

     

    2️⃣ Custom

    변경 사항에 대해 커스텀한 마이그레이션 단계를 생성해야 합니다.

    즉, 예를들어 Trip의 이름이 고유해지기전에 중복을 제거할 수 있습니다.

     

    그럼 커스텀한 마이그레이션은 어떻게 하는 걸까요?

     

    Custom Migration

    우선 VersionedSchema를 지정해줘야 합니다.

    enum SampleTripsSchemaV1: VersionedSchema {
        static var models: [any PersistentModel.Type] {
            [Trip.self, BucketListItem.self, LivingAccommodation.self]
        }
    
        @Model
        final class Trip {
            var name: String
            var destination: String
            var start_date: Date
            var end_date: Date
        
            var bucketList: [BucketListItem]? = []
            var livingAccommodation: LivingAccommodation?
        }
    
        // Define the other models in this version...
    }
    
    enum SampleTripsSchemaV2: VersionedSchema {
        static var models: [any PersistentModel.Type] {
            [Trip.self, BucketListItem.self, LivingAccommodation.self]
        }
    
        @Model
        final class Trip {
            @Attribute(.unique) var name: String
            var destination: String
            var start_date: Date
            var end_date: Date
        
            var bucketList: [BucketListItem]? = []
            var livingAccommodation: LivingAccommodation?
        }
    
        // Define the other models in this version...
    }
    
    enum SampleTripsSchemaV3: VersionedSchema {
        static var models: [any PersistentModel.Type] {
            [Trip.self, BucketListItem.self, LivingAccommodation.self]
        }
    
        @Model
        final class Trip {
            @Attribute(.unique) var name: String
            var destination: String
            @Attribute(originalName: "start_date") var startDate: Date
            @Attribute(originalName: "end_date") var endDate: Date
        
            var bucketList: [BucketListItem]? = []
            var livingAccommodation: LivingAccommodation?
        }
    
        // Define the other models in this version...
    }

    이렇게 버전마다 스키마 모델을 설정해둘 수 있습니다.

     

    그 다음으로 SchemaMigrationPlan을 구성해 릴리즈간 마이그레이션을 처리하는 방법을 나타내야 합니다.

    enum SampleTripsMigrationPlan: SchemaMigrationPlan {
        static var schemas: [any VersionedSchema.Type] {
            [SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self]
        }
        
        static var stages: [MigrationStage] {
            [migrateV1toV2, migrateV2toV3]
        }
    
        static let migrateV1toV2 = MigrationStage.custom(
            fromVersion: SampleTripsSchemaV1.self,
            toVersion: SampleTripsSchemaV2.self,
            willMigrate: { context in
                let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV1.Trip>())
                          
                // De-duplicate Trip instances here...
                          
                try? context.save() 
            }, didMigrate: nil
        )
      
        static let migrateV2toV3 = MigrationStage.lightweight(
            fromVersion: SampleTripsSchemaV2.self,
            toVersion: SampleTripsSchemaV3.self
        )
    }

    마이그레이션의 순서 지정 후 경량 혹은 커스텀 마이그레이션인지 지정해줍니다.

    그리고 stages에 담는것이죠.

    보시면 V1에서 V2까지는 데이터가 마이그레이션 되기 전에 작업을 수행할 수 있는 커스텀한 구현을 할 수 있어요.

    예를들어 willMigrate에서 보이는것처럼 마이그레이션 발생 전 Trip의 중복을 제거할 수 있습니다.

     

    이제 마이그레이션을 다 지정했으니 실제 마이그레이션을 동작시켜야 합니다.

     

    struct TripsApp: App {
        let container = ModelContainer(
            for: Trip.self, 
            migrationPlan: SampleTripsMigrationPlan.self
        )
        
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
            .modelContainer(container)
        }
    }

    현재 스키마와 마이그레이션 플랜으로 ModelContainer를 설정하기만 하면 간단히 끝납니다🙏🏻

     

    마무리

    아주 쉬운것 같은데 실제로 써보면 다를 수도..?

     

    참고 자료

    https://developer.apple.com/wwdc23/10195

     

    Model your schema with SwiftData - WWDC23 - Videos - Apple Developer

    Learn how to use schema macros and migration plans with SwiftData to build more complex features for your app. We'll show you how to...

    developer.apple.com

    'SwiftData' 카테고리의 다른 글

    Build an app with SwiftData (feat. WWDC 2023)  (12) 2023.06.13
    Meet SwiftData (feat. WWDC 2023)  (8) 2023.06.08
Designed by Tistory.