CoreData를 사용해보자

문인범·2024년 3월 16일

Swift

목록 보기
8/9

CoreData

Persist or cache data on a single device, or sync data to multiple devices with CloudKit.
단일 기기의 데이터 캐싱, 유지 또는 CloudKit을 이용한 여러기기의 데이터 동기화
not Database, 일종의 framework

개요

1. Persistence (저장)

Core Data는 데이터베이스의 직접적인 관리 없이 Swift or Objective-C를 사용해 저장하려고 하는 오브젝트들의 맵핑 디테일을 추상화할 수 있다.

2. Undo and redo of individual and batched changed (되돌리기 기능)

Core Data의 undo manager는 변화를 추적해 그 변화를 각각, 그룹별로 또는 전체를 한번에 되돌릴 수 있다.

흔들어서 되돌리기 기능 뜻하는듯 합니다.

3. Background data tasks

JSON 파싱과 같은 UI를 막을 수 있는 데이터 작업들을 백그라운드에서 진행할 수 있다.

그런 다음 캐싱 또는 저장을 하여 서버의 반복호출을 줄일 수 있다.

4. View synchronization

Core Data also helps keep your views and data synchronized by providing data sources for table and collection views.

5. Versioning and migration

Core Data includes mechanisms for versioning your data model and migrating user data as your app evolves.

특징

In-Memory 방식이 존재

사용 하려는 모든 데이터는 우선 메모리에 로딩되는 과정이 존재한다.
Core Data에서 읽고 쓰는 모든 데이터는 원칙적으로 메모리로 로드된 다음에 처리된다.
⇒ (영구 저장소를 아예 사용하지 않고 순수하게 인메모리 방식으로만 사용하는 것이 가능)

엔티티(Entity)를 통해 데이터 저장 구조 정의

일반적인 다른 DB는 테이블을 통해 정의한다.

  • 엔티티(Entity) : 데이터가 저장될 구조
  • 어트리뷰트(Attribute) : 엔티티의 하위 속성들을 정의하는 역할
  • 릴레이션(relation) : 엔티티끼리의 관계 정리
  • 페치속성(Fatched Properties) : 템플릿 형태로 만들어 놓은 것 ( 반복되는 요청이나 값만 바꾸어 비슷한 요청들을 묶어놓은 것 )

이들이 일종의 스키마(schema) 역할을 한다

데이터를 객체로 취급(테이블의 행, 레코드 하나하나를 독립된 객체로 사용)

예를들어 사원 정보의 레코드 정보 2개를 읽을 땐 2개의 사원 객체가 생성된다.

Core Data Stack

데이터 모델을 만든 후 앱의 모델 레이어를 서로 보조해주는 클래스들을 설정하면 된다.
이 클래스들을 Core Data Stack 이라고 부른다.

  • NSManagedObjectModel : 앱의 타입, 프로퍼티, 관계등을 서술해놓은 앱의 모델 파일을 나타낸다. (데이터베이스의 스키마)
  • NSManagedObjectContext : 앱 타입들의 인스턴스 변화를 추적한다. 데이터를 불러오거나 저장할 때 무조건 여기를 거치게 된다. 일종의 창구 역할
  • NSPersistentStoreCoordinator : 저장소에서 앱 타입들의 인스턴스를 저장하고 불러온다. (저장소는 얘를 거쳐서 왔다갔다 할듯?)
  • NSPersistentContainer : model, context, store coordinator를 한번에 설정한다. ( 하위 3개의 클래스를 포함하는 클래스)

앱이 시작되는 동안에 Core Data는 초기화를 진행한다.
persistant container를 delegate 안에 처음 사용되기 전까지 인스턴스화를 미루어야 하기 때문에 지연 저장 프로퍼티로 생성한다 ( lazy )

저장 과정

  1. NSManagedObjectContext 를 가져온다.
  2. Entity 를 가져온다
  3. NSManagedObject를 생성한다.
  4. NSManagedObject에 값을 설정해준다.
  5. NSManagedObjectContext를 저장한다.

저장소 사용법

UIKit은 사용법이 인터넷에 잘 나와있으므로 그거 보고 따라하면 된다.
SwiftUI는 비교적 적어 그 내용에 대해 적어보겠다.
특히 SwiftData라고 SwiftUI 친화적인 데이터모델이 있기 때문에 그것을 사용하는게 더 편리해 보이긴 한다.
하지만 SwiftData 또한 CoreData 기반이기 때문에 이거부터 공부하는게 맞겠지?

1. Data Model 생성 ( 프로젝트 생성 때 core data 선택시 안해도 됌)

Data Model 파일을 생성한다.

2. Add Entity, Attribute


왼쪽 하단의 Add Entity 를 눌러 엔티티를 생성하고 Attributes를 넣어주면 된다.
Entity가 일종의 스키마라고 생각하면 된다고 한다.
그리고 엔티티 이름을 잘 설정해놔야한다. 저 이름으로 쓰고 불러오기 때문이다.
이 외에도 여러가지 설정이 가능하다.

Class 부분에 Codegen이 있는 것을 볼 수 있다.
Code Generation의 약자로 옵션마다 설정이 약간씩 바뀌며 현재 Class Definition은 얘가 자동으로 데이터모델 클래스를 자동생성 해준다.
커스텀을 하고 싶은 경우에는 저 옵션을 입맛에 맞게 바꿔주면 된다.

3. NSPersistentContainer 생성

위에서 봤듯이 NSPersistentContainer 하나로 손쉽게 사용을 할 수 있기 때문에 이것을 만들면 된다.

import Foundation
import CoreData

struct PersistentController {
    static let shared = PersistentController()
    
    let container: NSPersistentContainer
    
    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "Model")
        container.loadPersistentStores { _, error in
            if let error = error as NSError? {
                fatalError("Unsolved Error \(error), \(error.userInfo)")
            }
        }
    }
    
    func saveContext() {
        let context = container.viewContext
        
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
    
}

싱글톤을 사용하여 단 하나의 객체에 접근하게 만들었다.
컨테이너에게 PersistentStores 를 로드 시킨다. ( 저장소 셋업 느낌이라고 한다)
saveContext는 데이터를 디스크에 저장하는 과정이다.

  1. 컨테이너의 Context에 접근한다.
  2. context에 변화가 있을 시에 저장한다.

기본적으로 앱 시작 중에 Core Data를 초기화 하기 때문에 기존 UIKit 에서는 AppDelegate 에서 만들며 인스턴스화를 늦추기 위해 lazy 하게 만든다.
하지만 SwiftUI는 AppDelegate가 없기 때문에 static을 사용해 lazy 하게 만든다.
(static 변수 → 전역 변수, 전역 변수 → lazy / ⇒ static = lazy)

4. 앱에 연결시키기

앱의 엔트리포인트로 가서 연결시켜주면 된다. (프로젝트명)App.swift ← 이거

import SwiftUI

@main
struct CoreDataStudyApp: App {
    let persistentController = PersistentController.shared
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistentController.container.viewContext)
        }
    }
}

shared를 불러와서 container의 context 를 environment로 설정한다.

5. View에서 사용하기

// in ContentView
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(
    entity: Movie.entity(),
    sortDescriptors: [.init(keyPath: \Movie.title, ascending: true)],
    animation: .spring)
var movies: FetchedResults<Movie>

@Environment 프로퍼티 래퍼를 통해 저장소에 접근한다.

@FetchRequest

SwiftUI에서 CoreData 사용을 위한 전용 fetch 프로퍼티 래퍼라고 한다. ㄷㄷ

다양한 옵션을 사용할 수 있다.

  • entity : 이건 사용할 entity 말하는 듯
  • sortDescriptors : 어떤 것을 기준으로 어떻게 정렬할지 정한다.
  • predicate : 쿼리 조건을 줄 수 있다. (일종의 필터링 역할이라고 한다.)
  • animation : 말 그대로 애니메이션

특히 @FetchRequest 로 데이터를 불러오면 데이터에 변화가 생길때 마다 fetch 해오기 때문에 UI와 연관되어 있을시에 사용하기 좋다.

6. CRUD 메소드

private func addMovie(title: String, genre: String, releaseDate: Date) {
    let movie = Movie(context: managedObjectContext)
        
    movie.title = title
    movie.genre = genre
    movie.releaseDate = releaseDate
        
    saveContext()
}
    
private func deleteMovie(at offsets: IndexSet) {
    offsets.forEach { index in
        let movie = movies[index]
        managedObjectContext.delete(movie)
            
        saveContext()
    }
}

생성, 삭제 후에 Context를 저장해주어야지 적용된다.

7. View에 표시하는 방법

난 ForEach를 사용해 간단하게 만들어봤다.

import SwiftUI

struct ContentView: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @FetchRequest(
        entity: Movie.entity(),
        sortDescriptors: [.init(keyPath: \Movie.title, ascending: true)],
        animation: .spring)
    var movies: FetchedResults<Movie>
    
    @State var isAddMovieViewPresented: Bool = false
    
    
    var body: some View {
        NavigationStack {
            VStack {
                if movies.isEmpty {
                    Text("Empty!")
                } else {
                    List {
                        ForEach(movies) { movie in
                            NavigationLink(movie.title ?? "nil") {
                                MovieDetailView(movie: movie)
                            }
                        }
                        .onDelete(perform: deleteMovie)
                    }
                }
            }
            .navigationTitle("Movie List")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button(action: addButton) {
                        Image(systemName: "plus")
                    }
                }
                
                ToolbarItem(placement: .topBarLeading) {
                    EditButton()
                }
            }
            .sheet(isPresented: $isAddMovieViewPresented) {
                AddMovieView(isPresented: $isAddMovieViewPresented)
                    .environment(\.managedObjectContext, managedObjectContext)
            }
        }
    }
    
    func addButton() {
        isAddMovieViewPresented.toggle()
    }
}

마무리

데이터베이스 말고도 다른 많은 기능들이 있다고 하는 만큼 시간나면 공부를 해야겠다.

캐시 부분도 궁금하다.

CoreData를 찾아보던 중 SwiftData가 있는 것을 확인했는데, 이건 SwiftUI 친화적이라 해서 이것도 공부해보면 재미가 있을거같다.

아무튼 오늘도 화이팅~

https://github.com/mooninbeom/CoreDataStudy
(전체 코드 참고용)

profile
월클 개발자를 향한 도전일지

0개의 댓글