코드네임 :

🍏 SwiftData와 데이터베이스 본문

👥Club/🍀UMC🍀

🍏 SwiftData와 데이터베이스

비엔 Vien 2025. 4. 9. 16:32

와 졸려

 

[ SwiftData ]

 

@Model

  • SwiftData에서 데이터 모델을 정의할 때 사용하는 속석 래퍼입니다.
  • 클래스에 붙여서 데이터베이스 테이블처럼 사용할 수 있도록 지정합니다.
  • @Model은 반드시 참조 타입(Class)에만 사용할 수 있습니다.
@Model
class Task {
    var title: String
    var isDone: Bool
    var createdAt: Date

    init(title: String, isDone: Bool = false, createdAt: Date = .now) {
        self.title = title
        self.isDone = isDone
        self.createdAt = createdAt
    }
}

 


@Attribute

  • 특정 속성에 대한 세부 설정을 할 때 사용합니다.
  • 인코딩 방식, 기본값, 제약 조건 등을 지정할 수 있
@Attribute(.unique) var email: String
@Attribute(.externalStorage) var photoData: Data?
  • @Attribute(.unique)
    • 고유한 값만 허용한다는 의미입니다.
    • 위 코드로 설명을 하면,  같은 값을 가진 email을 가진 두 개의 객체는 데이터베이스에 동시에 존재할 수 없습니다.
    • 보통 회원가입 기능을 구현할 때, 이메일이나 사용자 ID는 중복되면 안되겠죠?!
  • @Attribute(.externalStorage)
    • 파일처럼 덩치가 큰 데이터를 데이터베이스 내부가 아닌 외부에 저장하겠다는 의미입니다.
    • SwiftData는 내부적으로 이 속성을 가진 데이터를 앱의 샌드박스 파일 시스템에 따로 저장합니다.
    • 이미지, 동영상, 오디오 등 Data 타입으로 저장되는 큰 바이너리 파일을 그대로 SQLite에 저장하면 앱 성능에 나쁜 영향을 줄 수 있어요.

@Relationship

  • 모델 간 관계를 설정할 때 사용합니다.
  • 명시하지 않아도 배열을 통해 관계가 자동 인식되긴 하지만, 복잡한 관계를 다룰 때 명시적으로 사용하면 좋습니다.
  • 필요한 것만 선택해서 설명합니다.
@Relationship(deleteRule: .cascade) var tasks: [Task] // deleteRule 속성은 아래 4개입니다.
  • .cascad
    • 부모가 삭제되면, 연결된 자식도 자동으로 함께 삭제
  • .nullify
    • 부모가 삭제되면, 자식 객체의 관계만 끊고 객체 자체는 남겨둠
  • .deny
    • 부모 객체가 자식과 연결된 상태이면 삭제 자체를 막음
  • .noAction
    • 부모가 삭제되더라도, 자식 객체는 아무 변화 없이 남겨짐
@Relationship(.unique) var profile: Profile?
  • .unique
    • 1 : 1 관계를 나타냄 즉, 하나의 자식 객체가 여러 부모에게 동시에 속할 수 없게 만드는 옵션
    • 만약 1 : N 관계를 나타내고 싶으면 참조 타입을 배열로 감싸면 됨. 즉, [Profile]? 이렇게 감싸면 1 : N 관계가 됩니

 

@Model
class User {
    var name: String
    @Relationship(inverse: \Book.owner) var books: [Book]
}

@Model
class Book {
    var title: String
    @Relationship(inverse: \User.books) var owner: User
}
  • invers
    • 두 모델의 관계 속성이 서로 연결되어 있다는 것을 명시합니다. 즉, 양방향 참조 관계!!
    • 양방향으로 연결된 관계를 명확하게 알려줍니다.

@Transient

  • 영속적으로 저장되지 않는 속성을 만들 때 사용합니다.
  • 주로 계산된 값(computed-like 저장 값)이나 UI용 속성 등 일시적 데이터를 처리할 때 사용됩니다.
@Transient var tempID: UUID = UUID()

 


@ModelContext

  • 저장소 컨텍스트를 SwiftUI 뷰 또는 ViewModel에서 사용하기 위한 환경 속성입니다.
  • @Model 내부에서는 직접 사용하지 않지만, SwiftData 전체에서 필수 역할이라 함께 이해하면 좋습니다.

 

@Query

@Query는 SwiftData에서 데이터를 읽어오는(read) 데에 사용하는 아주 핵심적인 속성

 

[ 읽기 기본 사용법 ]

 

<전체 조회>

@Query var tasks: [Task]

값이 새롭게 업데이트 되면 뷰 또한 최신 상태로 자동 업데이트 됩니다.

대신 조건이 있습니다. 무조건 View 안에 선언해야 합니다. 뷰모델 선언 아닙니다

 

<조건 필터링 조회>

@Query(filter: #Predicate<Task> { $0.isDone == false })
var pendingTasks: [Task]


<정렬 적용>

@Query(sort: \Task.createdAt, order: .reverse)
var recentTasks: [Task]

@Query는 읽기 전용 입니다.

데이터를 쓰거나 수정하려면 @Environment(\.modelContext)로 ModelContext를 주입받아야 합니다.

 

[ 쓰기 기본 사용법 ]

<새로운 데이터 삽입>

let newTask = Task(title: "새 할 일")
context.insert(newTask) // 이 부분 중요
try? context.save() // 이 부분 중요

 

<데이터 수정>

task.isDone.toggle()
try? context.save()

 

<데이터 삭제>

context.delete(task)
try? context.save()

 

⬇️ Todo 만들기

<DataModel.swift>

@Model
class Task {
    @Attribute(.unique) var title: String
    var isDone: Bool
    var createdAt: Date

    @Transient var isBeingEdited: Bool = false // 현재 편집 중인지 나타내는 임시 상태 변수 생성

    init(title: String, isDone: Bool = false, createdAt: Date = .now) {
        self.title = title
        self.isDone = isDone
        self.createdAt = createdAt
    }
}

 

<SwiftDataPracticeApp.swift> (메인 앱 구조) 

import SwiftUI
import SwiftData

@main
struct SwiftDataPracticeApp: App {
    var body: some Scene {
            WindowGroup {
                ContentView()
            }
            .modelContainer(for: Task.self) // 나는 Task라는 모델을 저장하고 불러올거야!! 그러니까 이 모델을 위한 저장소를 준비해줘!! 라고 말하는 거에요! 중요합니다!! 꼭 작성해야 돼요!!
        }
}

 

<ContentView>

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var context
    @Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task]
    @State private var newTaskTitle: String = ""

    var body: some View {
        NavigationStack {
            VStack {
                HStack {
                    TextField("할 일 입력", text: $newTaskTitle)
                        .textFieldStyle(.roundedBorder)

                    Button("추가") {
                        addTask()
                    }
                    .disabled(newTaskTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
                }

                List {
                    ForEach(tasks) { task in
                        HStack {
                            Button {
                                toggleDone(task)
                            } label: {
                                Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
                                    .foregroundStyle(task.isDone ? .green : .gray)
                                    .imageScale(.large)
                            }

                            VStack(alignment: .leading) {
                                Text(task.title)
                                    .strikethrough(task.isDone)
                                Text(task.createdAt.formatted(date: .numeric, time: .shortened))
                                    .font(.caption)
                                    .foregroundStyle(.gray)
                            }
                        }
                    }
                    .onDelete(perform: deleteTask)
                }
            }
            .navigationTitle("할 일 목록")
        }
    }

    private func addTask() {
        let trimmed = newTaskTitle.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }

        let task = Task(title: trimmed)
        context.insert(task)
        try? context.save()
        newTaskTitle = ""
    }

    private func toggleDone(_ task: Task) {
        task.isDone.toggle()
        try? context.save()
    }

    private func deleteTask(at offsets: IndexSet) {
        for index in offsets {
            context.delete(tasks[index])
        }
        try? context.save()
    }
}
#Preview {
    ContentView()
}