Test를 작성하면서 외부 모듈에 의존성을 최대한 끊어내는 방식으로 코드를 작성하고 있으니 Model 부분에 하나의 문제가 있었습니다. 바로 Model에 정의된 property들이 Firebase에 정의된 객체들을 사용하고 있다는 것이죠. 이렇게 되면 만약에 앱에서 Firebase를 사용하지 않게 되면 Model 자체를 다시 만들어야 합니다.
즉 만약에 단어를 저장하는 곳을 Firebase에서 Local Storage로 바꾸게 되면 TimeStamp 타입을 다른 타입으로 바꾸지 않으면 사용할 수 없게 되는 것이죠.
그리고 추가적으로 Firebase에서 제공하는 함수 (Firebase의 document를 객체로 파싱해주는 함수) data(as: )를 활용하기 위해서 @DocumentID라는 property wrapper를 사용하고 Codable, Hashable의 프로토콜을 준수하고 있습니다.
//🚫 model에서 Firebase를 import 하고 있다.
import FirebaseFirestoreSwift
import Firebase
enum StudyState: Int, Codable {
case undefined = 0, success, fail
}
protocol Word {
var id: String? { get }
var wordBookID: String? { get set }
var meaningText: String { get }
var meaningImageURL: String { get }
var ganaText: String { get }
var ganaImageURL: String { get }
var kanjiText: String { get }
var kanjiImageURL: String { get }
var studyState: StudyState { get set }
var timestamp: Timestamp { get } //🚫 Firebase의 객체를 사용하고 있음.
var hasImage: Bool { get }
}
struct WordImpl: Word, Codable, Hashable{
@DocumentID var id: String? //👉 Firebase에서 정의된 property wrapper를 사용하고 있음.
var wordBookID: String?
var meaningText: String = ""
var meaningImageURL: String = ""
var ganaText: String = ""
var ganaImageURL: String = ""
var kanjiText: String = ""
var kanjiImageURL: String = ""
var studyState: StudyState
let timestamp: Timestamp
var hasImage: Bool {
!self.meaningImageURL.isEmpty || !self.ganaImageURL.isEmpty || !self.kanjiImageURL.isEmpty
}
}
Firebase에 정의된 Timestamp를 사용하는 대신에 Swift 자체의 타입인 Date로 수정을 했습니다. 그리고 깔끔하게 data(as: )를 포기하고 document 안에 있는 dictionary를 받아서 객체를 init하는 방식으로 수정을 했습니다.
import Foundation
enum StudyState: Int {
case undefined = 0, success, fail
}
protocol Word {
var id: String { get }
var wordBookID: String { get }
var meaningText: String { get }
var meaningImageURL: String { get }
var ganaText: String { get }
var ganaImageURL: String { get }
var kanjiText: String { get }
var kanjiImageURL: String { get }
var studyState: StudyState { get set }
var timestamp: Date { get } //👉 Date로 수정
var hasImage: Bool { get }
}
struct WordImpl: Word {
let id: String
let wordBookID: String
let meaningText: String
let meaningImageURL: String
let ganaText: String
let ganaImageURL: String
let kanjiText: String
let kanjiImageURL: String
var studyState: StudyState
let timestamp: Date //👉 Date로 수정
var hasImage: Bool {
!self.meaningImageURL.isEmpty || !self.ganaImageURL.isEmpty || !self.kanjiImageURL.isEmpty
}
// TODO: Handle Parsing Error
init(id: String, wordBookID: String, dict: [String : Any]) {
self.id = id
self.wordBookID = wordBookID
self.meaningText = dict["meaningText"] as? String ?? ""
self.meaningImageURL = dict["meaningImageURL"] as? String ?? ""
self.ganaText = dict["ganaText"] as? String ?? ""
self.ganaImageURL = dict["ganaImageURL"] as? String ?? ""
self.kanjiText = dict["kanjiText"] as? String ?? ""
self.kanjiImageURL = dict["kanjiImageURL"] as? String ?? ""
let rawValue = dict["studyState"] as? Int ?? 0
self.studyState = StudyState(rawValue: rawValue) ?? .undefined
self.timestamp = dict["timestamp"] as? Date ?? Date()
}
}
Firebase에서 제공하는 함수를 통해서 쉽게 객체를 init합니다.
let wordBooks = documents.compactMap({ try? $0.data(as: WordBookImpl.self) })
살짝 복잡(?)해졌습니다. 더 이상 Timestamp 타입을 사용하지 않기 때문에 date 객체로 바꾸어서 사용합니다.
for document in documents {
let id = document.documentID
var dict = document.data()
let timestamp = dict["timestamp"] as! Timestamp
dict["createdAt"] = timestamp.dateValue()
wordBooks.append(WordBookImpl(id: id, dict: dict))
}
Firebase의 객체를 사용하지 않는 대신에 객체를 init하는 부분이 복잡해졌습니다. 두 장점을 모두 안고 가는 방법은 없었을까요?
이 포스팅을 작성하는 생각해보면 Protocol에 대한 이해가 좀 더 있었다면 더 좋은 방식으로 개선했을 것 같다는 생각이 듭니다.
중요한 것은 Protocol입니다. Model인 구조체 자체는 사실 Firebase에 의존해도 관계 없습니다. 만약에 백엔드를 다른 서비스를 사용한다고 하면 그 서비스를 위한 다른 구조체를 새로 만드는 것도 가능합니다. 따라서 data(as: )를 포기하지 않고 그대로 사용하는 대신에 아래처럼 computed property를 사용하는 방법은 어땠을까요?
var createdAt: Date {
timestamp.dateValue()
}
이렇게 하면 Protocol은 Firebase에 의존하지 않고 data(as: )메소드도 사용할 수 있었을텐데요.
나중에 이런 방식으로도 구현해보고 포스팅을 남겨보도록 하겠습니다!