Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
SwiftUI/SwiftUI(Intermediate)

2. CoreData 코어데이터 기본 CRUD

by Toughie 2023. 5. 21.

⭐️CoreData⭐️

앱을 사용하다가 종료해도 데이터가 남아있어야 하는 경우가 많다. 

작은 데이터라면 userDefaults, AppStorage 등을 활용할 수도 있지만, 더 크고 많은 데이터를 

안정적으로 아이폰에 저장하기 위해서는 CoreData를 사용해야한다.

 

https://developer.apple.com/documentation/coredata

 

Core Data | Apple Developer Documentation

Persist or cache data on a single device, or sync data to multiple devices with CloudKit.

developer.apple.com

'영구적으로 데이터를 기기에 저장'이 코어데이터 프레임워크의 핵심 기능이다.

'로컬 저장'

 

그러면 코어데이터는 데이터베이스인가? 정확히 말하면 DB의 기능도 있는 것이지, 코어데이터 == DB는 아니다.

코어데이터는 '객체 그래프 관리 프레임워크'이다.  객체 그래프가 뭔데..🤦🏻‍♂️

 

객체 그래프는 메모리에 있는 객체들과 이 객체들간의 관계를 말한다.

(ex.클래스나 구조체를 만들고 다른 객체에서 호출하면 객체간 관계가 형성된다.)

 

메모리 안에서는 수많은 객체들이 관계를 형성하고 있다.

이러한 관계도를 그대로 하드디스크(아이폰)에 저장하기 위해 존재하는 것이 코어데이터이다.

 

내부적으로는 SQlite를 저장소 유형으로 사용한다고 한다.

 

또한 코어데이터는 앱의 모델 레이어 객체 관리를 위한 프레임워크이기도 하다.

name, age라는 프로퍼티를 가진 Dog 모델이 있다고 치자.

이 모델은 코어데이터의 데이터 모델 에디터를 통해서 Entity(Dog), 그리고 Attribute(properties)로 만들어져 관리된다.

* 엔티티는 테이블, 컬럼은 어트리뷰트라고 이해하면 된다.(SQL 측면)

 

요약

코어데이터는 객체들 간의 관계, 그리고 모델 레이어 객체 관리를 프레임워크이고 핵심 기능은

데이터를 로컬 디바이스(아이폰)에 저장하는 것이다. (cloudKit을 통해 다른 디바이스 연동도 가능)

 

어떻게 쓰는지 간단한 예시를 통해 알아보자.

DataModel 파일을 만들고, Entity와 Attribute를 추가해줬다.

 

먼저 다양한 뷰에서 코어데이터 코드를 활용할 가능성이 있기 때문에

뷰모델로 형성해준다.

//  Created by Toughie on 2023/05/21.
//

import SwiftUI
import CoreData

class CoreDataViewModel: ObservableObject {
    
    let container: NSPersistentContainer
    
    @Published var savedEntities: [MyEntity] = []
    
    init() {
        self.container = NSPersistentContainer(name: "TestContainer")
        loadCoreData()
        fetchItems()
    }
    
    ...

코어데이터의 구조를 단순화 하면 위와 같은 그림으로 나타낼 수 있는데 이는 클래스로 이루어져 있고

묶어서 'Core Data Stack'이라고 한다.

NSPersistentContainer는 Core Data Stack을 초기화 하고 관리하기 위한 인터페이스를 제공해 준다.

주요 역할은 데이터베이스를 생성하고, 데이터 모델과 연결해서 데이터의 영속성을 관리하는 것이다.

영구컨테이너 초기화

    let container: NSPersistentContainer
    
    @Published var savedEntities: [MyEntity] = []
    
    init() {
        self.container = NSPersistentContainer(name: "TestContainer")
        loadCoreData()
        fetchItems()
    }

컨테이너를 초기화 해주는 부분이다.

이렇게 데이터 모델을 만들어 준 상태에서 이 데이터 모델 파일명을 넣어준다. ("TextContainer")

 

영구스토어 로딩

    func loadCoreData() {
        container.loadPersistentStores { description, error in
            if let error = error {
                print("ERROR LOADING CORE DATA. \(error)")
            } else {
                print("코어 데이터 로드 성공")
            }
        }
    }

.loadPersistentStores메서드는 비동기적으로 작동하고, 호출되면
NSPersistentContainer는 데이터베이스를 설정하고 로드하기 위해 필요한 작업을 수행한다.

completionHandler block

데이터베이스 로드가 성공하거나 실패했을 때 호출되는 클로저.
NSPersistentSotreDescription은 DB의 구성 정보와 관련된 정보를 제공한다.(데이터베이스 유형,위치 옵션 등)

Error?는 데이터베이스 로드 중 발생한 오류를 나타내는 값이다. 오류가 없으면 nil로 설정됨.

 

모델 파일명을 잘 설정했다면 에러가 나지 않을 것이다.

 

Read (FetchRequest) _데이터 검색 / 조회

    func fetchItems() {
        let request = NSFetchRequest<MyEntity>(entityName: "MyEntity")
        
        do {
            savedEntities = try container.viewContext.fetch(request)
        } catch let error {
            print("Error fetching: \(error)")
        }
    }

NSFetchRequest는 Core Data에서 데이터를 검색하기 위해 사용되는 클래스이다.

데이터베이스로부터 특정 조건을 충족하는 데이터를 검색하고 가져오는 데 사용된다.

 

request

데이터 요청서를 작성하고 (request) 이 요청서를 통해 데이터를 받아온다고 볼 수 있다.

검색에 대한 세부 정보 포함(엔티티 이름, 조건, 정렬 순서 등)

예시에서는 엔티티 이름만 포함되어 있음.

 

viewContext 

NSPersistentContainer에서 생성된 기본 NSManagedObjectContext

일반적으로 UI작업에 사용되는 주요 컨텍스트로 '메인 스레드에서 실행'됨.

데이터 작업을 수행하고 변경 사항을 관리하는 데 사용된다.

 

fetch

데이터를 검색하는 메서드. NSFetchRequest 객체를 파라미터로 받는다. (그래서 request, 요청서를 만들어 줬음)

[MyEntity]를 반환함.

 

요청서에 따라 검색된 결과룰 미리 생성해둔

@Published var savedEntities에 할당.

 

do try catch문을 통해 에러 처리가 되어있음.

 

데이터 저장

    func saveData() {
        do {
            try container.viewContext.save()
            fetchItems()
        } catch let error {
            print("Error saving: \(error)")
        }
    }

.save() 

NSManagedObjectContext에서 변경된 데이터를 저장하는 메서드.

변경된 데이터를 데이터베이스에 반영함.

 

데이터 저장 후 fetchItems()를 호출해 변경된 데이터를 검색할 수 있도록 업데이트함.

 

Create _ 데이터 생성/추가

    func addItem(text: String) {
        let newItem = MyEntity(context: container.viewContext)
        newItem.name = text
        saveData()
    }

새로운 entity를 생성하고 이 객체는 container.viewContext에 속하고 이 컨텍스트에서 관리됨.

String을 받아서 엔티티의 attribute에 할당해주고 있다.

변경사항 저장을 위해 saveData()를 호출해 준다.

 

새로운 엔티티 생성, 그리고 저장!

 

Update _ 데이터 수정/ 업데이트

    func updateItem(entity: MyEntity) {
        guard let currentName = entity.name else { return }
        let newName = currentName + "⭐️"
        entity.name = newName
        saveData()
    }

탭제스쳐를 통해 이름에 별을 붙이는 코드이다.(업데이트 예시를 위한 코드)

엔티티 자체에 텝 제스쳐를 붙였기 때문에 이와 같은 코드가 작동할 수 있다.

엔티티의 어트리뷰트에 변경사항이 생겼기에 또한 saveData()를 호출한다.

 

Delete _ 데이터 삭제

    func deleteItem(indexSet: IndexSet) {
        guard let index = indexSet.first else { return }
        let entity = savedEntities[index]
        container.viewContext.delete(entity)
        saveData()
    }

* 이번 예시에서는 List를 통해 뷰가 구현되어 있다.

.onDelete()메서드를 통해 indexSet을 받아와서 

savedEntities 배열에서 해당 entity를 지워준다.

지워주고 나면 다시 변경사항 저장을 위해 saveData()를 호출한다.

 

뷰 적용

CRUD/ 앱 종료 후 실행해도 저장 잘 되어있음.

* 가독성을 위해 modifier는 제거함.

struct CoreDataPrac: View {
    //@StateObject로 코어데이터뷰모델 초기화
    @StateObject var vm = CoreDataViewModel()
    
    //텍스트필드 텍스트 바인딩용 변수
    @State var textFieldText: String = ""
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                TextField("Type something here", text: $textFieldText)
                    
                Button {
                    guard !textFieldText.isEmpty else { return }
                    //textFieldText를 통해 새로운 엔티티 생성
                    //뷰모델의 addItem을 실행한다.
                    vm.addItem(text: textFieldText)
                    textFieldText = ""
                } label: {
                    Text("SAVE")
                    
                }
                List {
                //뷰모델의 savedEntities배열을 loop
                    ForEach(vm.savedEntities) { entity in
                    //name이 nil인 경우 방지.(for safe)
                        Text(entity.name ?? "NONAME")
                        //탭하면 업데이트 하도록.
                            .onTapGesture {
                                vm.updateItem(entity: entity)
                            }
                    }
                    //스와이프를 통해 삭제. 뷰모델의 deleteItem 호출
                    .onDelete(perform: vm.deleteItem)
                }
            }
            .navigationTitle("CoreData")
        }
    }
}

 

아주 기초만 살펴봤는데.. 

CoreData를 완전히 이해하고 자유자재로 사용하는 것은 많은 연습이 필요해 보인다.😅

 

References

https://www.youtube.com/watch?v=nalfX8yP0wc&list=PLwvDm4VfkdpiagxAXCT33Rkwnc5IVhTar&index=15
https://www.youtube.com/watch?v=BPQkpxtgalY&list=PLwvDm4VfkdpiagxAXCT33Rkwnc5IVhTar&index=16

https://velog.io/@nala/iOS-Core-Data%EB%8A%94-%EB%8C%80%EC%B2%B4-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80

https://baechukim.tistory.com/m/145https://developer.apple.com/documentation/coredatahttps://icksw.tistory.com/224