이 글은 누구를 위한 것인가
- Core Data의 복잡한 설정 코드를 없애고 싶은 iOS 개발자
- SwiftData가 무엇인지, Core Data와 어떻게 다른지 알고 싶은 팀
- 기존 Core Data 앱을 SwiftData로 점진적으로 전환하려는 엔지니어
들어가며
SwiftData는 Swift 매크로 기반의 현대적 영속성 프레임워크다. Core Data 위에 구축됐지만, NSManagedObject나 .xcdatamodeld 파일 없이 @Model 하나로 시작할 수 있다.
이 글은 bluefoxdev.kr의 SwiftData 완전 가이드 를 참고하여 작성했습니다.
1. SwiftData vs Core Data 비교
[코드 비교]
Core Data:
.xcdatamodeld 파일 필요
NSManagedObject 상속
@NSManaged 속성
NSFetchRequest + NSPredicate
NSManagedObjectContext 수동 관리
SwiftData:
코드만으로 모델 정의
@Model 매크로 하나
일반 Swift 속성
#Predicate 타입 안전 술어
ModelContext 자동 관리
[SwiftData 지원 버전]
iOS 17+, macOS 14+, watchOS 10+
iOS 16 지원 필요하면 Core Data 유지
[마이그레이션 전략]
1. 신규 앱: SwiftData 사용
2. 기존 앱 iOS 17 최소 지원: SwiftData로 전환
3. iOS 16 지원 필요: Core Data + SwiftData 공존
또는 Core Data 유지
[데이터 마이그레이션]
Stage 1: SwiftData 모델 정의 (Core Data와 동일 스키마)
Stage 2: 앱 시작 시 Core Data → SwiftData 데이터 이전
Stage 3: Core Data 코드 제거
2. SwiftData 구현 및 마이그레이션
import SwiftData
import SwiftUI
// Core Data 방식 (기존)
// @objc(Task)
// class Task: NSManagedObject {
// @NSManaged var title: String
// @NSManaged var isDone: Bool
// @NSManaged var createdAt: Date
// }
// SwiftData 방식 (새로운)
@Model
final class Task {
var title: String
var isDone: Bool
var createdAt: Date
var priority: Int
// 관계 (to-one)
var category: Category?
init(title: String, priority: Int = 0) {
self.title = title
self.isDone = false
self.createdAt = Date()
self.priority = priority
}
}
@Model
final class Category {
var name: String
var colorHex: String
// 관계 (to-many)
@Relationship(deleteRule: .cascade, inverse: \Task.category)
var tasks: [Task] = []
init(name: String, colorHex: String = "#007AFF") {
self.name = name
self.colorHex = colorHex
}
}
// ModelContainer 설정
struct AppConfiguration {
static let schema = Schema([Task.self, Category.self])
static func makeContainer(inMemory: Bool = false) -> ModelContainer {
let config = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: inMemory,
allowsSave: true,
)
do {
return try ModelContainer(for: schema, configurations: [config])
} catch {
fatalError("ModelContainer 생성 실패: \(error)")
}
}
}
// SwiftUI 앱 진입점
@main
struct TaskApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(AppConfiguration.makeContainer())
}
}
// 뷰에서 사용
struct TaskListView: View {
@Environment(\.modelContext) private var context
// #Predicate: 타입 안전한 술어 (NSPredicate 대체)
@Query(
filter: #Predicate<Task> { !$0.isDone },
sort: [SortDescriptor(\Task.createdAt, order: .reverse)],
)
private var tasks: [Task]
var body: some View {
List(tasks) { task in
TaskRow(task: task)
}
.toolbar {
Button("추가") { addTask() }
}
}
private func addTask() {
let task = Task(title: "새 할 일")
context.insert(task)
// context.save()는 대부분 자동 처리
}
}
struct TaskRow: View {
let task: Task
@Environment(\.modelContext) private var context
var body: some View {
HStack {
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
.onTapGesture {
task.isDone.toggle() // 자동 감지 및 저장
}
Text(task.title)
Spacer()
}
.swipeActions {
Button("삭제", role: .destructive) {
context.delete(task)
}
}
}
}
// Core Data → SwiftData 데이터 마이그레이션
class DataMigrationManager {
static func migrateIfNeeded(
coreDataStack: CoreDataStack,
modelContext: ModelContext,
) async {
guard !UserDefaults.standard.bool(forKey: "swiftdata_migrated") else {
return
}
let cdContext = coreDataStack.viewContext
// Core Data에서 데이터 읽기
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Task")
guard let cdTasks = try? cdContext.fetch(fetchRequest) else { return }
// SwiftData로 변환
for cdTask in cdTasks {
let task = Task(
title: cdTask.value(forKey: "title") as? String ?? "",
priority: cdTask.value(forKey: "priority") as? Int ?? 0,
)
task.isDone = cdTask.value(forKey: "isDone") as? Bool ?? false
task.createdAt = cdTask.value(forKey: "createdAt") as? Date ?? Date()
modelContext.insert(task)
}
try? modelContext.save()
UserDefaults.standard.set(true, forKey: "swiftdata_migrated")
}
}
// 고급: 스키마 버전 마이그레이션
enum AppSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] = [Task.self]
@Model final class Task {
var title: String
init(title: String) { self.title = title }
}
}
enum AppSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] = [Task.self]
@Model final class Task {
var title: String
var priority: Int // 새로 추가
init(title: String, priority: Int = 0) {
self.title = title
self.priority = priority
}
}
}
enum AppMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] = [
AppSchemaV1.self,
AppSchemaV2.self,
]
static var stages: [MigrationStage] = [
MigrationStage.lightweight(
fromVersion: AppSchemaV1.self,
toVersion: AppSchemaV2.self,
)
]
}
class CoreDataStack {
var viewContext: NSManagedObjectContext { fatalError("stub") }
}
마무리
SwiftData는 iOS 17+에서만 사용 가능하다. 점진적 마이그레이션 순서: ①SwiftData 모델 정의(Core Data 스키마와 동일) → ②데이터 이전 코드 작성 → ③뷰 코드 변환(@FetchRequest → @Query) → ④Core Data 코드 제거. @Query는 뷰 리렌더링을 자동으로 처리해 NSFetchedResultsController가 필요 없다.