Realm을 통해 DB를 관리하는 프로젝트에서 DiffableDataSource를 이용해 CollectionView를 구성하고 있었다.
원하는 상품을 위시리스트에 담고, 그 리스트를 보여주는 화면이었으며,
각 Cell을 탭하면 위시리스트에서 제거되는 로직을 구성하는 중이었다.
그런데, Realm에 있는 데이터를 불러오고 데이터를 추가하는데는 문제가 없었으나, Cell을 탭하는 동작, 즉 삭제하는 과정에서 다음과 같은 런타임 에러가 발생했다.
Object has been deleted or invalidated
이 오류는 Realm에서는 데이터가 삭제됐는데, view에서는 삭제되지 않은 상태로 남아있을 때 발생하는 오류였다.
그런데, 뷰를 업데이트하는 로직도 문제 없었고, Realm의 DB 자체에도 문제는 없어보였다.
이번 에서는 이 오류가 발생하는 이유와 해결방법 두 가지를 함께 알아보도록 하겠다.
에러 발생
먼저, snapshot을 업데이트 하는 부분의 코드를 살펴보겠다.
private func updateSnapshot() {
let data = repository.fetchAll()
wishlistList = Array(data)
var snapshot = NSDiffableDataSourceSnapshot<Section, Wishlist>()
snapshot.appendSections(Section.allCases) // 섹션
snapshot.appendItems(wishlistList, toSection: .main) // 셀
dataSource?.apply(snapshot)
}
realm에서 fetch해온 데이터를 배열 형태로 만들어서 각 cell에 표시하고 있다.
다음은 각 Cell을 탭했을 때 데이터를 삭제하는 부분이다.
extension WishlistViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let data = wishlistList[indexPath.item]
repository.deleteItem(data: data)
updateSnapshot()
}
}
실제 구현 구현 코드는 레포지토리패턴으로 따로 분리를 해놓은 상태이며,
Cell을 탭했을때는 해당 삭제 로직을 실행하고 스냅샷을 업데이트 하는 동작이 일어나고 있다.
그런데 `updateSanpshot()` 메서드 내부에서 위의 사진에서와 같은 에러가 발생하며 앱이 강제종료됐다.
문제의 원인
다음의 블로그를 비롯해 구글링을 통해 찾아본 결과, 이 문제는 나 혼자만 겪고있는 문제가 아니었다.
그리고 사실 Realm의 공식문서에도 이 문제에 대한 내용이 있었다...!!
https://velog.io/@maddie/iOS-Diffable-Realm-Delete-Error
쉽게 정리하자면 문제의 원인은 다음과 같다
- Diffable이 View를 업데이트 하는 방식은 Snapshot방식으로, 데이터가 변화하기 이전의 모습을 캡쳐하고, 이후의 모습을 캡쳐해서 바뀐 부분을 업데이트 한다.
- 삭제버튼을 누르면 객체를 relam에서 지우는 동작이 일어난다.
- 이 이후에 snapshot을 통해 이전의 데이터와 현재의 데이터를 비교하려고 하는데 이전의 데이터는 이미 삭제된 이후이다!
- 즉, 비교할 이전의 데이터가 없기 때문에 에러가 발생하는 것이다!
그렇다면 이 문제를 해결하기 위해 어떻게 삭제 기능을 구현해야할까?
해결방법은 크게 두 가지 정도가 있으며, 사실 어떻게 구현하느냐에 따라 더 많은 방법이 존재할 수 있을 것으로 보인다.
다음으로 두 가지 해결방법을 소개하겠다.
reloadData 방식 사용
`UICollectionViewDataSource`에서 사용하던 방식인 reloadData를 통해 해결할 수 있다.
Diffable에서도 위와같은 문제가 많이 발생하는걸 알고있기 때문에 별도의 메서드를 이용해서 snapshot방식이 아닌 reloadData방식을 이용할 수 있는데, 코드는 다음과 같다.
private func updateSnapshot() {
let data = repository.fetchAll()
wishlistList = Array(data)
var snapshot = NSDiffableDataSourceSnapshot<Section, Wishlist>()
snapshot.appendSections(Section.allCases) // 섹션
snapshot.appendItems(wishlistList, toSection: .main) // 셀
// 여기 메서드 수정
dataSource?.applySnapshotUsingReloadData(snapshot)
}
`dataSource?.apply(snapshot)`에서 apply 대신 applySnapshotUsingReloadData라는 메서드로 바꿔주면 해결된다!
단, 이 방법을 사용하면 Diffable의 장점인 애니메이션이나 Diff연산 등을 사용할 수 없다는 아쉬운점이 있다.
그럼에도 불구하고 단 한줄의 코드만 수정함으로써 문제를 해결할 수 있어서 급할때 쓰기 좋을것 같았다.
isDeleted로 삭제상태 관리하기
삭제를 한 이후에 데이터에 접근하는게 문제라면, 삭제하지 않고 접근한 이후 접근하면 되지 않을까?
단, 사용자가 눈으로 보기에는 삭제된것처럼 보이게 하는 방법이다.
먼저 Realm Object에 isDeleted라는 Bool타입 칼럼을 추가해준다.
// Wishlist
final class Wishlist: Object {
@Persisted(primaryKey: true) var id: ObjectId
@Persisted var name: String
@Persisted var date: Date
// 이 칼럼을 만들어줌
@Persisted var isDeleted: Bool = false
convenience init(name: String) {
self.init()
self.name = name
self.date = Date()
}
}
그리고, 삭제버튼을 눌렀을때, 실제로 데이터를 삭제하는게 아니라 이 변수를 true로 설정해주는 것이다.
// WishlistRepository
final class WishlistTableRepository: WishlistRepository {
private let realm = try! Realm()
// ...
// 삭제 처리를 하기 위한 메서드
func updateItem(data: Wishlist) {
do {
try realm.write {
realm.create(Wishlist.self, value: [
"id": data.id,
"isDeleted": true
], update: .modified)
}
} catch {
print("렘 데이터 수정 실패")
}
}
}
데이터를 fetch해올때는 isDeleted가 false인 애들만 필터링해서 가져오면
사용자가 보기에는 삭제된것처럼 보이게 된다.
func fetchInFolder(folder: WishFolder) -> Results<Wishlist> {
let data = realm.objects(Wishlist.self)
.sorted(byKeyPath: "date", ascending: false)
.where { $0.isDeleted == false }
.where { $0.folder == folder }
return data
}
마지막으로, 이렇게 삭제한것처럼 보이게 한 이후에, delete 메서드를 통해 실제 realm에 남아있는 데이터를 삭제하면 realm에 남아있는 데이터도 삭제되게 된다.
위의 코드들을 적용하면 다음과 같다.
// VC
private var wishlistList: [Wishlist] = []
private let repository: WishlistRepository = WishlistTableRepository()
override func viewDidLoad() {
super.viewDidLoad()
configureView()
configureDataSource()
updateSnapshot()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
repository.getFileURL()
updateSnapshot()
}
// ...
private func updateSnapshot() {
let data = repository.fetchAll()
wishlistList = Array(data)
var snapshot = NSDiffableDataSourceSnapshot<Section, Wishlist>()
snapshot.appendSections(Section.allCases) // 섹션
snapshot.appendItems(wishlistList, toSection: .main) // 셀
// 여기 코드는 그대로
dataSource?.apply(snapshot)
}
// 셀을 탭해서 삭제하는 코드
extension WishlistViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let data = wishlistList[indexPath.item]
// 실제 삭제가 아닌 isDeleted 칼럼을 true로 만들어줌
repository.updateItem(data: data)
updateSnapshot()
// snapshot 비교가 끝난 이후에 실제 DB에서 삭제
repository.deleteItem(data: data)
}
}
이렇게 해결하면, Diffable의 장점은 살리면서 에러도 해결할 수 있다는 장점이 있다.
이 외에도 다양한 방법이 있을 수 있을것같다.
어쨌든 문제의 핵심은 "객체가 삭제된 이후에 접근하지 말 것" 이걸 기준으로 다양한 방법을 시도해보는것도 좋을것 같다.
※ 참고자료
https://velog.io/@maddie/iOS-Diffable-Realm-Delete-Error
[iOS] Diffable + Realm Delete Error
🚨 에러 내용이틀 내내 이거 해결 못하고 있었다.우선 DiffableDataSource와 Realm을 함께 사용했고,Diffable은 데이터가 변하면, 이전 상태와 비교해서 뷰를 갱신하는 방식으로 동작한다.따라서 Realm 데
velog.io
'iOS > UIKit' 카테고리의 다른 글
[iOS/UIKit] 멀티섹션, 멀티모델로 RxDataSource + Compositional Layout 구성하기 (0) | 2025.03.09 |
---|---|
[iOS/UIKit] input-output구조에서 cell안의 버튼을 탭했을 때 viewModel로 이벤트 보내기 (0) | 2025.03.05 |
[iOS/UIKit] Diffable DataSource를 이용해 서로 다른 Cell로 구성하기 - Xcode16에서 발생할 수 있는 오류 (0) | 2025.02.28 |
[iOS/UIKit] debounce VS throttle (0) | 2025.02.26 |
[iOS/UIKit] RxSwift에서의 Cell의 중첩 구독 문제 (0) | 2025.02.23 |