외부 라이브러리(Firebase)에 의존하는 Protocol 수정하기

SteadySlower·2022년 10월 26일
0
post-custom-banner

기존의 Protocol과 Model

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
    }
}

수정

Model

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()
    }
}

Database 부분

기존

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: )메소드도 사용할 수 있었을텐데요.

나중에 이런 방식으로도 구현해보고 포스팅을 남겨보도록 하겠습니다!

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.
post-custom-banner

0개의 댓글