Realm을 처음 알게되고 사용했을 때는 편리하다는 생각이 컸는데 쓰다보니 생각보다 고려해야 할 지점이 많다고 생각이 들었음
개발하면서 마주친 문제점과 해결한 방식들을 써보려고 함
일단 개발한 앱은
1) 검색을 하면 네이버쇼핑API로 상품정보를 불러와서 보여줌 (네트워크 통신)
2) 그 다음 좋아요를 누르면 Realm에 해당 상품이 저장됨 (DB 저장)
사실 상품 ID로 정보를 조회할 수 있는 API가 있다면 똑같이 네트워크 통신을 통해 해결하면 되는 일이지만, 없다는 게 문제임.
그렇다면 내부적으로 Realm으로 저장하는 방법을 사용할 수 밖에 없음.
어쨌든 그래서 문제는 뭐냐면
1. 네트워크 통신에서 받아온 데이터는 Realm Object가 아님
아래 화면은 네이버 쇼핑 API로 조회한 결과값임. API로 조회된 결과값은 아래처럼 Decodable을 채택한 구조체에서 관리함
struct Shop: Decodable {
let title: String
let link: String
let image: String
let lprice: String
let mallName: String
let productId: String
let titleDescription: String
let priceDescription: String
}
근데 Realm은 Object임 정확하게 말하면 Realm에서 정의한 RealmSwiftObject
class Like: Object {
@Persisted(primaryKey: true) var productId: Int
@Persisted(indexed: true) var title: String
@Persisted var link: String
@Persisted var image: String
@Persisted var lprice: String
@Persisted var mallName: String
convenience init(productId: Int, title: String, link: String, image: String, lprice: String, mallName: String) {
self.init()
self.productId = productId
self.title = title
self.link = link
self.image = image
self.lprice = lprice
self.mallName = mallName
}
}
아래처럼 RealmRepository에서 add, delete를 하고 있는데 매개변수 Object임
class RealmRepository: RealmProtocol {
private let realm = try! Realm()
func addLike(_ item: Like) {
do{
try realm.write {
realm.add(item)
}
}catch{
print("AddLike Failed")
}
}
func deleteLike(_ item: Like) {
do{
try realm.write {
realm.delete(item)
}
}catch{
print("deleLike Failed")
}
}
}
아래처럼 Object형태를 만들어서 addLike, deleLike를 하면 오류가 날 수밖에 없음
오류를 보면 Realm이 가지고 있는 오브젝트만 삭제 할 수 있다고 함.
네트워크를 통해 가져온 정보들은 struct기 때문에 아무리 Object형태를 만든다 해도 삭제는 안됨
그렇기 때문에 struct의 특정 ID로 테이블의 데이터를 찾고, 그 다음에 삭제를 해주는 방식으로 해야 함
let data = shopResult.items[indexPath.item]
let like = Like(
productId: Int(data.productId)!,
title: data.title,
link: data.link,
image: data.image,
lprice: data.lprice,
mallName: data.mallName
)
if isClicked {
repository.addLike(like)
}else{
repository.deleteLike(like)
}
아래처럼 PK로 특정 레코드를 찾고, 삭제하는 방식으로 동작하게 했음.
PK가 product_id이기 때문에, 구조체에서도 해당 값을 가지고 있고 이 값으로 조회를 하면됨.
func deleteLike(_ id: Int) {
if let item = realm.object(ofType: Like.self, forPrimaryKey: id){
do{
try realm.write {
realm.delete(item)
}
}catch{
print("deleLike Failed")
}
}
}
2. Results<Element: RealmCollectionValue>는 객체는 변동사항을 가지고 있음
앱에서 좋아요는 어디서든 발생할 수 있음. 이 좋아요가 발생할때마다 realm에서 가지고 있는 정보가 달라짐
일반적으로 collectionView나 tableView를 생각해보면, list = 새로운정보 & tableView.reloadData()라는 로직이 들어감
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
count = repository.fetchAllCount()
resultCollectionView.reloadData()
}
좋아요한 아이템만 보는 화면에서 위와 같이 코드를 짜면, reloadData()만으로도 업데이트 된 데이터를 가져옴
얘는 일반적인 우리가 알고 있는 배열의 느낌과 살짝 다름
한번 viewDidLoad에서 값을 넣어주면 상태가 변경되면 알아서 변경됨
그니까 새로 fetching을 하지 않고도 이 list를 데이터로 갖고있는 collectionView는 reload만 해주면 알아서 바뀐다는 것임
var list: Results<Like>!
override func viewDidLoad() {
list = repository.fetchAll() //try! Realm().objects(Like.self)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return list.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ResultCollectionViewCell.identifier, for: indexPath) as? ResultCollectionViewCell else {
return UICollectionViewCell()
}
let data = list[indexPath.item]
cell.configureData(Shop.init(managedObject: data))
cell.indexPath = indexPath
return cell
}
공식문서에서 이 사실을 확인할 수 있는데 대충 요약하면
1. 결과는 복사본이 아니고 live상태임. 그렇기 때문에 데이터를 변경하면 디스크에 바로 반영됨
2. 결과는 Lazy하게 동작함. 실제로 요청이 있을 때만 쿼리를 실행해서 성능이 뛰어남. pagination도 필요하지 않다고 함
3. Realm이 모든 객체 관계를 직접참조로 유지해서 쿼리로 관계 그래프를 직접 탐색할 수 있음
3. struct <-> Object 사이 값 변환
if isClicked {
let like = Like(
productId: Int(data.productId)!,
title: data.title,
link: data.link,
image: data.image,
lprice: data.lprice,
mallName: data.mallName
)
repository.addLike(like)
}else{
repository.deleteLike(Int(data.productId)!)
항상 Realm에 데이터를 넣어주려면 struct일 경우 Realm의 Object로 변경해서 사용을 해야함
반대로 Realm object를 struct로 바꿀때도 마찬가지임
과정이 복잡하진 않지만, 코드가 길어지기 때문에 어떻게 코드를 줄일 수 있을까 찾아보았음.
1. 프로토콜을 선언
struct -> object 변환 해주는 생성자와, object -> struct로 변환해주는 함수를 정의해놓음
protocol Persistable {
associatedtype ManagedObject: RealmSwift.Object
init(managedObject: ManagedObject)
func managedObject() -> ManagedObject
}
2. 사용하고 있는 구조체에 Persistable 프로토콜을 채택
extension Shop : Persistable {
typealias ManagedObject = Like
//Like(object) -> Shop(struct)
init(managedObject: Like) {
title = managedObject.title
link = managedObject.link
image = managedObject.image
lprice = managedObject.lprice
mallName = managedObject.mallName
productId = "\(managedObject.productId)"
}
//Shop(struct) -> Like(object)
func managedObject() -> Like {
let like = Like(
productId: Int(productId)!,
title: titleDescription,
link: link,
image: image,
lprice: lprice,
mallName: mallName
)
return like
}
}
3. 사용
if isClicked {
repository.addLike(data.managedObject())
}else{
repository.deleteLike(Int(data.productId)!)
}
참고 자료
공식문서 https://www.mongodb.com/docs/atlas/device-sdks/sdk/swift/crud/read/#read-characteristics
https://medium.com/@ludovicjamet/how-to-use-struct-with-realm-615fcbc8f0ee
'iOS > iOS 지식' 카테고리의 다른 글
[iOS] 네트워크 연결 감지 해보기 (0) | 2024.08.03 |
---|---|
[WWDC2020] Advances in UICollectionView (0) | 2024.07.19 |
[iOS] Data(ContentsOf)와 NSCache (0) | 2024.06.22 |
[WWDC2022] Design protocol interfaces in Swift (1) | 2024.06.08 |
[Swift] UserDefaults + PropertyWrapper (0) | 2024.05.26 |