🚨!!스압주의!!🚨

공미포 약 19000자의 글입니다.

-Today's Learning Content-

  • UIViewController 생명주기
  • CoreData
  • UserDefaults

1. Life Cycle

📌 개념 정리

UIViewController의 생명주기는 뷰 컨트롤러가 생성되어 화면에 표시되고 사라질 때까지의 일련의 과정이다. 이를 이해하고 사용하면 앱 상태 관리, 뷰 초기화, 데이터 처리 등을 효율적으로 구현할 수 있다.

1) viewDidLoad

viewDidLoad는 새로운 프로젝트를 만들거나 새 파일을 만들 때 cocoa touch파일을 만들 경우 자동으로 재정의 되는 메소드이기 때문에 자주 사용하고, 자주 보는 메소드이다.

viewDidLoad는 뷰 컨트롤러의 뷰가 메모리에 로드된 후 한 번만 호출된다. 때문에 주로 뷰의 초기 설정을 진행하거나 데이터를 로드 해야할 때, UI 요소를 초기화할 때 사용한다.

// viewDidLoad 사용 예시
override func viewDidLoad() {
	super.viewDidLoad()
    print("viewDidLoad: 뷰가 메모리에 로드됨")
}

2) viewWillAppear

viewWillAppear는 뷰가 화면에 표시되기 직전에 호출된다. 만약 뷰를 업데이트 해야 하거나 화면 표시가 되기 전에 필요한 작업이 있을 경우 viewWillAppear 메소드 내에서 선언하여 사용하면 된다.

// viewWillAppear 사용 예시
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    print("viewWillAppear: 뷰가 곧 표시됨")
}

3) viewDidAppear

viewDidAppear는 뷰가 화면에 표시된 직후에 호출된다. 주로 애니메이션이 시작되거나 네트워크 호출, 사용자 인터랙션을 활성화 할 때 viewDidAppear에서 선언하여 사용한다.

// viewDIdAppear 사용 예시
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    print("viewDidAppear: 뷰가 화면에 표시됨")
}

4) viewWillDisappear

viewWillDisappear는 뷰가 화면에서 사라지기 직전에 호출된다. 주요 용도로는 데이터의 저장, UI 상태 저장, 애니메이션 종료 처리 등으로 사용된다.

// viewWillDisappear 사용 예시
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    print("viewWillDisappear: 뷰가 곧 사라질 예정")
}

5) viewDidDisappear

viewDidDisappear은 뷰가 화면에서 사라진 직후 호출된다. 주요 용도로는 리소슷 해체, 백그라운드 작업 시작, 상태 복원을 준비할 때 사용된다.

// viewDidDisappear 사용 예시
override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    print("viewDidDisappear: 뷰가 화면에서 사라짐")
}

6) 추가 생명주기 메소드

viewWillLayoutSubviews

viewWillLayoutSubviews는 뷰의 서브뷰 레이아웃이 설정되기 직전에 호출되는 메소드이다. 커스텀 레이아웃 조정이나 제약 조건 등의 수정이 필요할 때 주로 사용된다.

// viewWillLayoutSubviews 사용 예시
override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    print("viewWillLayoutSubviews: 서브뷰 레이아웃 직전")
}

viewDidLayoutSubviews

viewDidLayoutSubviews는 뷰의 서브뷰 레이아웃이 모두 설정된 직후에 호출된다. 주로 최종 레이아웃의 상태를 확인하거나 복잡한 레이아웃을 변결할 때 사용된다.

// viewDidLayoutSubviews 사용 예시
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    print("viewDidLayoutSubviews: 서브뷰 레이아웃 완료")
}

7) 생명주기 활용 예시

viewDidLoad()에서 데이터 로드

override func viewDidLoad() {
    super.viewDidLoad()
    loadData()
}
func loadData() {
    // API 호출이나 로컬 데이터 로드
}

viewWillAppear()에서 UI 업데이트

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    refreshUI()
}
func refreshUI() {
    // 최신 데이터 반영
}

viewDidLayoutSubviews()에서 커스텀 레이아웃 조정

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    adjustLayout()
}
func adjustLayout() {
    // 특정 뷰의 위치나 크기 조정
}

8) 생명주기 메소드 호출 순서

// 뷰가 생성될 때
viewDidLoad()viewWillAppear()viewDidAppear()

// 뷰가 사라질 때
viewWillDisappear()viewDidDisappear()

// 서브뷰 레이아웃 조정 시
viewWillLayoutSubviews()viewDidLayoutSubviews()

CoreData

📌 개념 정리

CoreData란 Apple의 프레임워크로, iOS 및 macOS에서 데이터를 영구적으로 저장하고 관리하는데 사용된다. 주로 객체 관계형 데이터베이스 관리 시스템(ORM)으로, 데이터 모델을 정의하고, 객체를 영구 저장소에 저장하거나 검색하는 역할을 한다.

1) CoreData의 주요 구성 요소

CoreData의 주요 구성 요소는 아래와 같다.

  1. Managed Object Model (NSManagedObjectModel)

    • 역할: 데이터 구조(엔티티, 속성, 관계)를 정의
    • 설정: .xcdatamodeld 파일을 통해 시각적으로 구성하거나 코드로 생성할 수 있다.
  2. Managed Object Context (NSManagedObjectContext)

    • 역할: 애플리케이션과 영구 저장소 간의 인터페이스 역할
    • 작업: 객체를 삽입, 삭제, 수정하는 작업을 수행하고 변경 사항을 저장소에 저장한다.
  3. Persistent Store Coordinator (NSPersistentStoreCoordinator)

    • 역할: 영구 저장소와 상호작용하며, Managed Object Context가 영구 저장소에 접근할 수 있도록 돕는 역할
    • 저장소 타입: SQLite, In-Memory, Binary 등 다양한 저장소 타입을 지원한다.
  4. Managed Object (NSManagedObject)

    • 역할: Core Data의 데이터 모델과 연결된 객체로, 데이터베이스의 레코드와 대응

2) CoreData 생성하기

먼저 새로운 프로젝트를 생성하고 프로젝트 이름이나 팀 등 기본 설정을 완료한 후 마지막에 있는 StorageCoreData로 설정한다

그리고 프로젝트를 생성하면 아래 사진처럼 .xcdatamodel이라는 확장자를 가진 파일이 생성된 것을 확인할 수 있다.

이 파일이 데이터베이스가 될 CoreData이다.
이제 이 파일을 누르고 화면을 보면 아무것도 없을텐데, 여기서 우리가 직접 데이터베이스를 생성해줘야 한다.

먼저 화면 하단의 Add Entity 버튼을 눌러 새로운 엔티티를 생성해준다.

엔티티를 생성하면 화면의 좌측에 새로운 엔티티가 생성되며 아래 사진과 같이 변하는 모습을 볼 수 있다.

우리는 엔티티의 이름을 원하는 대로 수정할 수 있는데, 만들고 싶은 데이터베이스에 맞는 이름을 설정해주면 좋을 것 같다.

오른쪽 화면을 보면 Attributes, Relationships, Fetched Properties 라는 항목이 존재하고 각각 다른 옵션을 가지고 있는 것을 볼 수 있는데 이것들은 무엇일까??

3) Entity의 구성 요소

우리는 CoreData에서 데이터 모델을 설계할 때 Entity를 생성했다. 엔티티에는 저장할 데이터에 대한 정의를 해야하는데, Attributes, Relationships, Fetched Properties라는 3가지 옵션으로 데이터를 저장할 수 있다.

① Attributes

먼저 Attributes속성이라는 뜻으로, 엔티티 내의 데이터를 표현하는 개별 필드이다. 뜻 그래도 객체의 속성이라고 할 수 있으며, 데이터베이스의 열(column)에 해당한다.

사용 예시

  • 이름, 나이, 이메일 등과 같은 기본 데이터 필드를 정의할 때 사용한다.
  • 예: User 엔티티에 name, age, email 속성을 추가.

지원하는 데이터 타입

  • String: 텍스트 데이터 저장.
  • Integer 16/32/64: 정수 데이터.
  • Float/Double: 부동소수점 숫자.
  • Boolean: 참/거짓 값.
  • Date: 날짜 및 시간.
  • Binary Data: 파일 또는 이미지와 같은 이진 데이터.

속성 추가 예시

  • name: String
  • age: Integer 32
  • birthdate: Date

② Relationships

Relationships는 엔티티 간의 연결을 나타낸다. 이는 관계형 데이터베이스에서 테이블 간 관계를 정의하는 것과 같다. 두 엔티티가 어떻게 연결되는지를 설정하며, 1:N (One-to-Many), N:N (Many-to-Many) 등 다양한 관계 설정이 가능하다.

사용 예시

  • User 엔티티와 Order 엔티티가 있다고 가정했을 때, User가 여러 개의 Order를 가질 수 있는 관계를 정의할 때 사용한다.
  • 예:
    - User ↔️ Order: One-to-Many 관계 설정
    • Student ↔️ Course: Many-to-Many 관계 설정

관계 설정 속성

  • Destination: 관계가 연결되는 엔티티를 설정
  • Inverse Relationship: 반대 방향의 관계를 설정
  • Delete Rule: 관계된 객체가 삭제될 때의 행동을 설정 (예: Cascade, Nullify, Deny)

관계 설정 예시

  • User 엔티티에 orders라는 관계 추가:
    - Destination: Order
    • Type: To-Many

③ Fetched Properties

Fetched Properties동적으로 다른 엔티티에서 데이터를 가져오는 역할을 한다. 직접적인 관계가 설정되어 있지 않은 엔티티에서도 데이터를 쿼리하여 가져올 수 있다.

사용 예시

  • Department 엔티티에 속한 특정 Employee 객체들을 동적으로 가져올 때 사용
  • 필터링된 데이터 또는 특정 조건에 맞는 데이터를 참조해야 할 때 유용

설정 방법

  • Destination: 참조할 엔티티를 설정
  • Predicate: 데이터를 필터링할 조건
    - 예: Employee 엔티티에서 특정 조건을 만족하는 직원만 가져오기

Fetched Properties 예시

  • Department 엔티티의 activeEmployees:
    - Destination: Employee
    • Predicate: status == "active"

위의 내용을 정리하면 아래 표와 같다.

📊 Entity 옵션 비교

옵션설명사용 예시
Attributes엔티티의 개별 데이터 필드를 정의User 엔티티에 name, age, email 속성을 추가
Relationships엔티티 간의 관계 설정User ↔️ Order: One-to-Many 관계 설정
Fetched Properties동적으로 다른 엔티티에서 조건에 맞는 데이터를 가져옴Department 에서 특정 조건의Employees 목록 가져오기

6) Code Generation

엔티티의 데이터 설정까지 마무리하면 NSManagedObject subclass 파일로 만들어야 하는데, 그 전에 Code Generation에 대한 설정이 필요하다.

Code GenerationCore Data 모델 파일(.xcdatamodel)에서 정의한 엔티티를 Swift 또는 Objective-C 코드로 변환하는 방법을 결정하는 것이다.

① Manual/None (수동/없음)

Manual/None은 수동으로 코드를 생성한다는 뜻으로, 개발자가 직접 NSManagedObject subclass 파일을 만들어야 한다.
Xcode가 자동으로 파일을 생성하거나 업데이트하지 않기 때문에, Custom Logic을 작성하거나 Entity 클래스를 수동으로 관리하고 싶은 경우에 적합하다.

주의해야할 점은 데이터베이스에 변경 사항이 있을 때마다 개발자가 클래스 파일을 수동으로 업데이트 해야 한다는 점이다. 또, 모델에 새로운 속성이나 관계가 추가되면 해당 클래스를 직접 수정해야 한다.

② Category/Extension (카테고리/확장)

Category/ExtensionNSManagedObject의 기본 클래스를 Xcode가 생성하고, 그 클래스의 확장(extension)을 사용하여 커스텀 코드를 작성할 수 있도록 하는 방식이다.
기본 속성 및 관계에 대한 코드는 자동 생성되지만, 커스텀 코드는 Extension 파일에 작성하도록 유도된다.

주로 엔티티에 대한 기본 코드는 자동으로 관리하면서 커스텀 메소드나 추가 로직을 작성해야 할 때 사용한다. 이 방식은 코드와 데이터를 분리하여 관리하기 때문에 유지보수에 용이하다.

③ Class Definition (클래스 정의)

Class DefinitionNSManagedObject subclass의 전체 정의를 Xcode가 자동으로 생성하는 방식이다.
모든 AttributesRelationships가 포함된 클래스 파일이 생성되며, 필요에 따라 커스텀 코드를 추가할 수 있다.

이 방식은 빠르게 모델을 구현하고 싶거나 프로젝트 규모가 작아 기본적인 CRUD(Create-Read-Update-Delete) 연산만 필요한 경우, 혹은 자동 생성된 코드에 커스텀 로직을 추가하고 싶은 경우 사용하기 적합하다.

단, 자동으로 생성된 파일을 직접 수정할 수 있지만, 모델 변경 시 Xcode가 이 파일을 덮어쓸 수 있으므로 커스텀 코드를 안전하게 유지하려면 Category/Extension 방식을 사용하는 것이 더 좋다.

📊 Code Generation 옵션 비교

옵션특징사용 상황
Manual/None클래스 파일을 직접 작성 및 관리커스텀 로직이 많거나, 완전히 수동으로 제어하고 싶을 때
Category/Extension자동 생성된 클래스와 확장 파일로 커스텀 코드 분리기본 모델 관리는 자동으로 하고, 확장에서 커스텀 코드 작성시
Class Definition전체 클래스를 자동 생성, 커스텀 로직도 같은 파일에 작성빠르게 구현하고 싶거나, 작은 프로젝트에서 기본적인 CRUD 작업만 필요한 경우

5) Create NSManagedObject Subclass iOS Swift

엔티티를 어떻게 변환할지 Cord Generator까지 결정했다면 이제 데이터 베이스를 코드로 변환하는 작업을 진행한다.
화면 상단의 Editor를 선택하고, 아래쪽에 있는 Create NSManagedObject Subclass... 옵션을 선택한다.

이후 데이터 베이스가 있는 프로젝트를 선택한 후 코드로 변환할 엔티티를 선택해준다.

이후 파일을 생성할 폴더를 선택하고 create를 누르면 해당 폴더에 데이터베이스 파일이 생성된다.

CoreDataClassCoreDataProperties

6) CoreData의 CRUD

① 기초작업

우리는 프로젝트를 생성할 때 StorageCoreData로 선택했기 때문에 AppDelegate 파일을 확인해보면 새로운 코드가 추가되어 있는 모습을 볼 수 있다.

여기서 persistentContainer라는 변수를 정의하는 코드가 있는데, 이 변수는 CoreData의 핵심 개체로, NSPersistentContainer 클래스의 인스턴스를 의미한다. 이 개체는 CoreData 스택을 설정하고 관리하는 역할을 한다.

또, saveContext라는 항목이 있는데, 우리가 데이터 베이스에 값을 추가하거나 삭제하는 등 변동을 주면 반드시 데이터 베이스에 해당 사실을 알려야 하기 때문에 saveContext를 활용하여 데이터의 변동사항을 저장해야 한다.

persistentContainer의 주요 역할은 아래와 같다.

  1. Core Data 스택 관리:

    • Core Data의 주요 컴포넌트인 Managed Object Model, Persistent Store Coordinator, Managed Object Context를 초기화하고 관리한다.
  2. Persistent Store 로드:

    • SQLite 또는 Binary와 같은 저장소 파일을 로드하고, 해당 저장소와의 연결을 설정한다.
  3. Managed Object Context 제공:

    • 데이터를 관리하고 앱의 UI와 동기화하는 데 사용되는 NSManagedObjectContext를 생성 및 제공한다.

우리는 CoreData를 활용하기 위해 persistentContainer를 프로젝트 내부에 선언하여 사용해야 한다.

그러기 위해 먼저 ViewController에 새로운 프로퍼티를 만들어준다.

private var container: NSPersistentContainer!

여기서 container는 반드시 정의해줄 것이기 때문에 강제 옵셔널 언래핑으로 설정한다. NSPersistentContainer 타입은 AppDelegate에 있던 persistentContainer의 타입으로 클래스로 정의되어 있다.

이제 viewDidLoad() 메소드에서 위에서 만든 프로퍼티에 값을 정의해준다.

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

먼저 AppDelegate 타입으로 캐스팅한 변수를 선언해주고, 해당 변수의 persistentContainer를 위에서 만든 프로퍼티 container의 값으로 정의해준다.
이렇게 하면 이제 CoreData를 쓰기 위한 기초 준비가 완료된 것이다.

② Create

이번에는 메소드를 이용하여 CoreData에 데이터를 추가하는 방법을 알아볼 차례이다.
CoreData에 데이터를 추가하기 위해서는 우선 데이터 베이스와 연결된 Entity를 정의하여 사용해야 한다.

private func createData(name: String, phoneNumber: String) {
	guard let entity = NSEntityDescription.entity(forEntityName: "PhoneBook", in: self.container.viewContext) else { return }
}

위의 코드는 entity라는 새로운 변수를 guard문으로 옵셔널 체이닝하는 과정이다.
이 때, entity의 값은 CoreData와 연결된 엔티티로 정의하기 위해 엔티티 이름을 입력하고, 어떤 데이터 베이스의 엔티티인지 파라미터에 값을 입력한다.(위의 코드에서는 위에서 만든 container의 엔티티로 선언)
엔티티는 있을 수도 있고, 없을 수도 있기 때문에 옵셔널 타입이기 때문에 옵셔널 바인딩 하여 사용할 수 있다.

이제 메소드 내부에서 데이터 베이스에 값을 추가하는 코드를 입력하면 된다.

// 데이터 베이스 연결
let newPhoneBook = NSManagedObject.init(entity: entity, insertInto: self.container.viewContext)
newPhoneBook.setValue(name, forKey: "name")
newPhoneBook.setValue(phoneNumber, forKey: "phoneNumber")

위의 코드는 newPhoneBook이라는 변수를 선언하고, 이 변수를 사용해서 데이터 베이스에 값을 추가하기 위해 이와 관련된 동작을 수행할 수 있는 NSManagedObject 타입으로 정의한 뒤 setValue 메소드를 사용하여 데이터 베이스에 값을 추가하는 코드이다.
여기서 파라미터에는 위에서 만든 엔티티를 넣고, 어떤 데이터 베이스에 넣을 것인지 설정해준다.
그리고 메소드의 매개변수로 받는 namephoneNumber를 각각 데이터 베이스의 "name", "phoneNumber" 키에 저장한다.

이제 마지막으로 추가한 값을 데이터 베이스에 저장하면 된다.

do {
	// 변동사항을 데이터 베이스에 저장
	try self.container.viewContext.save()
	print("Data storage successful")
} catch {
	print("Failed to save data", error)
}

데이터를 저장할 때는 반드시 try를 사용해야 하는데, 그 이유는 데이터를 저장하는 save() 메소드가 에러를 던질 수 있는 throws의 형태이기 때문이다.

이제 이 메소드를 사용하면 데이터 베이스에 쉽게 데이터를 저장할 수 있다. 하지만 이 메소드는 값을 저장만 할 수 있기 때문에 잘 저장이 되었는지 확인할 수 없다.
그럼 이제부터 데이터를 읽는 메소드를 제작해보자.

③ Read

위에서 만든 메소드로 저장한 데이터를 확인하기 위해 Read 메소드를 만들어보자.
우선 메소드를 만들고 do-try-catch문을 사용해 데이터 베이스의 값을 가져오자.

private func readAllData() {
	do {
    	// 데이터 베이스의 모든 데이터를 가져옴
		let phoneBooks = try self.container.viewContext.fetch(PhoneBook.fetchRequest())
        
	} catch {
    	print("Failed to call")
    }
}

위 코드는 뷰 컨트롤러에 정의한 persistentContainerfetch 메소드를 사용하여 데이터 베이스에 저장된 값을 새로 정의한 변수에 저장하는 코드이다. 이 때, fetch는 에러를 던지는 throws 타입이기 때문에 try를 사용하여 호출해야 한다.

fetch 메소드의 파라미터인 PhoneBookCreate NSManagedObject Subclass... 으로 엔티티를 코드화 했을 때 생성된 클래스이다. 이 클래스는 NSManagedObject 클래스를 상속하고 있기 때문에 fetchRequest() 메소드를 사용할 수 있는데, 해당 메소드는 엔티티를 코드화 했을 때 같이 생성된 파일인 CoreDataProperties 파일에 정의되어 있다.

extension PhoneBook {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<PhoneBook> {
        return NSFetchRequest<PhoneBook>(entityName: "PhoneBook")
    }
    // 생략...
}

이 메소드는 데이터 베이스와 연결된 엔티티의 데이터를 NSFetchRequest<PhoneBook>의 형태로 가공하여 리턴하는 메소드이다. NSFetchRequest<ResultType>은 제네릭 타입을 가지는 클래스로, 여기서는 해당하는 엔티티의 데이터를 데이터 베이스의 클래스 타입(여기서는 PhoneBook 클래스)의 배열로 반환해주는 메소드라고 이해하면 된다.

이제 데이터를 불러왔으니 해당 데이터를 출력해보도록 하자.
위에서 데이터를 배열의 형태로 불러왔으니 다양한 방법으로 데이터를 출력할 수 있는데, 여기서는 for-in문으로 데이터를 출력해보도록 하겠다.

// 가져온 데이터를 NSManagedObject 타입으로 캐스팅
// 캐스팅된 데이터를 모두 출력
for phoneBook in phoneBooks as [NSManagedObject] {
	// Key값으로 데이터를 불러와 String 타입 캐스팅
	if let name = phoneBook.value(forKey: "name") as? String, 
    	let phoneNumber = phoneBook.value(forKey: "phoneNumber") as? String {
        
        // 출력
		print("name: \(name), phoneNumber: \(phoneNumber)")
	}
}

phoneBooks는 데이터 베이스의 클래스 타입인 PhoneBook 타입이고, 이 클래스는 NSManagedObject 클래스를 상속 중이기 때문에 타입 캐스팅이 가능하다.
타입 캐스팅을 하는 이유는 value(forKey:) 메소드를 사용하기 위함이다. 이렇게 하면 해당 키의 데이터를 가져올 수 있는데, 그냥 가져오면 Any? 타입을 가지기 때문에 타입 캐스팅으로 String 타입으로 변환한 뒤 출력한다.

이렇게 하고 위에서 만든 create 메소드와 같이 read 메소드를 사용하여 데이터의 저장이 잘 이루어졌는지 확인해보자.

createPhoneNumber(name: "crois", phoneNumber: "010-1111-2222")
        
readAllData()

사진처럼 콘솔 창에 데이터가 잘 출력되는 것을 보면 데이터가 잘 저장되었다는 것을 확인할 수 있다.

그런데 나는 코드를 분석하며 한가지 의문이 들었다.
for-in문으로 데이터를 출력하는데 꼭 타입 캐스팅을 사용해야 하는걸까??

데이터 베이스의 클래스를 보면 데이터 베이스의 Key 값이 아래와 같이 정의되어 있다.

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

클래스의 프로퍼티로 키를 갖고 있다면 굳이 타입 캐스팅을 안해도 되지 않을까?

그래서 실험을 해봤다.

for phoneBook in phoneBooks {
	if let name = phoneBook.name ,
    	let phoneNumber = phoneBook.phoneNumber {
        
		print(name, phoneNumber)
	}
}

문제 없이 출력 되었다.
이렇게 하면 타입캐스팅을 3번이나 진행해야 했던 지난 코드와 달리 if-let만 사용하여 간단히 원하는 데이터를 출력할 수 있다.
강의에서 타입캐스팅을 하라고 해서 그대로 했지만... 왜 타입캐스팅이 필요한지 잘 모르겠다.

추측을 해보자면, 이번의 경우 데이터 베이스의 클래스인 PhoneBook이 데이터 베이스의 Key 값을 프로퍼티로 가지고 있었기 때문에 위와 같이 축약 형태의 출력을 할 수 있었다.
하지만, 엔티티의 Code Generation 옵션에 따라 프로퍼티가 생길 수도, 안 생길 수도 있다고 생각한다면, 혹은 데이터 베이스를 수정한 탓에 새로운 Key 값이 생겼지만, Xcode가 그것을 자동으로 업데이트 하지 않을 수도 있기 때문에 value(forKey:) 메소드로 불러오는 편이 더욱 안전할 수 있다.
때문에 타입 캐스티을 해서 Key 값을 통해 데이터를 불러오는 방식을 사용하라고 한 것이 아닐까? 하고 생각해 보았다.

이것은 차차 공부해 보도록 하자
그럼 이제 데이터를 업데이트 하는 방법에 대해 알아보자

④ Update

데이터 베이스의 값은 항상 같은 값을 가진 채 유지 되지 않는다. 즉, 어떠한 이벤트에 의해 데이터 베이스의 값이 변경될 가능성이 있다는 뜻이다.
그렇다면 우리는 어떻게 데이터 베이스의 값을 업데이트 해줄 수 있을까??

지금부터 데이터 베이스를 업데이트 하는 메소드를 만들어보자.
우선 메소드를 정의하고 데이터 베이스의 모든 값을 가져올 수 있도록 fetchRequset() 메소드를 호출한다.

private func updateData(currentName: String, updateName: String) {
	let fetchRequest = PhoneBook.fetchRequest()
}

이렇게 하면 fetchRequest 변수는 PhoneBook의 데이터 베이스에 담긴 모든 데이터를 가져오기 위한 객체, NSFetchRequest<PhoneBook>타입이 된다.

이번 메소드에서는 name이라는 key를 변경할 것이기 때문에 필터링을 설정한다.

fetchRequest.predicate = NSPredicate(format: "name == %@", currentName)

여기서 NSpredicateCoreData에서 데이터를 필터링할 때 사용되는 조건식 객체이다. 주어진 조건을 기반으로 데이터 베이스에서 원하는 레코드를 가져오거나 수정할 때 유용하게 사용된다.

이 때, 조건 설정에 대해 format: "name == %@", currentName 으로 설정했는데, 이것은 name이라는 필드가 currentName과 일치하는 객체를 찾는다는 뜻이다.
여기서 %@와 같은 형식을 포맷 문자열이라고 하며 종류는 아래와 같다.

포맷 문자열의 종류

  • %@: 객체(문자열, 숫자 등)
  • %d or %i: 정수형 숫자
  • %f: 부동소수점 숫자
  • %K: 키 경로(동적으로 필드 이름을 삽입할 때 사용)

필터를 통해 원하는 데이터를 불러왔으니 이제 이 데이터를 업데이트(가공)해보도록 하자.

do {
	// 필터링 된 데이터를 모두 가져옴
	let result = try self.container.viewContext.fetch(fetchRequest)
    
    // 가져온 데이터를 모두 수정
	for data in result as [NSManagedObject] {
		data.setValue(updateName, forKey: "name")
	}
    
    // 변동사항을 데이터 베이스에 저장
	try self.container.viewContext.save()
	print("Data Update Successful")
            
} catch {
	print("Failed to update data", error)
}

먼저 Read 때처럼 fetch 메소드를 통해 필터링 된 데이터를 모두 result에 저장한다.
그리고 for-in문을 사용하여 데이터를 업데이트 할 것인데, 이 때 setValue 메소드를 사용해야 하기 때문에 NSManagedObject로 타입 캐스팅을 진행한 후 데이터를 업데이트 한다.

이제 아래 코드로 테스트를 해보자.

createPhoneNumber(name: "crois", phoneNumber: "010-1111-2222")
        
readAllData()
        
updateData(currentName: "crois", updateName: "sparta")

readAllData()

문제 없이 기존 데이터가 업데이트 된 모습을 볼 수 있다.
그럼 마지막으로 데이터 베이스의 값을 삭제하는 방법을 알아보도록 하자.

⑤ Delete

우리는 위에서 데이터 베이스에 값을 추가하고, 값을 불러오고, 값을 업데이트 하는 방법에 대해 학습했다.
그렇다면 데이터 베이스에서 값을 삭제하려면 어떻게 하면 될까??

지금부터 데이터 베이스의 값을 삭제하는 메소드를 함께 만들어보자.
우선 메소드를 만들고 Updata 때처럼 fetchRequest를 만들어준다.

private func deleteData(name: String) {
	let fetchRequest = PhoneBook.fetchRequest()
	fetchRequest.predicate = NSPredicate(format: "name == %@", name)
}

우리는 name 이라는 Key를 기준으로 값을 삭제할 것이기 때문에 필터링도 name을 사용한다.

이제 do-try-catch를 사용해서 데이터 삭제를 진행한다.

do {
	// 필터링된 데이터 모두 가져오기
	let result = try self.container.viewContext.fetch(fetchRequest)
    
    // 가져온 데이터를 NSManagedObject 타입으로 캐스팅
    // 데이터 베이스에서 해당하는 값 삭제
	for data in result as [NSManagedObject] {
		self.container.viewContext.delete(data)
	}
    
    // 변동사항을 데이터 베이스에 저장
	try self.container.viewContext.save()
	print("Data deletion successful")
            
} catch {
	print("Failed to delete data", error)
}

전체적인 과정은 Update를 할 때와 유사하다. 다만 for-in문 내부에서 실행하는 작업은 데이터 베이스의 delete 메소드를 사용한다는 점만 다르다.

여기서 delete 메소드는 아래와 같이 정의되어 있다.

파라미터로 NSManagedObject를 받는데... 그렇다면 이번에도 타입캐스팅 없이 삭제가 가능하지 않을까??

for data in result {
	self.container.viewContext.delete(data)
}

이렇게 코드를 작성하니 에러는 발생하지 않는다.
그럼 실제로 작동하는지 확인을 위해 아래처럼 코드를 작성하고 실행을 해보자.

createPhoneNumber(name: "crois", phoneNumber: "010-1111-2222")
        
readAllData()
        
deleteData(name: "crois")
        
readAllData()

콘솔창의 출력 상태를 보면 문제 없이 삭제가 된 모습을 볼 수 있다.
타입캐스팅을 하는 것은 이번에도 아마 안정성의 문제가 아닐까 싶다. 예제에서는 데이터 베이스도 한 개이고, 별다른 커스텀을 진행하지 않아 문제가 없지만, 프로젝트 규모가 커지고 데이터 베이스도 여럿을 사용하게 되거나 커스텀하여 사용하게 된다면 타입 캐스팅을 하여 사용하는 편이 안정성이 높을 것이다.

⑥ Code Refactoring

위에서 예시 코드를 보며 불편함을 느낀 사람이 있을까?
아마 있었을지도 모르겠다.

예를 들어 아래 코드를 보자

if let name = phoneBook.value(forKey: "name") as? String, 
	let phoneNumber = phoneBook.value(forKey: "phoneNumber") as? String {
    // 생략...
}

이 코드는 Read 메소드를 만들 때 작성한 코드이다.
여기서 주시할 부분은 value(forKey) 부분이다. Key에 대한 값을 String 타입으로 정의하고 있는데, 만약 키 값이 바뀐다면? 오타가 난다면?? 여러 상황을 상정해 보았을 때 String 타입으로 Key 값을 입력하는 것은 안정적이지 않다.

그럼 어떻게 할 수 있을까??

엔티티를 코드로 변환했을 때 생긴 파일 중 클래스 파일을 확인해보자.

import Foundation
import CoreData

@objc(PhoneBook)
public class PhoneBook: NSManagedObject {

}

아마 이런 형식으로 작성되어 있을 것이다.
이제 여기에 코드를 작성해주자.

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

먼저 데이터 베이스 클래스 타입의 이름을 정의하는 상수를 만들고 Key의 이름을 정의하는 상수를 열거형을 이용하여 정의한다.
이 값은 모두 static으로 선언되어 다른 파일에서 쉽게 불러와 사용할 수 있다.
그 말은 즉 이렇게 사용할 수 있다는 뜻이다.

if let name = phoneBook.value(forKey: PhoneBook.Key.name) as? String,
	let phoneNumber = phoneBook.value(forKey: PhoneBook.Key.phoneNumber) as? String {
    // 생략...
}

guard let entity = NSEntityDescription.entity(forEntityName: PhoneBook.className, in: self.container.viewContext) else { return }

위 코드에서는 Key 값이 필요할 때 PhoneBook.Key.name 등의 형태를 사용하여 값을 사용했다. 또, 클래스의 이름이 필요할 때는 PhoneBook.className 코드를 사용했다.
이처럼 클래스에서 static을 사용하여 값들을 정의하면 자동완성을 통해 휴먼 에러를 줄일 수 있고, 값의 변경을 일괄적으로 적용할 수 있어 유지보수 측면에서도 우수하다.

7) CoreData의 장단점

Core Data의 장점

  • 복잡한 데이터 모델 관리: 관계형 데이터 모델 및 객체 간 관계를 쉽게 설정 가능.
  • 메모리 관리: Lazy Loading을 통해 성능 최적화.
  • Undo/Redo 지원: 변경 이력을 관리하고 롤백 기능 제공.
  • NSFetchedResultsController: UITableView와 UICollectionView에서 데이터 변경 추적을 도와준다.

Core Data의 단점

  • 학습 곡선: 초기 설정이 복잡할 수 있다.
  • Thread Safety: Managed Object Context는 스레드 간 공유가 어렵기 때문에 주의가 필요하다.

-Today's Lesson Review-

오늘은 UIKit에서 UIController의 생명주기와
CoreData, UserDefaults에 대해 학습했다.
원래는 UserDefaults에 대해서도 정리하려고 했으나,
코어 데이터의 분량이 너무 많기도 하고
유저디폴트는 비교적 쉽기 때문에 굳이 정리할 필요성을 못 느껴
코어 데이터 까지만 정리하기로 했다.

코어 데이터는 이번에 처음 써보는데, 익숙해지려면 시간이 많이 필요할 것 같다.
그래도 잘 활용하면 프로젝트를 만들 때 유용하게 활용할 수 있을 것 같다는 생각이 든다.
profile
이유있는 코드를 쓰자!!

2개의 댓글

comment-user-thumbnail
2024년 12월 2일

헉 강의내용에서 viewWillLayoutSubviews 는 못 들은 것 같은데 어떻게 알게 되셨나요? 내가 졸았던것인가ㅠㅜ

1개의 답글