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)
}
}
'UIKIT' 카테고리의 다른 글
Firebase Firestore를 사용하여 팔로워(follower)와 팔로잉(following) 관계를 처리하는 기능 (0) | 2025.01.10 |
---|---|
ViewController 내에 init으로 viewModel 할당하는 이유 (0) | 2025.01.10 |
FireStore 특정 사용자의 트윗 정보 가져오기 + Combine (0) | 2025.01.10 |
Firestore에 새로운 트윗 저장 + Combine 비동기 처리 (0) | 2025.01.09 |
Firestore 데이터베이스에서 문서 업데이트, Combine 비동기작업 (0) | 2025.01.09 |