CoreData와 UserDefaults

동그라미·5일 전
4
post-thumbnail

CRUD 개념


CRUD (Create, Read, Update Delete) 란?

-> 일반적인 개발론에서 CRUD 라는 용어는 자주 사용합니다. 뜻은 다음과 같습니다.

  • Create = 데이터 생성
  • Read = 데이터 읽기
  • Update = 데이터 업데이트 (쓰기)
  • Delete = 데이터 삭제

전화번호 앱에서 일어나는 CRUD 에 대해 생각해봅시다.

  • C = 새로운 전화번호를 등록한다.
  • R = 저장된 전화번호 데이터를 조회한다.
  • U = 저장된 전화번호를 수정한다.
  • D = 전화번호를 삭제한다.

데이터 CRUD 는 네이티브 (앱) 내부에서도 일어날 수 있고, 서버에서도 일어날 수 있습니다.

CoreData


CoreData 란?

  • CoreData 는 앱에서 기기의 디스크에 데이터를 읽고 쓸 수 있게 돕는 프레임워크.
  • Swift 로 기기 내 디스크에 데이터를 저장할 수 있는 대표적인 방법으로는 CoreDataUserDefaults 가 있다.

(1) CoreData 프로젝트 생성


  • 프로젝트를 생성할 때 Storage → CoreData 를 선택.

(2) Entity 생성


  • 아래 Add Entity 버튼을 클릭해서 Entity 만들기. Entity 는 저장될 데이터들의 집합.
  • 우리는 PhoneBook 이라는 Entity 를 만들어보겠습니다.
  • Entity 이름은 PhoneBook
  • Attribute 로는 name , phoneNumber 를 추가합니다.
  • 전화번호부 라는 데이터 집합 안에 이름전화번호 라는 속성이 부여된 것.
  • 오른쪽 인스펙터 영역 중 Codegen 개념
    • Code Generator 의 줄임말. Entity 를 어떤 형식의 코드로 생성할 것인지 선택하는 속성.
    • Manual/None = Entity 의 서브 클래스를 자동으로 생성하지 않고 개발자가 클래스 작성.
    • Class Definition = Entity 의 서브 클래스를 자동으로 생성.
    • Category/Extension = Entity 클래스와 함께 extension 을 위한 파일까지 생성.
    • 여기서는 Manual/None 으로 생성해보겠습니다.

(3) Code Generate


  • Editor → Create NSManagedObject Subclass 를 클릭해서 코드를 생성.

그럼 위와 같이 2개의 파일이 생성됨.

  • PhoneBook+CoreDataClass.swift 간단 설명.
import Foundation
import CoreData

@objc(PhoneBook)
public class PhoneBook: NSManagedObject {

}
  • NSManagedObject는 Core Data 프레임워크에서 관리되는 객체를 나타내는 기본 클래스. 이 클래스는 Core Data 엔티티와의 상호작용을 관리하며, 속성 값의 저장 및 검색을 처리.

  • PhoneBook+CoreDataProperties.swift 간단 설명.

import Foundation
import CoreData


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 {

}
  • @nonobjc = Objective-C 에서는 동작하지 않고 Swift 에서만 동작하는 메서드임을 명시.
  • fetchRequest() = PhoneBook 에 대한 여러가지 데이터 검색을 도움.
  • @NSManaged = CoreData 에 의해 관리되는 객체를 의미.
  • Identifiable = PhoneBook 타입이 고유하게 식별될 수 있음을 의미.

(4) NSPersistentContainer 생성


  • NSPersistentContainer 는 CoreData에서 데이터를 저장하고 관리하는 데 필요한 핵심 객체. → 직역해보면 영구적인 저장 장소.
  • 먼저 NSPersistentContainer 를 생성해줘야하는데, 프로젝트 생성할 때 CoreData 를 사용한다고 체크 해줬으므로, AppDelegate.swift 에 기본적으로 NSPersistentContainer 를 세팅하는 코드가 존재.

  • AppDelegate.swift 아래쪽에보면 saveContext() 라는 메서드도 자동으로 생성되어있는데, 직역하면 문맥을 저장한다는 뜻.
  • 데이터의 업데이트(추가, 업데이트, 삭제 등)가 일어났으면 saveContext() 를 호출해서 그 문맥을 저장해야 함.

(4) CoreData 를 활용한 CRUD.


  • NSPersistentConatiner 를 만들었으므로, ViewController 에서 이를 활용해서 데이터 접근.
  • CoreData 를 통해 CRUD 의 C, R 을 하는 과정.
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
        
        createData(name: "Adam", phoneNumber: "010-1111-2222")
        readAllData()
    }

    // AdamCoreData 에 데이터 Create.
    func createData(name: String, phoneNumber: String) {
        guard let entity = NSEntityDescription.entity(forEntityName: "PhoneBook", in: self.container.viewContext) else { return }
        let newPhoneBook = NSManagedObject(entity: entity, insertInto: self.container.viewContext)
        newPhoneBook.setValue(name, forKey: "name")
        newPhoneBook.setValue(phoneNumber, forKey: "phoneNumber")
        
        do {
            try self.container.viewContext.save()
            print("문맥 저장 성공")
        } catch {
            print("문맥 저장 실패")
        }
    }
    
    // AdamCoreData 에서 데이터 Read.
    func readAllData() {
        do {
            let phoneBooks = try self.container.viewContext.fetch(PhoneBook.fetchRequest())
            
            for phoneBook in phoneBooks as [NSManagedObject] {
                if let name = phoneBook.value(forKey: "name") as? String,
                   let phoneNumber = phoneBook.value(forKey: "phoneNumber") as? String {
                    print("name: \(name), phoneNumber: \(phoneNumber)")
                }
            }
            
        } catch {
            print("데이터 읽기 실패")
        }
    }
}

→ 디스크에 저장했으므로, 데이터를 저장한 코드를 삭제한 뒤 앱을 다시켜도 데이터가 남아있음을 확인 가능.

💻 여기서 잠깐 리팩토링을 해봅시다.
코드를 좀 더 나은 방향으로 개선하는 것을 리팩토링이라고 합니다.

  • PhoneBook 클래스를 이렇게 수정해봅시다.
@objc(PhoneBook)
public class PhoneBook: NSManagedObject {
    public static let className = "PhoneBook"
    public enum Key {
        static let name = "name"
        static let phoneNumber = "phoneNumber"
    }
}

static 프로퍼티는 그 타입에 대고 호출을 할 수 있는 프로퍼티입니다.

PhoneBook.classNamePhoneBook 이라는 클래스 타입에 대고 점을찍고 호출.

  • 그럼 이렇게 했을때 장점..? 🤔
    1. 자동완성 기능을 사용할 수 있게 된다.
    2. 직접 손으로 “phoneNumber” 라고 타자를 치지 않기 때문에 실수할 일이 적어진다 → 휴먼 에러를 줄인다.
    3. 값을 수정해야할일이 생겼을때 전체 다 일일이 안고쳐도되고 이곳 한군데만 고치면 된다.
    4. 성격이 같은 프로퍼티끼리 모아서 관리할 수 있다.

- 리팩토링 후 C, R 과정

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
        createData(name: "Adam", phoneNumber: "010-1111-2222")
        readAllData()
    }

    func createData(name: String, phoneNumber: String) {
        guard let entity = NSEntityDescription.entity(forEntityName: PhoneBook.className, in: self.container.viewContext) else { return }
        let newPhoneBook = NSManagedObject(entity: entity, insertInto: self.container.viewContext)
        newPhoneBook.setValue(name, forKey: PhoneBook.Key.name)
        newPhoneBook.setValue(phoneNumber, forKey: PhoneBook.Key.phoneNumber)
        
        do {
            try self.container.viewContext.save()
            print("문맥 저장 성공")
        } catch {
            print("문맥 저장 실패")
        }
    }

    func readAllData() {
        do {
            let phoneBooks = try self.container.viewContext.fetch(PhoneBook.fetchRequest())
            
            for phoneBook in phoneBooks as [NSManagedObject] {
                if let name = phoneBook.value(forKey: PhoneBook.Key.name) as? String,
                   let phoneNumber = phoneBook.value(forKey: PhoneBook.Key.phoneNumber) {
                    print("name: \(name), phoneNumber: \(phoneNumber)")
                }
            }
        } catch {
            print("데이터 읽기 실패")
        }
    }
}

- CoreData 를 통해 CRUD 의 U 를 하는 과정.

func updateData(currentName: String, updateName: String) {

    // 수정할 데이터를 찾기 위한 fetch request 생성
    let fetchRequest = PhoneBook.fetchRequest()
    fetchRequest.predicate = NSPredicate(format: "name == %@", currentName) // 예시: 이름이 "Adam"인 데이터 수정
    
    do {
        // fetch request 실행
        let result = try self.container.viewContext.fetch(fetchRequest)
        
        // 결과 처리
        for data in result as [NSManagedObject] {
            // 데이터 수정
            data.setValue(updateName, forKey: PhoneBook.Key.name) // 이름을 "Adam"에서 "Abel"로 수정
            
            // 변경 사항 저장
            try self.container.viewContext.save()
            print("데이터 수정 완료")
        }
        
    } catch {
        print("데이터 수정 실패")
    }
}

- CoreData 를 통해 CRUD 의 D 를 하는 과정.

func deleteData(name: String) {
    // 삭제할 데이터를 찾기 위한 fetch request 생성
    let fetchRequest = PhoneBook.fetchRequest()
    fetchRequest.predicate = NSPredicate(format: "name == %@", name)
    
    do {
        // fetch request 실행
        let result = try self.container.viewContext.fetch(fetchRequest)
        
        // 결과 처리
        for data in result as [NSManagedObject] {
            // 삭제
            // CRUD 의 D.
            self.container.viewContext.delete(data)
            print("삭제된 데이터: \(data)")
        }
        
        // 변경 사항 저장
        try self.container.viewContext.save()
        print("데이터 삭제 완료")
        
    } catch {
        print("데이터 삭제 실패: \(error)")
    }
}

UserDefaults


  • UserDefaults 또한 디스크에 데이터를 저장할 수 있게 돕는 도구.
  • CoreData 보다 사용성이 간단.
  • keyvalue 를 이용해서 값을 저장.
  • 대량의 데이터를 담는데에는 CoreData 가, 비교적 단순한 데이터를 담는 데에는 UserDefaults 가 적절.

UserDefaults 의 CRUD


  • UserDefaults.standard.set() 메서드를 통해서 Create, Update
  • UserDefaults.standard.string(forKey: "") 메서드를 통해서 Read (각 타입에 맞는 메서드사용)
    • bool 타입 Read: UserDefaults.standard.bool(forKey: "")
    • Int 타입 Read: UserDefaults.standard.integer(forKey: "")
  • UserDefaults.standard.removeObject(forKey: "") 메서드를 통해서 Delete
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Create
        UserDefaults.standard.set("010-1111-2222", forKey: "phoneNumber")
        
        // Read
        let phoneNumber = UserDefaults.standard.string(forKey: "phoneNumber")
        print("저장된 전화번호: \(phoneNumber)")
        
        // Update
        // 같은 키에다가 set 을 하면 됨.
        UserDefaults.standard.set("010-6666-7777", forKey: "phoneNumber")
        let newPhoneNumber = UserDefaults.standard.string(forKey: "phoneNumber")
        print("바뀐 전화번호: \(newPhoneNumber)")
        
        // Delete
        UserDefaults.standard.removeObject(forKey: "phoneNumber")
        print("전화번호가 남아있는가: \(UserDefaults.standard.string(forKey: "phoneNumber"))")
    }
}
  • Int, String 과 같은 원시타입이 아닌 Struct 나 Class 타입을 저장하기 위해서는 json 인코딩 과정이 필요.

UserDefaults 를 활용해서 간단한 포스트잇 앱 만들기


포스트잇 앱 만들기

  • UserDefaults 는 디스크에 데이터를 저장하기 때문에, 포스트잇에 적은 텍스트를 UserDefaults 에 저장 해둔다면, 앱을 종료해도 데이터가 소멸되지 않습니다.
  • 메모를 작성하고, 앱을 종료했다가 다시 실행해도 메모가 남는 포스트잇 앱을 만들어봅니다.
import UIKit
import SnapKit

class ViewController: UIViewController {

    private let label: UILabel = {
        let label = UILabel()
        label.text = "포스트잇"
        label.font = .boldSystemFont(ofSize: 30)
        label.textColor = .black
        return label
    }()
    
    private let textView: UITextView = {
        let textView = UITextView()
        textView.text = UserDefaults.standard.string(forKey: "memo")
        textView.layer.cornerRadius = 10
        textView.backgroundColor = UIColor(red: 75/255, green: 253/255, blue: 30/355, alpha: 1.0)
        textView.textColor = .black
        textView.font = .boldSystemFont(ofSize: 30)
        return textView
    }()
    
    private lazy var button: UIButton = {
        let button = UIButton()
        button.setTitle("적용", for: .normal)
        button.backgroundColor = .red
        button.setTitleColor(.white, for: .normal)
        button.titleLabel?.font = .boldSystemFont(ofSize: 20)
        button.layer.cornerRadius = 10
        button.addTarget(self, action: #selector(buttonTapped), for: .touchDown)
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
    }

    private func configureUI() {
        [
            label,
            textView,
            button
        ].forEach { view.addSubview($0) }
        
        view.backgroundColor = .white
        
        label.snp.makeConstraints {
            $0.top.equalToSuperview().offset(100)
            $0.centerX.equalToSuperview()
        }
        
        textView.snp.makeConstraints {
            $0.top.equalTo(label.snp.bottom).offset(100)
            $0.centerX.equalToSuperview()
            $0.height.width.equalTo(200)
        }
        
        button.snp.makeConstraints {
            $0.top.equalTo(textView.snp.bottom).offset(50)
            $0.width.equalTo(60)
            $0.height.equalTo(40)
            $0.centerX.equalToSuperview()
        }
    }
 
    @objc
    private func buttonTapped() {
        UserDefaults.standard.set(textView.text, forKey: "memo")
        print("저장 완료")
    }
}
profile
맨날 최선을 다하지는 마러라. 피곤해서 못산다.

3개의 댓글

comment-user-thumbnail
4일 전

동글바리님의 코어데이터 튜토리얼 보고 코데마(코어데리터 마스터) 댓어요

답글 달기
comment-user-thumbnail
4일 전

숙성된 틸은 남다르구나...

답글 달기
comment-user-thumbnail
4일 전

forkey,,쓰지 마세요,,
체질이란 게 바뀝니다
엔티티 객체 컨텍스트만 지정해서 생성하고
해당 객체 프로퍼티에 비로 접근해서 씁시다,,,

답글 달기