[iOS] CoreData

Emily·2024년 12월 3일

CoreData는 앱을 통한 CRUD가 가능하도록 하는 프레임워크다. 앱에서 기기의 디스크에 데이터를 쓰고(Create), 읽고(Read), 수정하고(Update), 삭제(Delete)하도록 하는 것이다.

01) CoreData Model 생성

CoreData의 데이터 모델은 xcdatamodeld 파일로 생성된다. 이 파일은 프로젝트에서 Data Model 템플릿을 선택하여 생성할 수 있다.

02) Entity 생성 및 Attribute 정의

Data Model 파일을 생성하고 나면 이런 화면이 나온다.

여기서 Add Entity 버튼을 눌러 Entity를 추가할 수 있다.

엔티티가 뭘까? 사전적인 의미는 독립체라고 한다. 어떤 존재라는 의미도 갖고 있다. 프로그래밍 관점에서는 개체라고 부르는 게 적절할 것 같다. 엔티티는 코어데이터에서 사용하는 데이터 모델의 형식이다.

엔티티를 정의하기 전에, 구조체로 모델을 정의하는 걸 상상해보자.

struct PhoneBook {
	var name: String
    var phoneNumber: String
}

PhoneBook이라는 구조체가 namephoneNumber 프로퍼티를 갖고 있는 형태다. 이제 에밀리를 전화번호부에 등록하기 위해 인스턴스를 생성한다.

let emily = PhoneBook(name: "Emily", phoneNumber: "010-1234-5678")

에밀리의 전화번호를 계속 보관하기 위해서는, 이 데이터를 기기에 영구저장 해야할 것이다.(UserDefaults는 이걸 저장한다.)
이런 것처럼 CoreData에서 정의하는 데이터 모델의 형태는 Entity다.

Entity를 정의한 뒤 그 안에 프로퍼티처럼 들어가는 구성요소 AttributenamephoneNumber를 정의하고, 데이터 타입을 지정한 모습이다.

03) Code Generator

Entity를 생성하고 나면, 우측 인스펙터에서 Codegen이라는 항목을 볼 수 있다. 코드젠은 3가지 선택 항목이 있다. Entity를 가지고 코드를 작성할 때 코드 작성 방법을 선택하는 것이다.

  • Manual/None : Entity의 서브 클래스를 자동으로 생성하지 않고 개발자가 클래스를 작성
  • Class Definition : Entity의 서브 클래스를 자동으로 생성
  • Category/Extension : Entity 클래스와 함께 extension을 위한 파일까지 생성

Manual로 선택하고, Xcode → Editor → Create NSManagedObject Subclass를 눌러 클래스를 생성해주면, 자동으로 2개의 swift 파일이 생성된다.

import Foundation
import CoreData

// PhoneBook+CoreDataClass.swift
@objc(PhoneBook)
public class PhoneBook: NSManagedObject {

}

// PhoneBook+CoreDataProperties.swift
extension PhoneBook {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<PhoneBook> {
        return NSFetchRequest<PhoneBook>(entityName: "PhoneBook")
    }

    @NSManaged public var name: String?
    @NSManaged public var phoneNumber: String?
}

extension PhoneBook : Identifiable {

}

Entity와 같은 이름의 public class가 생성되었다.

  • 이 클래스가 상속하고 있는 NSManagedObjectCoreData 프레임워크에서 관리되는 객체를 나타내는 기본 클래스로, 엔티티와의 상호작용을 관리하며 속성 값의 저장 및 검색을 처리한다.
  • @nonobjcObjective-C에서는 동작하지 않고 Swift에서만 동작하는 메소드임을 명시하는 macro다.
  • fetchRequest() 메소드는 PhoneBook에 대한 여러가지 데이터 검색(데이터에 접근)을 돕는 함수다.
  • @NSManaged : CoreData에 의해 관리되는 객체를 의미한다.
  • Identifiale 프로토콜을 채택하는 건 PhoneBook 타입이 고유하게 식별될 수 있도록 하는 것이다.

04) 타입 프로퍼티 선언

CoreData 기능 구현과 직결되는 부분은 아니지만, 추후에 key 값으로 많이 사용될 값들을 PhoneBook 클래스에 선언해두면 유용하다.

@objc(PhoneBook)
public class PhoneBook: NSManagedObject {
	public static let className = "PhoneBook"
    public enum Key {
    	static let name = "name"
        static let phoneNumber = "phoneNumber"
    }
}

typealias Key = PhoneBook.Key

05) NSPersistentContainer

AppDelegate에 생성하는 Container로, 데이터의 영구 저장소다. 데이터를 저장하고 관리하는데 필요한 핵심 객체로, CoreData와 프로젝트 사이에 데이터를 주고 받을 수 있게하는 매개체다.

lazy var persistentContainer: NSPersistentContainer = {
	// name: 에 xcdatamodeld 파일 이름을 적어준다.
    let container = NSPersistentContainer(name: "CoreDataPractice")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

saveContext() 함수는 데이터에 변동 발생 시 호출되어 container에 변동사항을 저장한다.

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)")
        }
    }
}

06) container 선언

import UIKit
import CoreData

class ViewController: UIViewController {
    var container: NSPersistentContainer!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        self.container = appDelegate.persistentContainer
    }
}

07) 데이터 생성 (Create)

  1. PhoneBook 엔티티에 저장할 수 있도록 Attribute와 같은 타입의 데이터를 파라미터로 받도록 함수를 선언한다.
  2. NSEntityDescription의 메소드를 통해 PhoneBook 엔티티를 생성한다.
  3. containercontextNSManagedObject 객체를 생성한다.
  4. 객체에 setValue 메소드를 통해 값을 넣어준다.
  5. do-catch 문을 통해 container에 객체를 저장한다.
// view controller
// 함수 선언 (PhoneBook의 Attribute와 같이 name과 phoneNumber를 파라미터로 받는다.)
func createData(name: String, phoneNumber: String) {
	func createData(name: String, phoneNumber: String) {
    	// PhoneBook entity 생성
        guard let entity = NSEntityDescription.entity(forEntityName: PhoneBook.className, in: self.container.viewContext) else { return }
        
        // NSManagedObject 객체 생성
        let newPhoneBook = NSManagedObject(entity: entity, insertInto: self.container.viewContext)
        // 값 주입
        newPhoneBook.setValue(name, forKey: Key.name)
        newPhoneBook.setValue(phoneNumber, forKey: Key.phoneNumber)
        
        // container에 객체 저장
        do {
            try self.container.viewContext.save()
            print("저장 성공")
        } catch {
            print("저장 실패")
        }
    }
}

08) 데이터 읽기 (Read)

  1. PhoneBook extension에 정의한 fetchRequest 함수를 호출하여 container로부터 PhoneBook의 데이터를 불러온다.
    • fetchRequest의 반환 타입은 NSFetchRequest<PhoneBook>
    • viewContext(NSManagedObjectContext)fetch 메소드의 반환타입은 [Any]
  2. 위의 과정을 거쳐 불러 온 데이터는 [PhoneBook] 형태를 띄는데, 반복문을 통해 저장된 요소를 모두 조회하고 출력한다.
  3. 이 때, NSManagedObject로 타입 캐스팅을 해주는데 그 이유는 PhoneBook extension@NSManaged public var로 선언된 프로퍼티들에 접근하기 위해서다.
func readData() {
    do {
        let phoneBooks = try self.container.viewContext.fetch(PhoneBook.fetchRequest())
            
        for phoneBook in phoneBooks as [NSManagedObject] {
            if let name = phoneBook.value(forKey: Key.name) as? String,
               let phoneNumber = phoneBook.value(forKey: Key.phoneNumber) as? String {
                print("name : \(name), phone number : \(phoneNumber)")
            }
        }
    } catch {
        print("읽기 실패")
    }
}

09) 데이터 변경 (Update)

  1. 변경하고자 하는 데이터를 불러온다.
  2. 변경하고자 하는 Attribute의 기존 값으로 검색하여 해당 데이터를 받는다.
    • predicate라는 메소드를 통해 조건을 걸어 검색한다.
  3. do-catch문을 통해 container에 저장된 해당 데이터를 받는다.
  4. 결과값을 순회하며 update하고자 하는 값으로 setValue 한다.
  5. container에 지금까지의 context를 저장한다.
func updateData(currentName: String, updateName: String) {
	// 데이터 불러오기
    let fetchRequest = PhoneBook.fetchRequest()
    // predicate : 조건을 걸도록 함
    // name == %@ : name의 값이 파라미터의 값과 같은 것을 검색하라는 뜻
    fetchRequest.predicate = NSPredicate(format: "name == %@", currentName)
        
    do {
    	// container에서 해당 값에 속하는 데이터를 가져와 result에 할당
        let result = try self.container.viewContext.fetch(fetchRequest)
        
        // array 형태로 받은 result를 순회하며 새로운 값으로 set
        for data in result as [NSManagedObject] {
            data.setValue(updateName, forKey: Key.name)
        }
        // container에 저장
        try self.container.viewContext.save()
        print("데이터 수정 성공")
    } catch {
        print("데이터 수정 실패")
    }
}

10) 데이터 삭제 (Delete)

  1. 데이터를 fetch 하는 과정은 위와 같다.
  2. result를 순회하며 container에서 삭제한다.
  3. context를 저장한다.
func deleteData(name: String) {
	// name으로 검색하여 data fetch
    let fetchRequest = PhoneBook.fetchRequest()
    fetchRequest.predicate = NSPredicate(format: "name == %@", name)
    
    do {
        let result = try self.container.viewContext.fetch(fetchRequest)
        
        for data in result as [NSManagedObject] {
        	// 검색해서 가져 온 데이터 삭제
            self.container.viewContext.delete(data)
        }
        
        // 삭제된 내역을 저장
        try self.container.viewContext.save()
        
        print("데이터 삭제 성공")
    } catch {
        print("데이터 삭제 실패")
    }
}

구현 뒤 실행

profile
iOS Junior Developer

0개의 댓글