[iOS] SwiftUI에서 CoreData 써보기

룰루날라·2022년 6월 5일
9
post-thumbnail

오늘은 직접 코어데이터를 앱에 적용해보면서 코어데이터가 어떻게 작동하는지 알아보도록 하자.

보면서 따라하는 용도보다는 각 과정을 왜 그렇게 하는건지, 각 객체의 역할은 무엇인지가 궁금해 공부한 내용이다보니 중간중간 내용이 많을 예정이다!

(Raywenderlich의 Core Data with SwiftUI Tutorial: Getting Started를 바탕으로 글을 작성하였다.)

1️⃣ Data Model 만들기

Core Data를 사용하기 위해 제일 먼저 해야할 일은 객체들의 구조를 정의하기 위해, 즉 객체 그래프를 추가하기 위해서 data model 파일(.xcdatamodeld 파일)을 만드는 것이다.

새 프로젝트를 만들 때 추가하는 방법과 이미 진행중인 프로젝트에 추가하는 방법 2가지가 있다.

1-1. .xcdatamodeld 파일 추가하기

새로운 프로젝트

프로젝트를 만들 때, Use Core Data 체크박스에 체크를 해주면 자동으로 데이터 모델 파일을 추가해준다.
프로젝트 내비게이터에서 바로 확인할 수 있다.

새로운 프로젝트를 생성할 때 체크를 하면 코어데이터에 필요한 코드들을 자동으로 생성해주기 때문에 더욱 편리하다.

진행중인 프로젝트

새로운 파일을 만들어 Core Data 섹션에 있는 Data Model 파일을 추가해준다. 그러면 끝!

1-2. 데이터모델의 Entity, Attribute 정의하기

데이터 모델은 앱의 객체와 그 객체들이 어떻게 서로 관련되어 있는지를 나타내는 객체 그래프에 대한 정보를 담고 있는 것이다.

객체 그래프에 대한 정보를 우리는 .xcdatamodeld 파일에 Entity와 Attributes, Relationships, Fetched Properties를 추가해 알려줄 것이다.

EntityAttributes, Relationships, Fetched Properties는 쉽게 말하면 모델 계층의 객체(class)프로퍼티라고 볼 수 있다.

예를 들어 코어데이터를 사용하지 않던 앱에서 아래와 같은 모델 타입이 있었다면

struct Movie {
  let genre: String
  let releaseDate: Date
  let title: String
}

코어데이터를 사용하는 앱에서는 위 타입은 제거하고, 아래와 같이 data model editor에서 Entity와 Attributes를 추가해 사용해준다.

즉, 우리의 모델 계층의 객체 그래프를 데이터모델 파일에 추가해주는 과정이라고 보면된다.
이 객체들이 UI의 source of data가 되는 것!
그런데 밑에 있는 Relationships와 Fetched Properties는 무엇일까?

Attributes, Relationships, Fetched Properties

Attributes와 Relationships, Fetched Properties는 모두 Entity의 프로퍼티(코어데이터 프레임워크가 관리하는 객체의 값)다. 어떤 차이가 있을까?

  • Attributes
    • Entity를 구성하는 프로퍼티들. 모델 타입의 프로퍼티라고 생각하면 쉽다.
    • 기본적으로 optional로 체크되어 있는데, 웬만하면 optional로 하지 않는 게 좋다. 특히 숫자의 경우는 더욱! 코어 데이터가 사용하는 Objective-C의 null과 SQL의 null이 다르다보니, 데이터베이스의 null은 빈 문자열이나 빈 데이터 블록과 동일하지 않다.
  • Relationships
    - Entity들끼리 어떻게 관련되어 있는지, 변경 사항이 Entity간에 어떻게 전달될지를 정의한다.
    즉, 2개 이상의 Entity가 있는 경우 서로 어떤 영향을 끼치는지를 정의해놓는거다.
    - 이전 글에서의 예시를 통해 보면, FamilyPerson Entity 사이의 관계를 Relationships에서 정의해줘야 한다.

    1. FamilyPerson을 배열로 갖고 있으므로 Relationships를 하나 추가해주고 이름과 Destination을 정의해준다.
    (Destination은 relation을 갖는 상대 Entity를 이야기한다.)
    2. Relationships를 정의해줄 땐 Inverse도 정해줘야 한다. Inverse는 역방향의 relationships를 이야기하는데 inverse를 설정해줘야 변화가 생겼을 때 코어 데이터가 양쪽으로 모두 변화를 전달할 수 있다.
    만약 Family의 members라는 Relationships에서 Inverse를 설정하지 않았다면, Person 객체를 지워도 Family는 여전히 Person을 가리키고 있을거다.
    3. Person을 배열로 여러개 가질 거기 때문에 Data Model Inspector에서 Type을 To Many로 바꿔준다. 끝!
  • Fetched Properties
    • 코어데이터에서 fetch해온 프로퍼티를 정의한다. weak, one-way relationships를 나타낸다.
    • 주로 여러 persistent store 사이에 relatioships를 만들기 위해 사용된다. persistent store를 여러개 만드는 경우가 흔하지 않기 때문에 잘 사용되지는 않는다.

1-3. Core Data Model File로부터 swift class를 생성한다.

Data Model Inspectector를 보면 Codegen이라는 설정칸이 있다.

코어데이터와 관련한 코드를 자동으로 생성할지, 수동으로 만들어줄지를 Codegen을 통해 결정할 수 있다.

Manul/None
Editor-Create NSManagedObject Subclass을 하면 xcode will generate the class files for what you’ve defined in the core data model.

하나의 Entity에 대해 CoreDataClass파일과 CoreDataProperties파일을 만든다.

  • CoreDataClass
    • Entity를 표현하는 클래스. 생성된 클래스는 자동으로 NSManagedObject를 상속한다.(코어데이터가 serialize, deserialize할 수 있도록)
      → data model의 모든 entity들은 NSManagedObject의 서브클래스다. 코어데이터가 라이프사이클과 persistence를 다루기 때문에 managed object다.
    • 이제 코어데이터는 이 클래스를 저장하고, 필요할 때 가져올 수 있다.
    • Custom Code를 적는 파일
  • CoreDataProperties
    • CoreDataClass의 익스텐션. Attribute들을 프로퍼티로 갖고 있다.
    • fetchRequest() 라는 메소드도 있음.
    • 변경하지 않는 파일

Class Definition
기본 설정인 Class Definition은 위의 두 파일을 자동으로 만들어 알아서 관리하기 때문에 개발자가 해당 파일을 볼 수 없어 커스텀 코드를 추가할 수 없다.

Category/Extension
CoreDataClass 파일만 만들어주고 CoreDataProperties 파일은 Xcode가 자동으로 관리해준다.

이후 Core Data Persistent Container에 대한 참조를 얻는다. persistent container로부터 managed object context를 얻는다. managed object context를 통해서 객체를 생성하고 코어데이터에 저장할 수 있다.

2️⃣ Core Data Stack 세팅하기

data model을 만든 뒤에는 앱의 모델 레이어를 협력해서 도와주는 클래스들을 추가해줘야한다.
이러한 클래스들을 Core Data Stack이라고 한다.

Core Data Stack이란?

앱의 모델 레이어를 협력해서 서포트하는 객체들로 Core Data를 사용하기 위해서는 Core Data Stack이 반드시 필요하다!

Core Data Stack은 아래와 같이 구성되어 있는데 각각이 하는 역할을 이해하는 것이 중요하다.

NSManagedObjectModel

Entities라고 불리는 모델 객체와 다른 Entity들과의 관계를 정의한다.

데이터 모델을 로드하고 Core Data Stack에 노출한다.

FaveFlicks 앱에서는 FaveFlicks.xcdatamodeld 안에서 Movie entity를 managed object model의 부분으로 정의할 것이다. NSManagedObjectModel 클래스를 코드의 managed object model에 접근하는데 사용할거다.

NSManagedObjectContext

데이터베이스에 있는 객체를 보고 접근하게 해주는 window

Core Data Stack의 핵심. managed object context로 Core Data Stack과 소통하기 때문에 이 객체를 개발자가 가장 많이 사용한다. managed object model이나 persistent store coordinator는 직접 사용하는 경우는 많지 않다.

앱의 타입의 인스턴스에 생긴 변화를 트랙한다. 항목을 생성, 수정, 삭제 또는 검색할 수 있는 in-memory 스크래치패드인 NSManagedObjectContext. 일반적으로 Core Data와 상호 작용할 때 managed object context로 작업한다.

NSPersistentStoreCoordinator

Core Data Stack의 접착제같은 존재다. managed obejct model과 managed object context에 대한 참조를 유지한다.

managed object model을 통해서 데이터 모델을 이해하고, persistent store를 관리한다.

실제 데이터베이스를 관리해 store로부터 앱의 타입의 인스턴스들을 저장하고 fetch한다.

⇒ 복잡해보이지만 NSPersistentContainer 덕분에 아주 쉽게 Core Data Stack을 추가할 수 있다.

NSPersistentContainer

  • Core Data Stack을 캡슐화한 컨테이너. 즉, NSPersistentContainer 클래스가 NSManagedObjectModel, NSManagedObjectContext, NSPersistentStoreCoordinator를 프로퍼티로 가지고 있다. managed object model, persistent store coordinator, managed object context의 생성을 핸들링해서 Core Data Stack의 생성과 관리를 간단하게 해준다.
  • 코어 데이터의 데이터베이스라고 볼 수 있다.

Core Data는 내부적으로 SQLite를 사용하는 Object-Oriented Database다.

Object-Oriented Database라고 하는 이유는, 데이터베이스에 직접 CRUD를 명령하는 일반적인 데이터베이스와 달리 코어 데이터는 데이터베이스와 직접 소통하지 않는다.
객체를 만들고, 앱의 객체와 상호작용하면 코어데이터가 알아서 관리해주는 개념인 것.

여기서 코어 데이터의 데이터베이스는 NSPersistentContainer, 그 위에서 데이터베이스에 접근하게 해주는 window가 NSManagedObjectContext다.
데이터를 persistent store에 저장하거나 가져올 때 이 context를 반드시 거쳐야 한다.

Core Data Stack 추가하기

그럼 이제 본격적으로 Core Data Stack을 추가해보자.

PersistenceController

먼저, persistent container를 만들어줄거다. 위에서 말했던 것처럼 Core Data Stack은 복잡해보이지만, persistent container가 캡슐화를 하고 있어 persistent container만 만들어주면 모두 관리해줄 수 있다.

struct PersistenceController {
  static let shared = PersistenceController()

  let container: NSPersistentContainer

  init(inMemory: Bool = false) {
    container = NSPersistentContainer(name: "FaveFlicks")
    container.loadPersistentStores { _, error in
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    }
  }
}
  1. FaveFlicks라는 이름의 contatiner를 만들어준다.
    .xcdatamodeld의 파일 이름과 container의 이름이 같아야 한다.
  2. 컨테이너에게 persistent store(Core Data Stack을 간단히 set up할 수 있는)를 로드하라고 시킨다.

⚠️ UIKit 앱과 다른 점

  • 일반적으로 앱 시작 중에 Core Data를 초기화하기 때문에 UIKit 앱의 경우, AppDelegate에서 만들어주지만 SwiftUI 앱에서는 별도의 PersistenceController를 만들어서 ~App.swift 파일에서 PersistenceController의 인스턴스를 사용해줄거다.
  • 또한 AppDelegate에서 만들어주는 경우 persistent container를 처음 사용할 때까지 인스턴스화를 연기하기 위해서 container를 lazy 변수로 만든다.
  • 그런데 지금의 경우 PersistenceController가 static을 사용해 타입 프로퍼티로 인스턴스를 생성하기 때문에 lazy 변수를 사용할 때처럼 사용 시점에 초기화가 된다.sav

끝이다! Core Data Stack을 set up하는 건 이렇게만 하면 된다.

3️⃣ 뷰와 연결하기

3-1. 데이터를 디스크에 저장하는 메소드 만들기

// PersistenceController

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)")
    }
  }
}
  1. persistent container의 viewContext를 가져와 데이터베이스에 접근한다.
  2. context.hasChanges를 사용해 화가 있을때만 저장한다.
  3. context를 저장한다.

3-2. 앱의 나머지 부분에 연결하기

struct CoreDataPracticeApp: App {
  let persistenceController = PersistenceController.shared

  var body: some Scene {
    WindowGroup {
      ContentView()
        .environment(\.managedObjectContext, persistenceController.container.viewContext)
    }
  }
}

context를 Environment를 통해 모든 뷰에 전해준다.

4️⃣ CRUD

4-1. 뷰에서 managed object context 사용하기

// ContentView

@Environment(\.managedObjectContext) var managedObjectContext

managedObjectContext에 @Environment를 통해 접근한다.

// ContentView

func saveContext() {
  do {
    try managedObjectContext.save()
  } catch {
    print("Error saving managed object context: \(error)")
  }
}

Entity를 생성하고 업데이트, 삭제할 때 우리는 이 작업을 모두 managed object context를 통해서 수행한다.

변경 사항이 생긴 걸 디스크에 쓰려면 컨텍스트를 저장해야 하므로 항상 managedObjectContext.save()를 실행해줘야 한다.

4-2. 객체를 fetch해오기

@FetchRequest(
  entity: Movie.entity(),
  sortDescriptors: [
    NSSortDescriptor(keyPath: \Movie.title, ascending: true)
  ],
  predicate: NSPredicate(format: "genre contains 'Action'")
) var movies: FetchedResults<Movie>
  1. 데이터를 fetch해서 화면에 보여주기 위해서는 FetchRequest를 통해 persistent store에서 fetch해온다.

    @FetchRequest를 통해 데이터를 fetch해오는 경우, 해당 데이터에 변경이 생길 때마다 계속해서 fetch해오기 때문에 데이터와 UI 사이의 싱크를 맞추는데 최고의 도구다!

  2. 어떤 Entity를 fetch해올지, 어떤 순서로 정렬할지 등의 정보를 프로퍼티 래퍼 내부에서 정해준다.

  3. 위 코드의 경우에는 코어데이터에 저장되어있는 모든 Movie를 가져올텐데 만약 객체를 filter하고 싶거나 특정 entity만 가져오고 싶은 경우엔 프로퍼티 래퍼 안에서 predicate 매개변수를 추가해 사용한다. 특정 연도의 '영화'만 가져오거나 특정 장르와 일치하는 것과 같이 결과를 제한하기 위해 predicate로 fetched request를 구성한다.
    predicate의 경우, 문법이 정해져있기 때문에 어떤 정보를 filter해서 가져올지 정해진 문법에 따라 format을 작성해줘야 한다.

4-3. CRUD 메소드 구현하기

// ContentView

func addMovie(title: String, genre: String, releaseDate: Date) {
  let newMovie = Movie(context: managedObjectContext)

  newMovie.title = title
  newMovie.genre = genre
  newMovie.releaseDate = releaseDate

  saveContext()
}

func deleteMovie(at offsets: IndexSet) {
  offsets.forEach { index in
    let movie = self.movies[index]
    self.managedObjectContext.delete(movie)
  }
  saveContext()
}

추가하고 삭제하는 메소드도 구현해준다!



코어 데이터를 적용해보는 방법을 알아보았다.
처음 공부하기 시작할 때 러닝 커브가 있다는 이야기를 들었는데
하루만에 뚝딱 쓸 줄 알았던 글이 며칠이 걸렸다..ㅎ
정말 러닝커브가 있구나 하하

앱에서 직접 써보면서 더 공부해봐야지!



참고
https://www.raywenderlich.com/9335365-core-data-with-swiftui-tutorial-getting-started
https://cocoacasts.com/what-are-core-data-entities-and-attributes
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/index.html#//apple_ref/doc/uid/TP40001075-CH2-SW1
https://cocoacasts.com/what-is-the-core-data-stack

profile
즐거운 인생 (~-_-)~ ~(-_-~)

0개의 댓글