💾 SwiftData 완전정복
Core Data를 대체하는 현대적 데이터베이스. Swift 매크로로 간편하게 영구 저장소를 구현합니다.
⭐ 난이도: ⭐⭐
⏱️ 예상 시간: 1-2h
📂 App Frameworks
✨ SwiftData란?
SwiftData는 iOS 17+에서 사용 가능한 새로운 데이터 영속화 프레임워크입니다. Core Data를 완전히 대체하며, Swift 매크로를 활용해 훨씬 간단합니다.
📦 Model 정의
Todo.swift
import SwiftData import Foundation @Model final class Todo { var title: String var isCompleted: Bool var createdAt: Date init(title: String, isCompleted: Bool = false) { self.title = title self.isCompleted = isCompleted self.createdAt = Date() } }
🏗️ App 설정
TodoApp.swift
import SwiftUI import SwiftData @main struct TodoApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Todo.self) } }
📝 데이터 읽기 (@Query)
ContentView.swift
import SwiftUI import SwiftData struct ContentView: View { @Query private var todos: [Todo] @Environment(\.modelContext) private var modelContext var body: some View { NavigationStack { List { ForEach(todos) { todo in HStack { Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle") .foregroundStyle(todo.isCompleted ? .green : .gray) Text(todo.title) .strikethrough(todo.isCompleted) } .onTapGesture { todo.isCompleted.toggle() } } .onDelete(perform: deleteTodos) } .navigationTitle("할 일") .toolbar { Button("추가", systemImage: "plus") { addTodo() } } } } func addTodo() { let newTodo = Todo(title: "새 할 일") modelContext.insert(newTodo) } func deleteTodos(at offsets: IndexSet) { for index in offsets { modelContext.delete(todos[index]) } } }
🔍 Query 필터링 & 정렬
FilteredView.swift
struct FilteredTodoView: View { // 완료되지 않은 할 일만, 최신순 정렬 @Query( filter: #Predicate<Todo> { todo in !todo.isCompleted }, sort: \Todo.createdAt, order: .reverse ) private var incompleteTodos: [Todo] var body: some View { List(incompleteTodos) { todo in Text(todo.title) } } } // 여러 정렬 조건 @Query(sort: [ SortDescriptor(\Todo.isCompleted), SortDescriptor(\Todo.createdAt, order: .reverse) ]) private var sortedTodos: [Todo]
🔗 관계(Relationship) 정의
Models.swift
import SwiftData @Model final class Category { var name: String @Relationship(deleteRule: .cascade, inverse: \Todo.category) var todos: [Todo] = [] init(name: String) { self.name = name } } @Model final class Todo { var title: String var isCompleted: Bool var category: Category? init(title: String, category: Category? = nil) { self.title = title self.isCompleted = false self.category = category } }
💾 수동 저장 & 롤백
ManualSave.swift
struct TodoEditView: View { @Environment(\.modelContext) private var modelContext @State private var title = "" var body: some View { Form { TextField("제목", text: $title) HStack { Button("저장") { save() } Button("취소", role: .cancel) { rollback() } } } } func save() { let todo = Todo(title: title) modelContext.insert(todo) do { try modelContext.save() } catch { print("저장 실패: \(error)") } } func rollback() { modelContext.rollback() } }
🔧 ModelContext 직접 사용
FetchData.swift
import SwiftData func fetchTodos(context: ModelContext) throws -> [Todo] { let descriptor = FetchDescriptor<Todo>( predicate: #Predicate { !$0.isCompleted }, sortBy: [SortDescriptor(\Todo.createdAt, order: .reverse)] ) return try context.fetch(descriptor) } // 개수만 가져오기 func countTodos(context: ModelContext) throws -> Int { let descriptor = FetchDescriptor<Todo>() return try context.fetchCount(descriptor) }
🗑️ 삭제 규칙 (Delete Rule)
DeleteRule.swift
@Model final class Project { var name: String // cascade: Project 삭제 시 Task도 모두 삭제 @Relationship(deleteRule: .cascade) var tasks: [Task] = [] // nullify: Project 삭제 시 Task의 project를 nil로 // @Relationship(deleteRule: .nullify) // deny: Task가 있으면 Project 삭제 불가 // @Relationship(deleteRule: .deny) init(name: String) { self.name = name } } @Model final class Task { var title: String var project: Project? init(title: String, project: Project? = nil) { self.title = title self.project = project } }
🎯 Unique 제약 조건
UniqueModel.swift
import SwiftData @Model final class User { @Attribute(.unique) var email: String var name: String init(email: String, name: String) { self.email = email self.name = name } } // 동일한 email로 insert 시 자동으로 update됨
🧪 테스트용 In-Memory Container
PreviewContainer.swift
import SwiftData extension ModelContainer { static var preview: ModelContainer { let schema = Schema([Todo.self]) let configuration = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: schema, configurations: configuration) // 샘플 데이터 추가 let context = container.mainContext context.insert(Todo(title: "할 일 1")) context.insert(Todo(title: "할 일 2")) return container } } // Preview에서 사용 #Preview { ContentView() .modelContainer(ModelContainer.preview) }
💡 HIG 가이드라인
✅ @Query는 자동으로 UI 업데이트
✅ ModelContext는 자동 저장
✅ Core Data보다 80% 적은 코드
✅ 타입 안전성 보장
✅ Swift 매크로로 보일러플레이트 제거