오늘의 목표

잘못된 Firestore 구조를 수정하고, Image를 저장하는 방법을 찾기

1. Firestore 구조 변경

기존에 사용하려던 Firestore의 구조는 아래와 같다.

📂 users 
   └ 📄 {userId}
      ├── name: "홍길동"
      ├── email: "user@example.com"
      ├── coupleId: "abc123" 
      ├── createdAt: timestamp

📂 couples 
   └ 📄 {coupleId}
      ├── user1: "userId1"
      ├── user2: "userId2"
      ├── dDay: timestamp

📂 schedules 
   └ 📄 {scheduleId}
      ├── coupleId: "abc123"
      ├── title: "기념일"
      ├── date: timestamp
      ├── createdBy: "userId1"

📂 diaries 
   └ 📄 {diaryId} 
      ├── coupleId: "abc123"
      ├── author: "userId1"
      ├── content: "오늘 데이트 너무 좋았다!"
      ├── createdAt: timestamp

구조 자체는 문제가 없는데, 프로젝트를 진행하다보니 schedulesdiaries가 부족한 점이 보였다.
예를 들어 diaries는 새로운 일기를 생성할 때마다 diaryId가 생성되며 새로운 문서를 생성하게 되는데, 이 경우 다이어리를 삭제할 수가 없어진다. diaryId를 사용하면 되지 않냐고? 아쉽게도 불가능하다. diaryId를 사용해서 삭제를 구현하려면 구조상 다이어리를 삭제할 때 diaryId를 알고 있어야 하는데, 이를 어딘가에 직접 저장해두지 않으면 알 수 없고 이번 프로젝트에서는 CoreData를 사용하지 않기 때문에 불가능한 방법이다. schedules도 마찬가지인 상황.

schedule과 diary가 가진 다른 데이터로 삭제를 시도하려고 해도 전부 중복 가능성이 높은 데이터 밖에 없었기 때문에 새로운 데이터를 추가해야 했다. 때문에 각각의 문서에 id라는 데이터를 추가하였고, 삭제 로직도 id를 사용해서 구현하기로 하였다. id의 지정 방법은 문서ID를 그대로 사용하려고 한다. 문서ID는 자동 생성되는데다가 중복되는 일이 없기 때문에 안전해서 선택하였다.

또, diary에는 이미지를 삽입할 수 있도록 구현할 예정인데 이미지 데이터를 저장하고 있지 않았기 때문에 이것도 추가로 구현해 주어야 했다.

여기서 문제가 발생했는데, 원래는 Firebase의 storage를 이용해서 이미지를 저장하고 해당 경로만 Firestore에 저장해서 저용량 고효율로 이미지를 불러오도록 하려고 했다. 그러나 storage는 유료로 사용할 수 있었다...

다른 방법을 생각하다가 이미지를 Base64와 같은 코드로 인코딩하고, 불러올 때 다시 디코딩하는 방식을 생각해보았다. 허나 이 방법 역시 사용할 수 없었는데, 이유는 Firestore의 문서에 저장할 수 있는 데이터는 5MB 이하여야 하는데, 디코딩한 데이터가 생각보다 크기가 커서 저장하기가 어렵고, 용량을 줄이려고 이미지의 퀄리티를 줄이면 화질이 줄어들기 때문에 좋지 않기 때문이다.

그래서 일단은 로컬디렉토리에 이미지를 저장하고, 해당 경로를 Firestore에 저장해서 로컬디렉토리에서 이미지를 불러오는 방식을 사용하기로 했다.
이렇게 하면 앱을 삭제하거나 폰을 바꾸게 되면 이미지가 삭제되어 경로를 찾을 수 없게된다는 치명적인 단점이 있다... 때문에 추후 iCloud나 타사 온라인 storage를 이용하는 방법을 사용하는 방법으로 리팩토링이 필요할 것 같다.

수정된 구조

📂 schedules 
   └ 📄 {scheduleId}
   	  ├── id: "{scheduleId}"
      ├── coupleId: "abc123"
      ├── title: "기념일"
      ├── date: timestamp
      ├── createdBy: "userId1"

📂 diaries 
   └ 📄 {diaryId} 
      ├── id: "{diaryId}"
      ├── coupleId: "abc123"
      ├── author: "userId1"
      ├── content: "오늘 데이트 너무 좋았다!"
      ├── image: "Lovechive/lovechive/image.png" // 이미지 경로
      ├── createdAt: timestamp

2. Firestore 로직 수정

Firestore의 문서 구조를 수정했기 때문에 프로젝트 내에서 Firestore의 CRUD 메소드도 수정할 필요가 있었는데, 기존의 로직은 아래와 같다.

    /// Firestore에 저장/업데이트를 실행하는 메소드
    /// - Parameters:
    ///   - data: 저장/업데이트 할 데이터
    ///   - type: 저장할 데이터 타입
    func saveToFirestore(_ data: FirestoreModelProtocol, type: FirestoreDataTypes) {
        switch type {
        case .user:
            let userId = UserDefaults.standard.string(forKey: AppConfig.UserDefaultsConfig.userId) ?? ""
            let collection = db.collection(type.typeName).document(userId)
            
            collection.setData(data.transform()) { error in
                if let error {
                    debugPrint(error.localizedDescription, "❌ 데이터 저장 실패")
                } else {
                    debugPrint("✅ 데이터 저장 성공", data.transform().values)
                }
            }
        case .couple:
            let coupleId = UserDefaults.standard.string(forKey: AppConfig.UserDefaultsConfig.coupleId) ?? ""
            let collection = db.collection(type.typeName).document(coupleId)
            
            collection.setData(data.transform()) { error in
                if let error {
                    debugPrint(error.localizedDescription, "❌ 데이터 저장 실패")
                } else {
                    debugPrint("✅ 데이터 저장 성공", data.transform().values)
                }
            }
        case .diary:
            let diaryId = UserDefaults.standard.string(forKey: AppConfig.UserDefaultsConfig.diaryId) ?? ""
            let collection = db.collection(type.typeName).document(diaryId)
            
            collection.setData(data.transform()) { error in
                if let error {
                    debugPrint(error.localizedDescription, "❌ 데이터 저장 실패")
                } else {
                    debugPrint("✅ 데이터 저장 성공", data.transform().values)
                }
            }
        case .schedule:
            let scheduleId = UserDefaults.standard.string(forKey: AppConfig.UserDefaultsConfig.scheduleId) ?? ""
            let collection = db.collection(type.typeName).document(scheduleId)
            
            collection.setData(data.transform()) { error in
                if let error {
                    debugPrint(error.localizedDescription, "❌ 데이터 저장 실패")
                } else {
                    debugPrint("✅ 데이터 저장 성공", data.transform().values)
                }
            }
        }
    }

기존에는 FirestoreDataTypes에 따라 Switch문으로 나눠서 각각의 로직을 구현했는데, 이 방식은 성능적으로도 좋지 않고, RxSwift를 활용하고 있지 않기 때문에 적합하지 않다고 생각했다.
때문에 RxSwift를 활용하여 보다 가독성과 효율이 높은 코드로 수정해 보았다.

/// Firestore에 저장/업데이트를 실행하는 메소드
    /// - Parameters:
    ///   - data: 저장/업데이트 할 데이터
    ///   - type: 저장할 데이터 타입
    func saveToFirestore(_ data: FirestoreModelProtocol, type: FirestoreDataTypes) -> Single<Void> {
        return Single.create { single in
            let collectionRef = self.db.collection(type.typeName)
            var documentRef: DocumentReference
            
            switch type {
            case .user:
                let userId = self.udm.userId
                documentRef = collectionRef.document(userId)
                
            case .couple:
                let coupleId = self.udm.coupleId
                documentRef = collectionRef.document(coupleId)
                
            case .diary(id: let id):
                if !id.isEmpty {
                    documentRef = collectionRef.document(id)
                } else {
                    documentRef = collectionRef.document()
                }
                
            case .schedule(id: let id):
                if !id.isEmpty {
                    documentRef = collectionRef.document(id)
                } else {
                    documentRef = collectionRef.document()
                }
            }
            
            documentRef.setData(data.transform()) { error in
                if let error {
                    debugPrint("❌ \(type.typeName) 데이터 저장 실패: \(error.localizedDescription)")
                    single(.failure(error))
                } else {
                    debugPrint("✅ \(type.typeName) 데이터 저장 성공: \(data.transform().values)")
                    single(.success(()))
                }
            }
            
            return Disposables.create()
        }
    }

수정된 코드에서는 중복되는 코드를 줄이고 흐름을 더욱 잘 읽을 수 있도록 하여 가독성을 높였다. 또, RxSwift의 Single을 사용하여 비동기 작업을 더 효율적으로 처리할 수 있도록 수정하였다.
코드도 좀 더 짧아져서 보기가 좋아진 것 같다. 비슷한 방식으로 CRUD 메소드를 모두 수정해주면 오늘의 작업은 완료된다.

오늘의 목표 달성도

  • Firestore 구조 수정
  • FirestoreManager CRUD 메소드 수정
profile
이유있는 코드를 쓰자!!

0개의 댓글