UIKIT

Firebase Firestore를 사용하여 팔로워(follower)와 팔로잉(following) 관계를 처리하는 기능

밤새는 탐험가89 2025. 1. 10. 15:26

https://explorer89.tistory.com/308

 

ViewController 내에 init으로 viewModel 할당하는 이유

SearchViewController에 SearchViewViewModel을 init을 통해 주입하는 구조는 의존성 주입(Dependency Injection) 패턴의 대표적인 예입니다. 이 방법은 코드의 재사용성, 테스트 가능성, 유연성을 높이기 위해 많이

explorer89.tistory.com

 

func collectionFollowings(isFollower: String, following: String) -> AnyPublisher<Bool, Error> {
    db.collection(followingPath)
        .whereField("follower", isEqualTo: isFollower)
        .whereField("following", isEqualTo: following)
        .getDocuments()
        .map(\.count)
        .map {
            $0 != 0
        }
        .eraseToAnyPublisher()
}

역할

  • 특정 사용자가 다른 사용자를 팔로우하고 있는지 확인하는 함수입니다.

작동 방식

  1. Firestore 쿼리 생성:
    • db.collection(followingPath)에서 followingPath 컬렉션을 참조합니다.
    • whereField("follower", isEqualTo: isFollower)를 통해 follower 필드가 isFollower 값인 문서를 검색합니다.
    • whereField("following", isEqualTo: following)를 통해 following 필드가 following 값인 문서를 추가로 필터링합니다.
  2. 문서 개수 확인:
    • .getDocuments()로 Firestore에서 문서 데이터를 가져옵니다.
    • .map(\.count)를 사용하여 검색된 문서 개수를 가져옵니다.
  3. 팔로잉 여부 반환:
    • .map { $0 != 0 }를 사용하여 검색된 문서 개수가 0이 아니면 true, 0이면 false를 반환합니다.
  4. 결과 반환:
    • 결과를 AnyPublisher<Bool, Error>로 반환합니다. true는 팔로잉 중임을, false는 팔로잉 중이 아님을 의미합니다.

 

문서 개수가 0인지 확인하는 이유

  • 팔로잉 관계를 문서로 표현:
    • 팔로잉 여부는 Firestore의 followingPath 컬렉션에 저장된 문서를 기반으로 확인됩니다.
    • 예를 들어, 팔로우 관계는 아래와 같은 문서로 저장될 수 있습니다:
    • follower가 "userA"이고, following이 "userB"인 문서가 존재하면, 이는 "userA가 userB를 팔로잉하고 있다"는 의미입니다.
Collection: followingPath
Document ID: (자동 생성된 고유 ID)
Fields:
  - follower: "userA"
  - following: "userB"

 

 

  • Firestore에서 검색된 문서 개수:
    • .getDocuments()는 Firestore에서 조건(whereField)에 맞는 문서들을 검색하여 반환합니다.
    • 만약 문서 개수가:
      • 0: 해당 조건에 맞는 문서가 없으므로, 팔로잉 관계가 존재하지 않음.
      • 1 이상: 해당 조건에 맞는 문서가 하나 이상 있으므로, 팔로잉 관계가 존재함.
  • 조건에 맞는 문서가 없을 때:
    • 문서 개수가 0인 경우:
      • whereField("follower", isEqualTo: isFollower)와 whereField("following", isEqualTo: following) 조건을 만족하는 문서가 없음을 의미합니다.
      • 즉, 특정 follower가 해당 following을 팔로우하지 않았음을 뜻합니다.

 

func collectionFollowings(follower: String, following: String) -> AnyPublisher<Bool, Error> {
    db.collection(followingPath).document().setData([
        "follower": follower,
        "following": following
    ])
    .map { true }
    .eraseToAnyPublisher()
}

 

역할

  • 팔로잉 관계를 생성하는 함수입니다.

작동 방식

  1. Firestore에 데이터 추가:
    • db.collection(followingPath).document()로 새로운 문서를 생성합니다.
    • .setData([ ... ])를 통해 follower와 following 정보를 포함한 데이터를 Firestore에 저장합니다.
  2. 결과 반환:
    • .map { true }를 사용하여 저장 성공 시 true를 반환합니다.
    • 저장 과정에서 오류가 발생하면, Combine이 이를 자동으로 Error로 처리합니다.
  3. 결과 타입:
    • AnyPublisher<Bool, Error>로 결과를 반환합니다. true는 팔로잉 추가 성공을 의미합니다.

 

func collectionFollowings(delete follower: String, following: String) -> AnyPublisher<Bool, Error> {
    db.collection(followingPath)
        .whereField("follower", isEqualTo: follower)
        .whereField("following", isEqualTo: following)
        .getDocuments()
        .map(\.documents.first)
        .map { query in
            query?.reference.delete(completion: nil)
            return true
        }
        .eraseToAnyPublisher()
}

 

역할

  • 팔로잉 관계를 삭제하는 함수입니다.

작동 방식

  1. Firestore 쿼리 생성:
    • db.collection(followingPath)에서 followingPath 컬렉션을 참조합니다.
    • whereField("follower", isEqualTo: follower)와 whereField("following", isEqualTo: following)로 특정 팔로잉 관계를 필터링합니다.
  2. 문서 검색:
    • .getDocuments()로 Firestore에서 문서를 가져옵니다.
    • .map(\.documents.first)를 통해 검색된 첫 번째 문서를 가져옵니다.
  3. 문서 삭제:
    • .map { query in query?.reference.delete(completion: nil) }를 통해 해당 문서를 삭제합니다.
    • completion: nil은 삭제 작업 후 추가 작업이 필요 없음을 의미합니다.
  4. 결과 반환:
    • .map { true }를 사용하여 삭제 성공 시 true를 반환합니다.
    • 삭제 과정에서 오류가 발생하면 Combine이 이를 Error로 처리합니다.
private func checkIfFollowed() {
    guard let personalUserID = Auth.auth().currentUser?.uid,
          personalUserID != user.id
    else {
        currentFollowingState = .personal
        return }

    DatabaseManager.shared.collectionFollowings(isFollower: personalUserID, following: user.id)
        .sink { completion in
            if case .failure(let error) = completion {
                print(error.localizedDescription)
            }
        } receiveValue: { [weak self] isFollowed in
            self?.currentFollowingState = isFollowed ? .userIsFollowed : .userIsUnfollowed
        }
        .store(in: &subscription)
}

 

checkIfFollowed()는 특정 사용자가 현재 로그인한 사용자(본인)를 팔로우하고 있는지 확인하는 역할을 합니다. 이를 통해 앱에서 팔로우 상태를 확인하고 UI를 업데이트할 수 있습니다.

 

함수의 역할

  1. 로그인한 사용자와 선택된 사용자가 같은지 확인:
    • 본인이 본인의 계정을 보고 있다면, 팔로우 상태를 확인할 필요가 없으므로 별도의 작업을 수행하지 않습니다.
    • 이 경우, 상태를 .personal로 설정하고 즉시 종료합니다.
  2. 팔로우 여부를 확인:
    • Firebase의 followingPath에서 현재 로그인한 사용자와 선택된 사용자 간의 팔로우 관계를 쿼리합니다.
    • 쿼리 결과를 바탕으로 팔로우 상태를 .userIsFollowed 또는 .userIsUnfollowed로 업데이트합니다.
guard let personalUserID = Auth.auth().currentUser?.uid,
      personalUserID != user.id
else {
    currentFollowingState = .personal
    return
}

 

  • Auth.auth().currentUser?.uid를 통해 현재 로그인한 사용자의 Firebase Auth ID를 가져옵니다.
  • 로그인한 사용자와 검색된 사용자가 같으면 팔로우 상태를 확인할 필요가 없으므로 상태를 .personal로 설정하고 종료합니다.

 

DatabaseManager.shared.collectionFollowings(isFollower: personalUserID, following: user.id)
    .sink { completion in
        if case .failure(let error) = completion {
            print(error.localizedDescription)
        }
    } receiveValue: { [weak self] isFollowed in
        self?.currentFollowingState = isFollowed ? .userIsFollowed : .userIsUnfollowed
    }
    .store(in: &subscription)

 

 

  • DatabaseManager를 통해 Firebase Firestore에서 팔로우 관계를 확인하는 비동기 작업을 시작합니다.
  • 쿼리 조건:
    • follower가 현재 로그인한 사용자(personalUserID).
    • following이 선택된 사용자(user.id).

 

 var followButtonActionPublisher: PassthroughSubject<ProfileFollowingState, Never> = PassthroughSubject()

 

 var followButtonActionPublisher: PassthroughSubject<ProfileFollowingState, Never>는 Combine 프레임워크의 PassthroughSubject를 사용하여 팔로우 버튼의 액션 이벤트를 외부로 전달하는 데 사용됩니다. 이를 통해 다른 객체(예: ViewController, ViewModel 등)가 이 버튼의 액션 이벤트를 구독하고 처리할 수 있습니다.

 

PassthroughSubject란?

  • PassthroughSubject는 Combine에서 제공하는 주제(subject) 중 하나로, 이벤트 스트림을 발행(publish)하는 데 사용됩니다.
  • 외부에서 값을 발행하면 이 값을 구독(subscribe)하고 있는 객체들이 실시간으로 해당 값을 받을 수 있습니다.
  • 여기서는 팔로우 버튼의 상태를 전달하기 위한 용도로 사용됩니다.
PassthroughSubject<ProfileFollowingState, Never>

 

  • ProfileFollowingState:
    • 발행되는 이벤트 값의 타입입니다.
    • 여기서는 팔로우 상태를 나타내는 타입으로, 예를 들어 .userIsFollowed(팔로우 중), .userIsUnfollowed(팔로우 안 함), .personal(본인 계정) 등을 나타낼 수 있습니다.
    • 팔로우 버튼이 눌릴 때 상태를 전달합니다.
  • Never:
    • 에러 타입으로, 여기서는 에러가 절대 발생하지 않는다는 의미로 사용됩니다.
    • 팔로우 버튼 액션은 단순 이벤트로, 에러가 발생할 일이 없으므로 Never를 사용한 것입니다.

 

역할

followButtonActionPublisher는 팔로우 버튼이 눌릴 때 발생하는 이벤트를 다른 객체(ViewController나 ViewModel 등)로 전달합니다.

  • 팔로우 버튼이 눌렸을 때 상태 발행:
    • 팔로우 상태를 업데이트하거나 서버에 요청을 보내야 할 때, 이를 발행하여 구독자들이 이벤트를 처리합니다.
followButtonActionPublisher.send(.userIsFollowed)

 

  • 구독을 통해 이벤트 처리:
    • 다른 객체가 이 이벤트를 구독하여 상태 변화에 따른 작업(예: 서버 요청, UI 업데이트)을 처리합니다. 
viewModel.followButtonActionPublisher
    .sink { newState in
        // newState에 따라 서버 요청, UI 업데이트 등 수행
    }
    .store(in: &subscription)

 

 

왜 사용하는가?

  1. 관심사의 분리:
    • View와 ViewModel 간의 의존성을 줄이고 이벤트 전달을 명확하게 분리합니다.
    • ViewModel은 버튼이 눌렸다는 이벤트만 받고, 상태 처리와 로직은 ViewModel에서 수행합니다.
  2. 리액티브 프로그래밍:
    • Combine을 사용해 이벤트 흐름을 처리하므로, 코드가 더 선언적이고 반응적으로 동작합니다.
    • 버튼 이벤트에 따른 비동기 작업(예: 서버 요청)을 자연스럽게 연결할 수 있습니다.

 

headerView.followButtonActionPublisher.sink { state in
        switch state {
        case .userIsFollowed:
            self.viewModel.unFollow()
        case .userIsUnfollowed:
            self.viewModel.follow()
        case .personal:
             return
        }
    }
    .store(in: &subscription)

 

@objc private func didTapFollowButton() {
    followButtonActionPublisher.send(currentFollowState)
}