티스토리 뷰

iOS

[iOS] CoreData

Peppo 2024. 3. 17. 14:40
728x90

 

 

존재는 알았지만 언제 공부할까 각 보다가 이제서야 해봅니다.

오늘은 영구적으로 로컬에 데이터를 저장하고 관리하는 CoreData에 대해 정리해보려고 합니다.

 


 

 

들어가기전에

Q: UserDefaults도 로컬에 저장할 수 있지 않나? 객체도 저장할 수 있잖아? (UserDefault로 객체를 저장해보자!)

A: 맞지만, 사실 UserDefaults는 간단한 정보를 저장하기에 적합합니다.

 

그리고 

CoreData를 사용해보려는 가장 큰 이유!

CloudKit을 사용해 여러장치에 동기화가 가능합니다.

 

예를들어, A라는 앱을 쓰다가 다른 아이폰 기기로 바꿔도 CloudKit을 통해 기존에 사용했던 A앱의 데이터를 그대로 가져다 쓸 수 있다는것.

 


 

1. CoreData 핵심 3요소

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

 

 

1. NSManagedObjectModel

NSManagedObjectModel

  • 앱의 타입, 속성, 관계를 설명하는 앱의 모델 파일을 나타냅니다.
  • 한개 이상의 NSEntityDescription을 포함합니다.
  • entity를 설명하는 데이터베이스에요.

 

2. NSManagedObjectContext

  • 앱의 인스턴스에 대한 변경사항을 추적하는 역할을 합니다.

 

3. NSPersistentStoreCoordinator

  • 앱 타입의 인스턴스를 저장하고 불러옵니다.

 

4. NSPersistentContainer

  • 모델, 컨텍스트 및 스토어 코디네이터를 한번에 설정합니다.

 

 

2. CoreData 튜토리얼

 

1. 프로젝트 만들기

 

Storage - CoreData를 선택하고 프로젝트를 만들어줍니다.

 

 

아래처럼 데이터베이스 모양의 파일이 생성 됐으면 성공!

 

2. CoreData를 체크안했을 경우 or 추가로 .xcdatamodeld 만들 때

새 파일 생성 창 (cmd + n) > Data Model을 선택해서 생성해주면 2번과 같이 .xcdatamodeld 파일이 생성됩니다.

 

 

3. Entity, Attribute 추가

저는 Profile이라는 정보를 CoreData에 저장하려고 합니다.

예를들어 Profile이라는 모델에 name, age, gender의 프로퍼티를 저장한다고 가정하고 해보겠습니다.

아래와 같은 형태겠죠.

struct Profile {
    let name: String
    let age: Int
    let gender: String
}

 

 

Entity를 먼저 추가해줍니다.

Entity는 Model 이름이라 이해를 했어요.

 

 

 

Profile로 이름설정해주고, Attributes를 추가해줍니다.

name, age, gender가 되겠죠.

 

 

이렇게하면 기본적인 세팅은 완성이 됩니다.

 

 

4. Core Data Stack

AppDelegate의 가장 하단부분에 보면 아래와 같은 코드가 있는데요.

기본적으로 제공되는 주석 부분을 요약하면 내용은 이렇습니다.

 

persistentContainer를 설정하고 변경사항이 있으면 데이터를 저장합니다.

// AppDelegate.swift

// MARK: - Core Data stack

// persistentContainer 설정
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "Profile")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support

// 변경 사항이 있을 경우 데이터 저장
    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }

 

 

참고

꼭 위의 로직이 AppDelegate에 있을 필요는 없습니다.

 

이제 어느정도 구성이 되었으니 데이터를 저장, 불러오기, 삭제를 구현해 봅시다.

 

저는 CoreDataManager라는 싱글톤으로 관리하려고 합니다.

들어가기전에 싱글톤으로 만들어줄게요.

 

먼저 CoreData를 import 해주시고,

AppDelegate에 있던 Container설정, 변경사항 저장 로직을 가져오겠습니다.

// CoreDataManager.swift

import CoreData

final class CoreDataManager {

  static let shared: CoreDataManager = .init()
  
  private init() { }
  
  lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "Profile")
        container.loadPersistentStores { storeDescription, error in
            if let error = error as NSError? {
                print("Unresolved error: \(error)")
            }
        }
        return container
    }()
    
    var context: NSManagedObjectContext { return self.persistentContainer.viewContext }
    
    func saveContext() {
    if context.hasChanges {
    do {
        try context.save()
    } catch let error {
        print(CoreDataManagerError.saveError(error))
    }
}

 

 

5. 저장하기(Save)

순서는 아래와 같습니다.

  • entity를 설정합니다.
  • 해당 ManagedObject에 setValue로 값을 등록
  • context에 저장
@discardableResult
    func saveProfile(profile: Profile) -> Bool {
        let entity = NSEntityDescription.entity(forEntityName: "Profile",
                                                in: self.context)
        
        if let entity = entity {
            let managedObject = NSManagedObject(entity: entity, insertInto: self.context)
            managedObject.setValue(profile.name, forKey: "name")
            managedObject.setValue(profile.age, forKey: "age")
            managedObject.setValue(profile.gender, forKey: "gender")
            
            do {
                try self.context.save()
                print("저장완료: \(managedObject)")
                return true
            } catch let error {
                print(#function, CoreDataManagerError.saveError(error))
                return false
            }
        } else {
            return false
        }
    }

 

 

 

6. 불러오기(Fetch)

fetch를 하려면 fetch하고자하는 entity이름을 가진 NSfetchRequest가 필요합니다.

 

func fetchContext() -> [Profile] {
  let profilefetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Profile")

        do {
            let fetchResult = try self.context.fetch(profilefetchRequest)
            return fetchResult
        } catch let error {
            print(CoreDataManagerError.readError(error))
            return []
        }
    }

 

물론 위처럼 구현해도 상관없지만 추후 entity가 더 늘어날것을 고려한다면 제네릭을 사용하는게 좋아보입니다.

NSFetchRequest의 타입에 따라 유동적으로 데이터를 불러올수 있도록 아래와 같이 로직을 변경해줍니다.

func fetchContext<T: NSManagedObject>(request: NSFetchRequest<T>) -> [T] {
        do {
            let fetchResult = try self.context.fetch(request)
            return fetchResult
        } catch let error {
            print(CoreDataManagerError.readError(error))
            return []
        }
    }

 

외부에선 이렇게 사용되겠네요.

// ViewController.swift

private func fetchProfiles() {
        let profilefetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Profile")
        profiles = CoreDataManager.shared.fetchContext(request: profilefetchRequest)
    }

 

이부분도 뭔가 의존성을 분리할 수 있을것 같은데 리팩토링하면서 다음블로깅에 작성해보겠습니다. 

 

 

7. 삭제하기(Delete)

 

NSManagedObjectContext.delete는 NSManagedObject를 받아 해당 부분을 삭제합니다. 

 

 

내부로직은 해당 object를 파라미터로 받아 삭제하는걸로 구현했습니다.

// CoreDataManager.swift

@discardableResult
    func delete(object: NSManagedObject) -> Bool {
        self.context.delete(object)
        
        do {
            try self.context.save()
            return true
        } catch let error {
            print(#function, CoreDataManagerError.deleteError(error))
            return false
        }
    }

 

외부에선 아래와 같이 쓰도록 구현했습니다.

저는 선택된 cell의 index를 파라미터로 넘겨 해당하는 데이터를 삭제하도록 구현했습니다.

// ViewController.swift

private func deleteProfile(_ index: Index) {
  // profiles: [NSManagedObject]
  CoreDataManager.shared.delete(object: profiles[index])
}

 

 


 

Zedd님 블로그를 보면서 CoreData를 사용할때 패턴이 있는것 같은데 그 부분을 잘 파악할 수 있던것 같습니다.

 

이외에도 CoreData에는 많은 기능이 있는걸로 아는데 오늘은 간단하게 CRUD하는 부분만 다뤄봤습니다.

그동안 사이드프로젝트(BestEats)라는 앱을 개발하면서 로컬 데이터 관리를 UserDefaults로만 사용했는데 

이번에 공부하면서 리팩토링 해볼게 생겼네요.

위 예제로직에서도 수정할 부분이 있을것 같은데 다음블로깅에는 사이드프로젝트에 적용해본 후기와 위 예제를 조금 보완한내용을 담아볼것 같습니다. 

 

 

 

참고

1. CoreData

  1-2. CoreData 생성

  1-3. CoreData Stack

2. CoreData CRUD for Beginner

3. https://zeddios.tistory.com/987

 

728x90