서론
네 번째 WWDC23 SwiftData 영상입니다.
이걸 들으면 드디어 한 개 빼고 다 들었네요 ㅎㅎ
CoreData를 대체할 수 있는 새로운 프레임워크라서 흥미도 생기고 빨리 접하고 싶은 마음에 달리고 있습니다.
지금까지 들은 내용은 굉장히 쉽게 느껴졌는데... Dive deeper into SwiftData 영상도 쉬울지는 모르겠네요 ㅋㅋ;
SwiftData 영상은 총 5개로 순서는 아래와 같습니다.
- Meet SwiftData
- Build an app with SwiftData, Migrate to SwiftData
- Model your schema with SwiftData
- 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에서 마이그레이션을 전달하고 설정한 코드입니다.
이제 버전에 따라 적절한 마이그레이션이 수행됩니다.
감사합니다.
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.