본문 바로가기

UIKIT

Firestore 데이터베이스에서 문서 업데이트, Combine 비동기작업

https://explorer89.tistory.com/303

 

Firebase - Combine 프레임워크 사용 비동기 작업 처리 (저장) map VS flatmap

https://explorer89.tistory.com/302 collectionUsers(retreive id: String)https://explorer89.tistory.com/301 CreateUser 함수?https://explorer89.tistory.com/300 AnyPublisher?https://explorer89.tistory.com/299 Combine, FirebaseStore 데이터 저장https:

explorer89.tistory.com

 

func collectionUsers(updateFields: [String: Any], for id: String) -> AnyPublisher<Bool, Error> {
    db.collection(userPath).document(id).updateData(updateFields)
        .map { _ in true }
        .eraseToAnyPublisher()
}

 

 

  • updateFields: 업데이트하려는 데이터의 키-값 쌍을 담고 있는 딕셔너리입니다.
  • id: Firestore에서 업데이트할 문서의 ID입니다.
  • db.collection(userPath).document(id).updateData(updateFields): Firestore의 updateData 메서드는 지정된 문서를 업데이트합니다. 이 메서드는 성공 시 Void를 반환하고, 실패 시 오류를 던집니다.
  • .map { _ in true }: 업데이트 성공 시 결과를 true로 변환합니다.
  • .eraseToAnyPublisher(): 반환 타입을 AnyPublisher<Bool, Error>로 제한합니다.

 

문제점

이 코드에는 몇 가지 문제가 있습니다:

  1. updateData는 Publisher가 아님:
    • updateDataCombine의 Publisher를 반환하지 않습니다. 대신, Firestore API는 비동기 작업을 클로저를 통해 처리합니다.
    • 따라서 updateData를 Combine과 함께 사용하려면 적절히 변환해야 합니다.
    • Combine의 Future를 사용하여 Firestore의 비동기 작업을 Combine 스트림으로 변환할 수 있습니다
  2. 에러 처리 부족:
    • Firestore의 updateData 메서드에서 발생한 에러를 Combine의 스트림으로 처리할 수 있도록 개선이 필요합니다. 
func collectionUsers(updateFields: [String: Any], for id: String) -> AnyPublisher<Bool, Error> {
    Future<Bool, Error> { promise in
        db.collection(userPath).document(id).updateData(updateFields) { error in
            if let error = error {
                promise(.failure(error)) // 에러 발생 시
            } else {
                promise(.success(true)) // 성공 시
            }
        }
    }
    .eraseToAnyPublisher()
}

 

수정된 코드의 작동 방식

  1. Future 사용:
    • Firestore의 updateData 메서드는 클로저 기반으로 동작하기 때문에, 이를 Combine 스트림으로 감싸기 위해 Future를 사용했습니다.
  2. promise:
    • Firestore 작업이 완료되면 promise를 호출하여 성공(true) 또는 실패(Error) 값을 반환합니다.
  3. eraseToAnyPublisher:
    • 반환 타입을 AnyPublisher<Bool, Error>로 제한합니다.

요약

수정된 코드는 Firestore의 비동기 작업을 Combine과 통합하여 안정적으로 성공/실패를 처리할 수 있도록 만듭니다. Combine과 Firestore를 함께 사용하려면 Future 또는 다른 Combine 기반의 비동기 변환 방법을 사용하는 것이 필수적입니다.

 

private func updateUserData() {
    guard let displayName,
          let username,
          let bio,
          let avatarPath,
          let id = Auth.auth().currentUser?.uid
    else  { return }

    let updateFields: [String: Any] = [
        "displayName": displayName,
        "username": username,
        "bio": bio,
        "avatarPath": avatarPath,
        "isUserOnboarded": true
    ]

    DatabaseManager.shared.collectionUsers(updateFields: updateFields, for: id)
        .sink { [weak self] completion in
            if case .failure(let error) = completion {
                print(error.localizedDescription)
                self?.error = error.localizedDescription
            }

        } receiveValue: { [weak self] onboardingState in
            self?.isOnboardingFinished = onboardingState
        }
        .store(in: &subscriptions)

}

 

전체적인 함수 동작 요약

  1. 사용자 정보(displayName, username, bio, avatarPath)와 현재 로그인한 사용자의 uid를 가져옵니다.
  2. Firestore 업데이트를 위해 필요한 데이터를 딕셔너리로 구성합니다.
  3. collectionUsers 함수를 호출하여 Firestore에 업데이트를 요청합니다.
  4. Combine의 sink를 사용해 작업 결과를 처리합니다:
    • 실패 시 에러를 처리하고, 로그와 UI 상태를 업데이트합니다.
    • 성공 시 온보딩 완료 상태를 업데이트합니다.

 

개선 포인트 적용

  1. Helper 메서드로 분리:
    • prepareUpdateFields: 업데이트할 필드 준비를 별도 함수로 분리해 가독성을 높였습니다.
    • handleCompletion 및 handleSuccess: 성공 및 실패 처리 로직을 별도의 함수로 나누어 코드 중복을 줄이고 유지보수를 용이하게 했습니다.
  2. 에러 메시지 세분화:
    • prepareUpdateFields 또는 인증 실패 시 적절한 오류 메시지를 출력합니다.
    • Firestore 작업이 실패한 경우와 성공했지만 비정상적인 값이 반환된 경우를 구분하여 처리합니다.
  3. 로깅 추가:
    • 각 주요 단계에서 print를 통해 진행 상황과 오류를 기록합니다.
  4. UI 상태 업데이트 명확화:
    • self.error와 self.isOnboardingFinished 값을 적절히 업데이트하여 UI와 동기화합니다.
  5. Guard 및 Optional 처리 강화:
    • Optional 바인딩을 적극 활용해 불필요한 nil 상태의 작업을 방지했습니다. 
private func updateUserData() {
    guard let updateFields = prepareUpdateFields(),
          let id = Auth.auth().currentUser?.uid
    else {
        print("Error: Missing required user data or user is not authenticated.")
        self.error = "User data is incomplete or user is not authenticated."
        return
    }
    
    DatabaseManager.shared.collectionUsers(updateFields: updateFields, for: id)
        .sink { [weak self] completion in
            self?.handleCompletion(completion)
        } receiveValue: { [weak self] isSuccess in
            self?.handleSuccess(isSuccess)
        }
        .store(in: &subscriptions)
}

// MARK: - Helper Methods

/// Prepares the fields to update for Firestore
private func prepareUpdateFields() -> [String: Any]? {
    guard let displayName,
          let username,
          let bio,
          let avatarPath
    else { return nil }
    
    return [
        "displayName": displayName,
        "username": username,
        "bio": bio,
        "avatarPath": avatarPath,
        "isUserOnboarded": true
    ]
}

/// Handles the completion of the Firestore update
private func handleCompletion(_ completion: Subscribers.Completion<Error>) {
    switch completion {
    case .failure(let error):
        print("Firestore update failed: \(error.localizedDescription)")
        self.error = error.localizedDescription
    case .finished:
        print("Firestore update finished successfully.")
    }
}

/// Handles successful Firestore update
private func handleSuccess(_ isSuccess: Bool) {
    guard isSuccess else {
        print("Unexpected error: Firestore update returned false.")
        self.error = "Firestore update returned unexpected failure."
        return
    }
    print("Firestore update was successful.")
    self.isOnboardingFinished = true
}