WWDC/Swift

[Swift] WWDC23 - Model your schema with SwiftData

유정주 2023. 6. 13. 14:40
반응형

서론

네 번째 WWDC23 SwiftData 영상입니다.

이걸 들으면 드디어 한 개 빼고 다 들었네요 ㅎㅎ

CoreData를 대체할 수 있는 새로운 프레임워크라서 흥미도 생기고 빨리 접하고 싶은 마음에 달리고 있습니다.

지금까지 들은 내용은 굉장히 쉽게 느껴졌는데... Dive deeper into SwiftData 영상도 쉬울지는 모르겠네요 ㅋㅋ;

 

SwiftData 영상은 총 5개로 순서는 아래와 같습니다.

  1. Meet SwiftData
  2. Build an app with SwiftData, Migrate to SwiftData
  3. Model your schema with SwiftData
  4. Dive deeper into SwiftData

 

WWDC23에서는 영상 챕터를 지원합니다.

이번 포스팅도 영상 챕터를 기준으로 작성되었습니다.

 

 

Intro

이번 세션에서는 SwiftData용 스키마를 구축하는 방법을 다룹니다.

스키마 매크로를 최대한 활용할 수 있는 방법과 스키마 마이그레이션을 통해 스키마를 발전시킬 수 있는 방법을 소개합니다.

 

이번 세션은 SwiftData 개념을 설명하고 있지 않습니다.

따라서 Meet SwiftData Build an app with SwiftData를 선행한 후 시청하는게 좋습니다.

간단히 아래 두 개 개념이 중요합니다.

  • SwiftData는 데이터 모델링 및 관리를 위한 프레임워크이다.
  • SwiftData는 Swift 매크로 시스템을 사용한다.

 

아래는 이번 세션에서 활용한 모델입니다.

일반 클래스에 @Model 매크로를 사용해 SwiftData 모델로 변경했습니다.

자세히 살펴보면 name이 유니크한지 확인을 못함, camelcase 불일치(start_date, end_date) 등 약간의 문제가 있습니다.

이를 해결하기 위해 약간의 수정이 필요합니다.

 

 

Utilizing schema macro

@Attribute unique 옵션

먼저 name 속성을 유니크하게 설정하겠습니다.

@Attribute 스키마 매크로와 unique 옵션을 이용하면 됩니다.

이제 동일한 name 데이터를 저장하면 기존 데이터가 업데이트됩니다.

이 동작을 upsert라고 부릅니다.

 

 

unique 옵션은 기본 Value 타입(Numeric, String, UUID 등)이거나 1:1 relationship을 지정할 수 있는 고유 제약 조건에 적용할 수 있습니다.

 

 

@Attribute originalName 옵션

다음은 start_date, end_date의 camelCase를 Swift에 어울리게 바꿔줍시다.

만약 변수명 바꾸듯이 속성명을 바꾼다면 기존 속성의 이름이 변경되는게 아니라 새로운 속성이 생성되는 것입니다.

 

@Attribute 매크로의 originalName 옵션을 사용하면 새로운 이름을 기존 속성 이름에 매핑시킬 수 있습니다.

(Codable에서 CodingKey로 key 이름을 매핑하는 것과 동일한 동작임)

 

 

Relationship 설정

위 모델에서 Trip이 삭제되면 bucketList와 livingAccommodation을 함께 삭제하고 싶습니다.

(기본 동작은 속성 무효화임)

 

이는 @Relationship을 이용하면 구현할 수 있습니다.

cascade 삭제 규칙을 적용하여 연결된 데이터가 함께 삭제되도록 설정했습니다.

 

 

이외에도 @Relationship은 originalName modifier와 최소 및 최대 개수 지정 등을 지원합니다.

 

 

@Transient

새로운 클래스 프로퍼티가 필요한데 SwiftData에는 저장하고 싶지 않을 때가 있습니다.

이런 경우 @Transient를 사용하면 됩니다.

 

@Transient를 사용해 데이터를 본 횟수를 구현한 코드입니다.

이 속성은 SwiftData에 저장되지 않고 변수 역할만 하게 됩니다.

 

 

SwiftData 스키마 매크로에 대한 더 자세한 내용느 SwiftData 문서를 확인하세요.

 

 

Evolving schemas

지금까지의 내용처럼 앱은 언제든 변화할 수 있으며, 변경 내용이 릴리즈 버전마다 잘 적용되는지 확인해야 합니다.

SwiftData의 VersionedSchema와 SchemaMigrationPlan을 이용하면 쉽게 처리할 수 있습니다.

 

 

새 버전을 릴리즈할 때마다 이전 버전의 스키마를 캡슐화하는 VersionedSchema를 정의해야 합니다.

VersionedSchema를 통해 각 버전 간에 발생한 변경 사항을 알 수 있습니다.

그다음 VersionedSchema의 total order를 이용해 SchemaMigrationPlan을 생성합니다.

이러면 SwiftData가 필요한 마이그레이션을 순서대로 수행할 수 있습니다.

 

 

그다음에는 각 마이그레이션 stage를 정의합니다.

두 가지 타입의 마이그레이션 stage를 사용할 수 있습니다.

  • Lightweight 마이그레이션 Stage
  • Custom 마이그레이션 Stage

 

Lightweight 타입은 기존 데이터 마이그레이션을 위한 추가 코드가 필요하지 않습니다.

위 예시에서 Lightweight가 적절한 케이스는 originalName 설정과 releationship cascade 설정입니다.

 

name을 unique하게 설정한 케이스는 Custom Migration Stage가 적절합니다. (기존에는 데이터가 중복일 수 있기 때문)

Custom Migration Stage는 변경 사항에 대한 코드를 작성해야 합니다.

 

 

VersionedSchema 정의

먼저 VersionSchema를 캡슐화해보겠습니다.

 

버전1의 스키마를 SampleTripsSchemaV1으로 정의하겠습니다.

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...
}

초기의 Trip 모델과 동일합니다.

 

 

이제 V2를 정의하겠습니다.

V2는 name에 unique 제약을 추가한 버전입니다.

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...
}

 

 

마지막으로 V3을 정의하겠습니다.

V3은 originalName 적용 버전입니다.

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 정의

이제 VersionedSchema를 이용해 SchemaMigrationPlan를 구성하겠습니다.

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
    )
}

스키마 순서를 배열로 정의하고,

마이그레이션 stage를 lightweight 타입 또는 custom 타입으로 정의합니다.

 

마지막으로 각 단계에서 필요한 처리를 합니다.

예를 들어, V1에서 V2로 갈 때 name 중복 데이터를 제거해야 합니다.

위 코드에서는 willMigrate에 클로저를 전달하여 중복된 데이터를 제거하고 있습니다.

 

 

마이그레이션 수행

MoelContainer를 설정할 때 MigrationPlan을 전달하여 마이그레이션을 수행할 수 있습니다.

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

SwiftUI에서 마이그레이션을 전달하고 설정한 코드입니다.

 

 

이제 버전에 따라 적절한 마이그레이션이 수행됩니다.

 

 

감사합니다.


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형