XLOG

[Swift] CoreData 본문

Swift

[Swift] CoreData

X_PROFIT 2024. 6. 8. 23:20

보통 앱 내의 데이터를 저장할 때 많이 사용하는 것이 UserDefault 와 CoreData 이다. 

UserDefault 의 경우 앱의 설정(라이트모드, 다크모드 등) 과 같은 간단한 데이터를 저장하는데 사용한다면, CoreData 의 경우 좀 더 복잡한 데이터를 저장하는데 사용한다고 한다.

처음 Swift 를 접하고 코어데이터란게 있다 라는 얘기를 주변사람들에게 들었을 때는 DB 같은건가 하는 생각을 했었다.

하지만 CoreData 는 단순히 데이터베이스라고 생각하긴 어렵다.

CoreData 는 프레임워크다. 어플리케이션에서 offline으로 사용할 수 있는 영구적인 데이터를 저장, 관리를 도와주는 프레임 워크로 icloud 계정을 통해 다양한 device 에 데이터를 동기화까지 가능하게 해준다.

 

공식문서의 CoreData Stack 을 보면 Persistent Container 는 Model, Context, Store coordinator 로 구성이 되어 있는걸 볼 수 있다. Model 은 우리가 알고있는 데이터의 스키마로 생각을 하면 된다. Store Coordinator 는 저장소, 그리고 Context 는 일종의 Transaction으로 NSManagedObjectModel 을 CRUD 를 해준다. 위에 세 요소를 연결해주는 역할을 Persistent Container 가 해준다.

가볍게 CoreData에 대한 설명이 끝났으니 이제 사용법에 대해 알아보자.

1. NSPersistenceContainer 생성

2. 생성한 container 를 통해 persistence Store 를 load 하여 영구저장소와 연결

3. container의 viewContext 를 통해 fetch, 혹은 NSManagedObjectModel 을 생성, 변경, 삭제 등의 작업 진행

4. 변경사항을 저장

 

의 순서로 진행이 된다.

우선 프로젝트를 생성할 때 storage 에서 CoreData 를 생성하거나

새로운 파일을 만들 때 DataModel 을 만들면 된다. 그러면 프로젝트 내부에 filename.xcdatamodeld 의 파일이 보일 것이다.

 

이전 DiffableDataSource 설명에 사용했던 TodoList 프로젝트에 CoreData를 연결해 볼 것이기에, 그 때 정의한 Todo 구조체 와 같은 형태의 프로퍼티를 가지는 Todo Entity 를 생성하였다.

그후 NSManagedObject 를 자동으로 생성하게되면

해당 파일이 생성되고, 내부를 살펴보면

이렇게 자동으로 Todo 객체가 생성된다. 여기서 unwrappedTitle, unwrappedDeadline 은 추가한 내용이다. 이제 준비물이 완료 되었으니 PersistentContainer 를 생성해보자.

import CoreData

final class PersistenceManager {
    static let shared = PersistenceManager()
    
    let container: NSPersistentContainer
    
    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "TodoListWithUIKit")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as? NSError {
                fatalError("\(error)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

이 container 내부에 viewContext(transaction) 을 사용해서 NSManagedObject를 생성, 수정 등 관리가 가능하다.

func fetchTodos() {
    let request = Todo.fetchRequest()
    request.sortDescriptors = [NSSortDescriptor(keyPath: \Todo.deadline, ascending: false)]
    do {
        self.todos = try PersistenceManager.shared.container.viewContext.fetch(request)
    } catch {
        print(error)
    }
    updateView()
}

이전 TodoList 프로젝트에 ViewModel 에서 사용했던 [Todo] 에 CoreData를 연결해서 사용하기 위한 fetch 함수 이다.

위에 보여진 Todo 객체에 정의된 fetchRequest를 통해 NSFetchRequest 를 생성하고, 해당 request를 이용하여 viewContext로 원하는 request를 불러올 수 있다.

 

원하는 데이터를 추가, 수정, 삭제 시

func addTodo() {
	// 해당 Context에 Todo 객체를 생성
    // Transaction 엔 이미 새로운 Todo가 생성되음
	var todo = Todo(context: PersistenceManager.shared.container.viewContext)
    todo.id = UUID().uuidString
    todo.title = "title"
    todo.deadline = Date()
    todo.isCompleted = false
    
    do {
    	// 변화된 Transaction을 PersistenceStore 에 저장
    	try PersistenceManager.shared.container.viewContext.save()
    } catch {
    	print(error)
    }
}


func updateTodo(_ selectedTodo: Todo) {
	var todo = selectedTodo
    todo.title = "Change Title"
    ...
    do {
    	try PersistenceManager.shared.container.viewContext.save()
    } catch {
    	print(error)
    }
}

func deleteTodo(_ deleteTodo: Todo) {
	PersistenceManager.shared.container.viewContext.delete(todo)
}

 

문제점

CoreData를 연결한 후 프로젝트에서 Todo 객체를 Update 하여 데이터의 변경을 확인했지만, snapShopt 에 이를 적용을 . 할경우 update 된 타이틀을 반영하지 못했다.

 

func updateView() {

    let filtered = self.todos.filter({ !$0.isCompleted })
    print("filtered todo list>>>>>>>>>>>>>>>>>>>>>")
    for todo in filtered {
        print(todo.unwrappedTitle)
    }
    var snapshopt = NSDiffableDataSourceSnapshot<CollectionViewSection, Todo>()
    snapshopt.appendSections([.main])
    snapshopt.appendItems(filtered)
    self.dataSource.apply(snapshopt, animatingDifferences: true)
}

그래서 편법으로 snapshot의 배열을 비운 다음 다시 적용을 하여 변화를 두 번 주는 편법을 사용했다..사실 이렇게 된다면 비효율적이다. DiffableDataSource 의 장점은 변화를 캐치하여 효율적으로 그 변화에 대한 redraw를 해주는 것으로 파악하고 있는데. 이렇게 되면 모든 데이터를 다시 그려주게 된다고 생각한다. 이 부분은 시뮬레이터에서만 나타나는 문제인지, 아님 코드의 문제가 있는 것인지 조금 더 확인을 해봐야 할 것 같다..

 

임시 방편 코드

func updateView() {
    let temp = NSDiffableDataSourceSnapshot<CollectionViewSection, Todo>()
    self.dataSource.apply(temp, animatingDifferences: false)

    let filtered = self.todos.filter({ !$0.isCompleted })
    print("filtered todo list>>>>>>>>>>>>>>>>>>>>>")
    for todo in filtered {
        print(todo.unwrappedTitle)
    }
    var snapshopt = NSDiffableDataSourceSnapshot<CollectionViewSection, Todo>()
    snapshopt.appendSections([.main])
    snapshopt.appendItems(filtered)
    self.dataSource.apply(snapshopt, animatingDifferences: true)
}