본문 바로가기

UIKIT

유저를 검색하기 위한 함수 + Combine

https://explorer89.tistory.com/306

 

FireStore 특정 사용자의 트윗 정보 가져오기 + Combine

https://explorer89.tistory.com/305 Firestore에 새로운 트윗 저장 + Combine 비동기 처리https://explorer89.tistory.com/304 Firestore 데이터베이스에서 문서 업데이트, Combine 비동기작업https://explorer89.tistory.com/303 Fireba

explorer89.tistory.com

func collectionUsers(search query: String) -> AnyPublisher<[TwitterUser], Error> {
    db.collection(userPath).whereField("username", isEqualTo: query)
        .getDocuments()
        .tryMap(\.documents)
        .tryMap { snapshot in
            try snapshot.map({
                try $0.data(as: TwitterUser.self)
            })
        }
        .eraseToAnyPublisher()
}

 

이 함수는 Firestore에서 username 필드가 주어진 query 값과 동일한 TwitterUser 객체 목록을 가져옵니다. 데이터를 비동기적으로 처리하며, 결과를 Combine의 AnyPublisher로 반환합니다.

 

 

db.collection(userPath).whereField("username", isEqualTo: query)

 

  • 설명:
    • Firestore에서 userPath에 해당하는 컬렉션을 참조하고, 쿼리를 통해 username 필드 값이 query와 같은 문서를 가져옵니다.
  • 결과: Firestore의 Query 객체를 반환합니다.

 

.getDocuments()

 

  • 설명:
    • getDocuments()를 호출하면 Firestore에서 해당 쿼리에 매칭되는 문서를 가져옵니다.
    • 이 작업은 비동기적으로 이루어지며, Combine의 Publisher로 결과를 반환합니다.
  • 결과: Publisher가 Firestore에서 가져온 QuerySnapshot을 방출합니다.

 

.tryMap(\.documents)

  • 설명:
    • Firestore에서 반환된 QuerySnapshot의 documents 속성을 가져옵니다.
    • documents는 검색 결과 문서들의 배열입니다.
  • 역할:
    • QuerySnapshot 자체가 아니라, 배열 형태의 문서([QueryDocumentSnapshot])만 추출합니다.
  • 결과: [QueryDocumentSnapshot] 배열을 방출하는 Publisher로 변환합니다.

 

.tryMap { snapshot in ... }

  • 설명:
    • Firestore의 문서 데이터 (QueryDocumentSnapshot)를 Swift 객체로 변환합니다.
    • 각 문서를 TwitterUser 객체로 디코딩하는 작업을 수행합니다.
  • 역할:
    • 배열의 각 문서를 순회하며, QueryDocumentSnapshot 객체를 TwitterUser 객체로 변환합니다.
    • 디코딩 작업 중 오류가 발생할 수 있으므로, tryMap을 사용해 오류를 처리합니다. 
try snapshot.map({
    try $0.data(as: TwitterUser.self)
})

 

  • snapshot.map은 Firestore 문서 배열을 순회하며, 각각의 문서를 Swift 객체로 변환합니다.
  • data(as:)는 Firestore의 내장 메서드로, Firestore 문서 데이터를 Codable 프로토콜을 준수하는 Swift 객체로 변환합니다.

 

 

class SearchViewViewModel {
    
    private var subscription: Set<AnyCancellable> = []
    
    func search(with query: String, _ completion: @escaping ([TwitterUser]) -> Void) {
        DatabaseManager.shared.collectionUsers(search: query)
            .sink { completion in
                if case .failure(let error) = completion {
                    print(error.localizedDescription)
                }
            } receiveValue: { users in
                completion(users)
            }
            .store(in: &subscription)

    }
}

 

SearchViewController에서 SearchResultsViewController로 데이터를 넘기기 위해 클로저를 사용한 이유는 상황에 따라 더 간단하고 직관적인 비동기 처리 방식을 선택했기 때문입니다.

 

1. 클로저를 사용하는 이유

우리가 클로저를 사용하는 이유는 주로 비동기 호출 후 결과를 반환하는 패턴에서 간결하게 처리하기 위해서입니다. 특히, SearchViewController에서 SearchResultsViewController로 결과를 전달하려는 목적이라면, 클로저가 더 적합할 수 있습니다. 그 이유는 다음과 같습니다.

a. 간단하고 직관적

  • 단일 함수 호출 후 결과 전달: search(with:) 함수를 호출하고, 그 후 클로저에서 결과를 받는 방식은 매우 직관적입니다. 이 경우, 단방향 흐름이므로 데이터를 쉽게 전달할 수 있습니다.
  • 클로저는 한 번만 호출되는 비동기 함수에서 값을 반환할 때 적합합니다. search(with:)에서 클로저를 사용하면 한 번의 검색 후 결과를 SearchResultsViewController로 전달할 수 있습니다.

b. SearchResultsViewController에 데이터를 전달하는 용도

  • SearchResultsViewController는 검색 결과를 표시하는 뷰 컨트롤러일 뿐, 검색 기능을 직접 구현하지는 않습니다.
  • SearchViewController에서 검색 쿼리를 통해 데이터를 가져오고, 그 결과를 클로저를 통해 SearchResultsViewController로 전달하는 방식은 단일 책임 원칙(SRP)을 따르기에도 적합합니다.
  • SearchResultsViewController는 단순히 데이터를 받고 표시하는 역할만 하기 때문에, SearchViewController에서 데이터를 가져오는 로직을 위임받는 구조가 더 깔끔합니다.

c. Combine보다 간단한 처리

  • Combine을 사용하지 않는 이유는 SearchViewController와 SearchResultsViewController 사이에서 단순히 데이터를 전달하는 작업만 필요하기 때문입니다.
  • AnyPublisher를 사용하면 구독자(subscriber)가 필요하고, 구독을 설정하고 취소하는 등의 관리가 필요합니다. 이 추가적인 복잡성은 현재의 경우에는 필요 없을 수 있습니다.
  • 클로저는 결과를 반환하는 즉시 처리할 수 있기 때문에 더 직관적이고 관리하기 쉽습니다.

d. 뷰 모델을 각각 생성하는 구조

  • SearchViewController와 SearchResultsViewController가 서로 독립적인 뷰 컨트롤러이기 때문에, 각 뷰 컨트롤러마다 별도의 뷰 모델을 사용하는 것이 자연스럽습니다.
  • 각 뷰 컨트롤러에서 자기 자신만의 뷰 모델을 가지고 그 뷰 모델을 통해 데이터를 가져오는 방식이 맞습니다.
  • 이 경우, SearchResultsViewController는 검색 결과를 표시하는 역할을 하므로 단순히 클로저로 결과를 받는 것이 더 효율적입니다.

 

 

결론

클로저를 사용한 이유는 단순하고 직관적인 데이터 전달 방식을 선택했기 때문입니다. SearchViewController에서 검색 결과를 처리한 후 단방향 데이터 흐름으로 SearchResultsViewController에 결과를 전달하는 방식이 간단하고 유지보수하기 쉽습니다.

반면, Combine을 사용하면 더 유연한 비동기 스트림 처리가 가능하지만, 추가적인 복잡성을 발생시킬 수 있습니다. 현재 이 구조에서는 클로저 방식이 더 간결하고 적합할 수 있습니다.

 

 

Combine을 사용해서 AnyPublisher로 반환할 수 있는 방법

import Foundation
import Combine

class SearchViewViewModel {
    
    private var subscription: Set<AnyCancellable> = []
    
    // Combine을 사용해서 AnyPublisher를 반환
    func search(with query: String) -> AnyPublisher<[TwitterUser], Error> {
        return DatabaseManager.shared.collectionUsers(search: query)
            .mapError { error in
                return error as? Error ?? NSError(domain: "UnknownError", code: -1, userInfo: nil)
            }
            .eraseToAnyPublisher()
    }
}

 

class SearchViewController: UIViewController {
    private var viewModel = SearchViewViewModel()
    private var subscription: Set<AnyCancellable> = []
    
    func searchUser(query: String) {
        viewModel.search(with: query)
            .sink { completion in
                if case .failure(let error) = completion {
                    print("Error: \(error.localizedDescription)")
                }
            } receiveValue: { users in
                // 데이터가 바뀌면 UI를 업데이트
                self.showSearchResults(users: users)
            }
            .store(in: &subscription)
    }
    
    func showSearchResults(users: [TwitterUser]) {
        // 데이터를 검색결과 화면에 전달
        let resultsVC = SearchResultsViewController(users: users)
        navigationController?.pushViewController(resultsVC, animated: true)
    }
}