지난 글
지난번엔 ReactiveX 홈페이지를 아주 간단히 살펴봤다. 사실 살펴보면서 직접 사용해 봐야겠다는 생각이 들었다.
[Programming] 반응형 프로그래밍 - 2. ReactiveX
📝 지난 글 [CS] 반응형 프로그래밍 - 1. 기본 개념🧑🏻💻 내가 반응형 프로그래밍을 공부하게 된 이유갑자기 채용공고들을 올려서 이게 뭐지 싶겠지만, 여기서 우대 사항, 참고 사항에 들어
swift-library.tistory.com
ReactiveX는 RxSwift를 통해 Swift를 지원한다! 나는 이번에는 SPM을 통해 RxSwfit를 사용해 볼 예정이다.
GitHub - ReactiveX/RxSwift: Reactive Programming in Swift
Reactive Programming in Swift. Contribute to ReactiveX/RxSwift development by creating an account on GitHub.
github.com
📱 실습 앱
아래와 같이 카카오 API를 활용한 책 검색 앱을 간단하게 만들었다.
😊 실습 앱 코드
아래는 코드인데 사실 중요하지 않지만 설명은 넣는 게 좋을 것 같아서 넣었다. 코드를 살펴보면 아래와 같이 검색 버튼이 눌리게 된다면 결과 TableView에 reloadData를 직접적으로 명령하는 구조이다.
final class SearchTableViewController: UITableViewController {
private let searchResultsController = SearchResultsTableViewController()
private lazy var searchController = UISearchController(searchResultsController: searchResultsController)
override func viewDidLoad() {
super.viewDidLoad()
// ... 생략
searchController.searchBar.delegate = self
}
}
// MARK: - UISearchBarDelegate
extension SearchTableViewController: UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
Task {
let query = KakaoBookSearchQuery(query: searchBar.text)
let data = try await query.request()
let result = try KakaoBookSearchResult(from: data)
searchResultsController.searchResults = result.documents?.compactMap { Book(from: $0) } ?? []
searchResultsController.tableView.reloadData()
}
}
}
그렇게 되면 아래처럼 결과 테이블뷰에서 다시 tableView를 그리는 구조이다.
final class SearchResultsTableViewController: UITableViewController {
var searchResults: [Book] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "BookCell")
}
}
// MARK: - UITableViewDataSource
extension SearchResultsTableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell", for: indexPath)
let book = searchResults[indexPath.row]
// ... 셀 UI 설정
/// 비동기 이미지 로드
Task {
guard let url = URL(string: book.thumbnail) else { return }
let data = try await URLSession.shared.data(from: url).0
await MainActor.run {
content.image = UIImage(data: data)
cell.contentConfiguration = content
}
}
cell.contentConfiguration = content
cell.accessoryView = button
return cell
}
}
간단하게 그림으로 살펴보면 아래와 같다. 이런 구조에서 제일 아쉬운 점은 검색 결과 화면을 갱신하라는 요청을 데이터를 주는 것과 함께 알려준다는 것이다. 결과 화면에서는 아직 화면을 갱신할 준비가 되어있지 않을 수도 있다.
나는 이 부분에 집중해서 RxSwift를 적용해 보았다.
🦎 RxSwift 적용기
결론적으로 보자면 밋밋한 검색이 아닌 조금 더 실제 검색창 같은 느낌이 나게 만들 수 있었다. 실제 검색을 누르지 않더라도 사용자가 입력한 텍스트를 추적하면서 중간 결과도 보여주고, 이를 활용하면 실시간 검색어 추천, 자동완성 등 다양한 부분에서 응용이 가능할 것이라고 생각되었다.
물론 꼭 RxSwift를 사용하지 않더라도 Delegate를 통해 구현이 가능하지만 RxSwift를 활용하니 정말 쉽게 사용자의 입력 이벤트를 추적하고, 데이터를 가져오고, 화면에 업데이트까지 할 수 있었다! 🧑🏻💻
아래와 같이 사용자의 입력 이벤트를 쉽게 관리할 수 있었다. 예를 들면 distinctUntilChanged()를 통해 이전값과 현재값이 같다면 중단되게 할 수도 있었다.
final class SearchTableViewController: UITableViewController {
private let searchResultsController = SearchResultsTableViewController()
private lazy var searchController = UISearchController(searchResultsController: searchResultsController)
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// ... 생략
searchController.searchBar.rx.searchButtonClicked
.withLatestFrom(searchController.searchBar.rx.text.orEmpty)
.distinctUntilChanged()
.flatMapLatest(fetchBooks)
.observe(on: MainScheduler.instance)
.bind(to: searchResultsController.searchResultsRelay)
.disposed(by: disposeBag)
}
func fetchBooks(query: String) -> Observable<[Book]> {
return Observable.create { observer in
Task {
let kakaoBookSearchQuery = KakaoBookSearchQuery(query: query)
let data = try await kakaoBookSearchQuery.request()
let result = try KakaoBookSearchResult(from: data)
let books = result.documents?.compactMap { Book(from: $0) }
observer.onNext(books ?? [])
observer.onCompleted()
}
return Disposables.create()
}
}
}
아래와 같은 코드에서 정말 강력하게 작용하는 것을 볼 수 있었는데, debounce나 throttle을 활용하여 이벤트를 관리할 수 있었다. 그리고 아래의 코드는 검색할 때 요즘은 검색 버튼을 눌러야 검색이 되는 것이 아니고 미리 조금씩 검색이 되고 있는(?) 걸 경험할 수 있는데, 만약에 이렇게 이벤트관리를 하지 않고, 명령형으로 작성한다면 타이머를 사용하거나 값을 검증하는 부분을 따로 추가해줘야 하기 때문에 정말 편리하다고 느껴졌다! 😊
let searchBarText = searchController.searchBar.rx.text.orEmpty
.debounce(.seconds(1), scheduler: MainScheduler.instance)
let searchButtonClick = searchController.searchBar.rx.searchButtonClicked
.withLatestFrom(searchController.searchBar.rx.text.orEmpty)
Observable.merge(searchBarText, searchButtonClick)
.distinctUntilChanged()
.flatMapLatest(fetchBooks)
.observe(on: MainScheduler.instance)
.bind(to: searchResultsController.searchResultsRelay)
.disposed(by: disposeBag)
결과 화면은 BehaviorRelay를 통해 값이 변경된다면 reloadData를 주체적으로 할 수 있게 되었다!
final class SearchResultsTableViewController: UITableViewController {
private let disposeBag = DisposeBag()
let searchResultsRelay = BehaviorRelay<[Book]>(value: [])
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "BookCell")
/// 값이 변경되면 reloadData()
searchResultsRelay
.bind { [weak self] _ in self?.tableView.reloadData() }
.disposed(by: disposeBag)
}
}
😳 RxSwift 적용 후기
RxSwift를 적용해 보고 느낀 점은... 와 정말 잘 사용하면 편리한 것들이 너무 많겠다...라는 생각이었다. 단순히 검색 부분에서 적용했지만 다양한 이벤트를 이렇게 아래에 붙여가며 편하게 관리할 수 있다는 점이 너무 큰 장점 같았다. 검색을 구현할 때 어떻게 해야 사용자가 입력한 것을 미리 보여주거나 추천 검색어를 띄워줄 수 있을까? 했는데 이런 방식으로 관리하면 되겠구나 라는 생각도 가지게 되었다. 많은 부분에서 RxSwift를 활용해보고 싶다는 생각을 했다...!
그리고 진짜 느낀 점은 내가 이렇게 몇 번 적용해 본다고 되는 것이 아니고, 아직 한참 멀었구나...라는 생각이었다. 앞으로 더 사용해 보면서 경험치를 쌓아야겠다는 생각이 들었다
🗳️ Combine 적용기
Combine도 역시 똑같이 구현할 수 있었다. 차이점으로 느껴진 것은 Publisher, Subject를 사용한다 정도...? debounce, throttle 등 사용도 가능했다. 또한 RxSwift에서는 자체적으로 SearchBar 델리게이트를 내부에서 지원해 주는 것 같아서 조금 더 만들어진 메서드가 있었다 정도...?
final class SearchTableViewController: UITableViewController {
private let searchResultsController = SearchResultsTableViewController()
private lazy var searchController = UISearchController(searchResultsController: searchResultsController)
private let searchTextSubject = PassthroughSubject<String, Never>()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.title = "검색"
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
searchController.searchBar.delegate = self
searchTextSubject
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.removeDuplicates()
.flatMap { self.fetchBooks(query: $0) }
.receive(on: DispatchQueue.main)
.sink { self.searchResultsController.searchResultsSubject.send($0) }
.store(in: &cancellables)
}
private func fetchBooks(query: String) -> AnyPublisher<[Book], Never> {
return Future { promise in
Task {
let kakaoQuery = KakaoBookSearchQuery(query: query)
let data = try await kakaoQuery.request()
let result = try KakaoBookSearchResult(from: data)
let books = result.documents?.compactMap { Book(from: $0) }
promise(.success(books ?? []))
}
}
.eraseToAnyPublisher()
}
}
// MARK: - UISearchBarDelegate
extension SearchTableViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
searchTextSubject.send(searchText)
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let searchText = searchBar.text else { return }
searchTextSubject.send(searchText)
}
}
결과뷰에서는 아래와 같이 연결시킬 수 있다.
final class SearchResultsTableViewController: UITableViewController {
let searchResultsSubject = CurrentValueSubject<[Book], Never>([])
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "BookCell")
searchResultsSubject
.receive(on: DispatchQueue.main)
.sink { _ in self.tableView.reloadData() }
.store(in: &cancellables)
}
}
😳 Combine 적용 후기
역시 Combine도 RxSwift와 거의 동일하게 구현을 할 수 있게 되어있다. 애플에서 나온 프레임워크이다보니 .dataTaskPublisher 처럼 URLSession에서 제공하는 메서드들과 편하게 결합할 수 있다는 장점이 있는 것 같다. 물론 너무나도 깊지 않게 사용해 봤기 때문에 좀 더 사용해 봐야겠다는 생각이 들었다!
직접 사용해보면 차이점을 조금 느낄 수 있지 않을까? 해서 직접 사용해 봤지만... 역시 한참 멀었다. 다음에는 이론적으로라도 학습을 해보려고 한다.
'📺 Programming' 카테고리의 다른 글
[Programming] 반응형 프로그래밍 - 2. ReactiveX (1) | 2025.02.17 |
---|---|
[Programming] 반응형 프로그래밍 - 1. 기본 개념 (0) | 2025.02.17 |
[Programming] Test Double (0) | 2025.01.25 |
[Programming] 소프트웨어 아키텍쳐, 디자인 패턴 (0) | 2024.01.06 |