내용 정리
1분만 알람앱 프로젝트에서 알람을 수정해도 UI가 업데이트 되지 않는 버그가 발생했다.
문제의 원인을 분석하고 해결을 해보자.
1분만 앱의 알람 탭에서는 새로운 알람을 추가할 수도 있고, 기존의 알람을 수정할 수도 있다.
이 중 알람을 수정하는 기능을 테스트 해보는데, 이전까지는 잘 되던 UI 업데이트가 갑자기 이루어지지 않았다.

왜 이런 버그가 발생했는지 브레이크 포인트를 걸어가며 원인을 분석해본 결과
RxDataSource에서 UI를 업데이트 시켜주지 않는 것을 확인할 수 있었다.
그 이유는 RxDataSource의 로직과 원리에 있었는데...
RxDataSource는 섹션 모델로 정의한 구조체의 item을 비교하여 셀을 업데이트 시켜준다.
이 때 아이템의 비교는 고유 identifier를 기준으로, 변경 전 후를 비교하여 변경사항이 있을 경우 셀을 업데이트 한다. 참고
내가 정의한 item 모델은 아래와 같다.
struct AlarmItem: IdentifiableType, Hashable {
typealias Identity = UUID
var identity: Identity {
return data.id ?? UUID()
}
let data: Alarm
}
item이 가진 data라는 프로퍼티가 가진 id로 Identifier를 정의하는데, id는 옵셔널 타입이기 때문에 빈 값일 경우 새로운 UUID를 부여해주는 방식을 사용한다.
여기서 아이템의 아이디를 타고 들어가 아이템에 변화가 있는지를 확인하는데, 아이템의 프로퍼티가 Alarm으로 코어데이터의 entity 타입이다.
그런데 코어데이터 객체는 직접적으로 Equatable을 준수하지 않으므로 비교가 어렵고, 때문에 셀의 업데이트가 발생하지 않는 것이었다.
그럼 Equtable은 뭘까?
Equatable 프로토콜이 채택되어 있다.Equatable 프로토콜의 기본 형태
public protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}
즉, 우리가 Int 타입이나 String 타입 등을 비교할 때 사용하는 == 연산자 자체가 Equatable을 준수하는 타입이기에 가능했던 방식인 것이다.
그런데 코어데이터 객체는 Equatable 프로토콜을 채택하고 있지 않기 때문에 직접적인 비교가 어려워 데이터소스에서 변경을 감지하지 못하고 셀의 UI가 업데이트 되지 않는 것이다.
그렇다면 어떻게 이를 해결할 수 있을까?
여러 방법이 있겠지만, 이번에는 프로젝트의 구조를 크게 바꾸지 않고 성능 오버헤드를 일으키지 않으면서 문제를 해결하고 싶었다.
코어데이터에 데이터가 저장되는 순간은 알람을 수정하는 모달뷰의 '저장' 버튼을 눌렀을 때이다.
그럼 저장 버튼을 눌렀다는 것을 이벤트로 방출시키고, 메인 뷰 컨트롤러에서 이벤트를 받으면 해당 셀만 업데이트 시켜주면 어떨까??
private(set) var dataUpdate = PublishRelay<Void>() // 저장 버튼 클릭시 이벤트 방출
저장 버튼 바인딩 메소드
func bindSaveButton() {
saveButton.rx.tap
.asSignal(onErrorSignalWith: .empty())
.withUnretained(self)
.emit { owner, _ in
owner.dataUpdate.accept(())
}.disposed(by: disposeBag)
}
// 모달뷰 저장 버튼 이벤트 바인딩
modalVC.dataUpdate
.asSignal(onErrorSignalWith: .empty())
.withUnretained(self)
.emit { owner, _ in
guard let indexPath else { return }
owner.alarmView.collectionView.reloadItems(at: [indexPath])
}.disposed(by: self.disposeBag)
알람의 수정은 셀을 터치했을 때 진행되니, 셀을 터치했을 때 셀의 IndexPath를 모달뷰에 함께 전달하고, 전달된 IndexPath를 이용하여 해당하는 셀을 찾아 reloadItems(at:) 메소드를 사용하여 셀을 업데이트 해주는 방식이다.

이렇게 프로젝트의 구조는 바꾸지 않고 문제를 해결할 수 있었다.
코어데이터의 객체가 직접적으로 Equatable을 채택하고 있지 않지만, 코어데이터 자체적으로 객체의 변화를 감지하고 이를 처리할 수 있는 메커니즘을 제공한다고 한다.
코어데이터는 NSManagedObjectContext와 KVO(Key-Value Observing)를 사용해 객체의 변화를 감지한다고 한다. 그리고 이를 통해 객체의 속성 변경, 삽입, 삭제 등을 추적한다고 한다.
1. NSManagedObjectContext의 변경 사항 추적
Core Data는 관리형 객체 컨텍스트(NSManagedObjectContext)에 등록된 객체의 상태 변화를 추적한다.
그리고 상태 변화는 다음과 같은 방식으로 구분된다.
2. NSNotification 사용
Core Data는 다음과 같은 알림(Notification)을 통해 객체 변화 이벤트를 제공한다.
NotificationCenter.default.addObserver(
self,
selector: #selector(contextDidChange(_:)),
name: NSManagedObjectContextObjectsDidChange,
object: managedObjectContext
)
@objc func contextDidChange(_ notification: Notification) {
guard let userInfo = notification.userInfo else { return }
if let updatedObjects = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
for updated in updatedObjects {
print("Updated object: \(updated)")
}
}
}
NSManagedObject의 속성 변경은 KVO 이벤트로 노출된다.myManagedObject.addObserver(self, forKeyPath: "name", options: [.new, .old], context: nil)
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "name" {
print("Name changed: \(change?[.newKey])")
}
}
3. NSManagedObjectID 활용
NSManagedObjectID는 코어데이터 객체를 고유하게 식별하는 ID이다. 이를 사용하여 객체를 비교하거나 변화를 추적할 수 있다.
이 방식은 객체가 서로 다른 관리형 객체 컨텍스트에서 로드되더라도 동일성을 확인할 수 있는 방법이다.
if object1.objectID == object2.objectID {
print("Same Core Data object")
}
4. 코어데이터 변화 감지의 활용
코어데이터는 자체적으로 변화를 감지하고 UI 업데이트나 데이터 동기화에 반영할 수 있는 도구를 제공한다.
코어데이터에서 Equatable 또는 Hashable을 채택하지 않는 이유는 아래와 같다.
NSManagedObject는 Core Data의 관리형 객체로, 다양한 상태(삽입됨, 삭제됨, 수정됨, 영속됨 등)를 가질 수 있다.
메모리 상에서 동일한 객체라도 다른 관리형 객체 컨텍스트에서 로드될 경우, 별개의 인스턴스처럼 보일 수 있다. 따라서 메모리 주소를 비교하는 단순한 동등성 판단은 의미가 없다.
Core Data는 NSManagedObjectID를 사용하여 객체를 고유하게 식별하고 관리한다. 이 objectID는 객체의 영속성 저장소에서의 고유성을 보장한다.
RxDataSource도 코어데이터도 이제 좀 친해졌다고 생각했는데...
아직도 공부해야할 점이 많다.
다음에는 좀 더 구조를 잘 생각하고 코드를 작성해야겠다..!!