예제로 시작하는 RxSwift #1 - 기초
2017.09.26
by Łukasz Mróz, translated by pilgwon
수정 2017년 1월 18일: 이 게시글은 Swift 3과 RxSwift 3.1로 업데이트 되었습니다
Swift는 무엇을 하든 좋은 느낌을 줍니다. 다른 언어의 장점을 모아두었기 때문에 입문자들은 더 유연하고 쉽게 이해할 수 있습니다. 이런 이유로 Swift는 객체 지향 프로그래밍과 함께 쓰이면서 WWDC’15에서 소개된 프로토콜 지향 프로그래밍의 패러다임과 더 가까운지 알 수 있습니다. 또한 Swift에서 함수형 프로그래밍과 반응형 프로그래밍 을 쓸 수 있는 것을 검색할 필요가 없습니다. 오늘부터 몇 주 동안 우리는 이 두가지의 조합인 Functional Reactive Programming(함수형 반응형 프로그래밍) 에 대해 알아볼 것입니다.
Functional Reactive Programming 은 무엇일까요? 짧게 말하자면 반응형 프로그래밍을 함수형 프로그래밍 블록(filter, map, reduce 등)과 함께 사용하는 것입니다. 놀랍게도, Swift에는 이미 내장되어 있습니다! 그리고 반응형 부분에 대해서는 RxSwift가 도와주고 있습니다.
RxSwift 는 Swift로 작성된 반응형 익스텐션 버전입니다.
ReactiveX는 Observer 패턴, Iterator 패턴 그리고 함수형 프로그래밍에서 나온 훌륭한 아이디어들의 조합입니다
기본적으로 변수에 정적으로 값을 할당하는 것에서 무언가 미래에 바뀔 수 있는 것을 관찰(observe)하는 것으로 관점을 변경해야 합니다.
“왜 제가 그걸 원해야하죠?”라고 묻고 싶으실 것입니다. 답은 간단합니다. 그것은 일을 간단하게 만들어줍니다. 테스트 하기 어려운 notification 대신에, 신호(signal)를 사용할 수 있습니다. 많은 코드를 작성해야하는 delegate 대신에, 블록을 작성해서 switch와 if를 삭제할 수 있습니다. RxSwift에는 쉽게 조종할 수 있는 KVO, IBAction, 입력 필터, MVVM 그리고 더 많은 것이 포함되어 있습니다. 기억하세요, 이것은 언제나 문제를 해결하기 위한 가장 좋은 방법은 아니지만, 이것의 모든 잠재력을 알고 언제 사용할 지 알아야 할 필요가 있습니다. 어플리케이션에서 사용할 수 있는 몇 가지 예제를 보여드리겠습니다.
정의.
먼저, 몇 가지 정의로 시작하겠습니다. 논리의 더 나은 이해를 위해 아주 기본부터 하겠습니다.
스마트폰은 관찰이 가능(observable) 합니다. 스마트폰은 페이스북 알림, 메세지, 스냅챗 알림 등과 같이 신호(signal)를 방출 합니다. 우리는 자연적으로 스마트폰을 구독(subscribe)하고 있고, 모든 알림을 홈 스크린에서 확인할 수 있습니다. 이제 그 신호(signal) 로 무엇을 할 지 정할 수 있습니다. 우리는 관찰자 (observer) 입니다.
예제를 위한 준비가 끝났습니다! 🎉
예제.
이제 City Searcher라는 서치 박스에 도시 이름을 입력할 수 있고 동적으로 목록을 보여주는 앱을 만들어보겠습니다. 검색 바에 무언가가 적힐 때, 동적으로 주어진 글자로 시작하는 이름을 가지고 있는 도시들을 걸러내고, 테이블뷰에 그려줘야 합니다. 간단하죠? 동적 검색을 앱에서 만드려고 시도할 때, 무엇이 잘못될 것인지에 대해 항상 생각해야 합니다. 예를 들어 만약 제가 정말 빠르게 타이핑할 수 있고 자주 생각이 바뀐다면 어떨까요? 아마 필터링 하기 위한 API 요청을 아주 많이 해야할 것입니다. 실제 앱에서는 이전의 요청을 취소하거나, 다른 요청을 던지기 전에 잠시 기다리거나, 검색할 키워드가 이전과 같은지 확인해야 하는 등의 작업이 필요합니다. 이처럼 많은 경우에서 처음 봤을 때 꽤 쉬워보이는 기능이 엄청나게 큰 로직이 될 때가 있습니다. “그냥 간단한 동적 검색이잖아요, 무엇이 문제죠?” 당연하게도 당신은 Rx없이 이 문제를 해결할 수 있지만, 코드를 거의 사용하지 않고 로직을 작성하는 방법을 알아봅시다.
먼저 프로젝트를 만들어야 합니다. (만약 어떻게 하는지 모르신다면, 튜토리얼이 준비되어 있습니다. 여기서 확인 가능 합니다.) 그리고나서 CocoaPods와 RxSwift + RxCocoa를 설치해야 합니다. 예시 Podfile 을 보여드리겠습니다:
platform :ios, '8.0'
use_frameworks!
target 'RxSwiftExample' do
pod "RxSwift"
pod "RxCocoa"
end
만약 모든 도구가 준비됐다면, 우린 코드를 작성할 준비가 끝난 것입니다!
이제 UISearchBar
와 UITableView
를 합친 간단한 UI를 만들어 보겠습니다.
그 다음은 도시를 저장할 배열이 필요합니다. 코드의 로직을 줄이기 위해 다음 튜토리얼에서 다룰 예정인 API는 사용하지 않을 것입니다. 그 대신에 모든 도시 정보가 저장될 배열 하나와 보여줄 도시 정보가 저장될 배열, 총 두 개의 배열을 만들것입니다. 이것은 API만큼 효과적일 것입니다.
var shownCities = [String]()
let allCities = ["New York", "London", "Oslo", "Warsaw", "Berlin", "Praga"] // 고정된 API 데이터
이젠 UITableViewDataSource
를 설정하고 shownCities
와 연결할 것입니다:
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return shownCities.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cityPrototypeCell", for: indexPath)
cell.textLabel?.text = shownCities[indexPath.row]
return cell
}
이젠 보통의 UITableView
와 동일하게 작동할 것이고 shownCities
의 값을 바꾸면 화면에 보일 것입니다.
더 흥미로운 내용으로 넘어가보겠습니다. 우린 이제 UISearchBar 의 텍스트를 관찰(observe) 할 것입니다. (RxSwift 의 익스텐션인) RxCocoa 에 관찰에 관한 것이 만들어져 있는 덕분에 이 작업은 매우 쉽습니다. Cocoa 프레임워크에 있는 UISearchBar
와 다른 많은 컨트롤들이 Rx 팀의 지원을 받고 있습니다. UISearchBar
를 쓰는 우리의 경우엔, 검색 바의 텍스트가 변경될 때 신호를 발생하는 속성인 rx.text
를 사용 할 수 있습니다. 멋집니다! 그럼 어떻게 이를 관찰할까요? 쉽습니다! 먼저 RxCocoa 와 RxSwift 를 추가해야 합니다.
import RxCocoa
import RxSwift
그 다음 관찰하는 부분으로 가봅시다! ViewDidLoad*()
에서 UISearchBar
의 rx.text
속성을 관찰하는 코드를 추가할 것입니다:
searchBar
.rx.text // RxCocoa의 Observable 속성
.orEmpty // 옵셔널이 아니도록 만듭니다.
.subscribe(onNext: { [unowned self] query in // 이 부분 덕분에 모든 새로운 값에 대한 알림을 받을 수 있습니다.
self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // 도시를 찾기 위한 “API 요청” 작업을 합니다.
self.tableView.reloadData() // 테이블 뷰를 다시 불러옵니다.
})
.addDisposableTo(disposeBag)
메모: RxSwift 3.1에서는 Swift 2.2 이하의 버전에 있던
rx_something
접두사 대신에,rx.something
이 있습니다. 그 덕분에, 어떤 속성 또는 메소드가 주어진 객체에 존재하는지 확인하기 더 쉬워졌습니다. 게다가subscribeNext
문법이 없어졌습니다. 하지만 제 팟인 RxShortcuts를 사용해서 이것과 다른 도움되는 함수들을 돌려낼 수 있습니다.
완벽합니다! 그리고 동적 검색도 저것과 같이 작동합니다! subscribeNext
는 아마도 이해가 가능할 것입니다. 우리는 신호를 발생하는 Observable 속성을 구독중입니다. 그것은 마치 자신의 폰에 “좋았어. 너가 가진 모든 시간이 새로워졌으니 나에게 보여줘.” 이라고 하는 것과 같습니다. 이제 완전히 새로워진 내용을 보여줄 것입니다. 우리의 경우엔 새로운 값만 필요로 하지만, 구독하기(subscribe)는 onError, onCompleted 등과 같은 이벤트가 포함된 더 많은 래퍼도 가지고 있습니다.
더 흥미로운 부분은 마지막 줄입니다. Observable을 구독할 때, 객체가 할당 해제될 때 종종 구독을 취소하려는 경우가 있습니다. Rx에서는 DisposeBag
이라 불리는 보통 deinit
과정에서 구독 해제하려는 모든 것을 보관하는 것이 있습니다. 어떤 경우에서는 필요하지 않지만, 그 가방을 만들어서 거기에 사용 후에 버릴 것들을 넣는 것이 일반적인 방법입니다. 다음 시간에 이 과정에 도움이 되는 라이브러리를 어떻게 사용하는지에 대해 배울거지만, 지금은 프로젝트를 컴파일하기 위해 그 가방을 만들겠습니다:
var shownCities = [String]()
let allCities = ["New York", "London", "Oslo", "Warsaw", "Berlin", "Praga"] // 고정된 API 데이터
let disposeBag = DisposeBag() // 뷰가 할당 해제될 때 놓아줄 수 있는 일회용품의 가방
컴파일이 끝나고 나면, 작동하는 어플리케이션이 생겼을 것입니다! “O”를 입력하면, 테이블뷰에 Oslo가 보여져야 합니다. 훌륭합니다! 하지만… 우리가 무서워 하는 것들은 어떨까요? 과도한 API 요청? 빈 키워드? 딜레이? 맞습니다, 우리는 우리를 지킬 필요가 있습니다. API 백엔드를 보호하는 것부터 시작해 봅시다. 타이핑 후에 검색 키워드가 변하지 않은 지 X초가 지나면 요청을 시작하는 딜레이를 추가해야 합니다. 보통 이 경우엔 NSTimer
인스턴스를 사용해서 딜레이 후에 요청을 시작하거나 검색어가 바뀌었을 때 무효화하는 식으로 구현을 합니다. 그렇게 어렵지는 않지만, 여전히 오류가 발생할 여지가 있습니다. 먼저, “O”를 입력했고 검색 결과가 나왔습니다. 그런 다음 “Oc”를 입력한 후에 마음이 바뀌어서 딜레이 끝나고 API 요청이 시작되기 전에 “O”로 바뀌었다고 가정해봅시다. 그 경우엔 API에 똑같은 요청을 두 번 할 것입니다. 어떤 경우엔 데이터베이스가 너무나도 빨리 리프레쉬되어서 그 행동을 원할수도 있습니다. 하지만 보통은 똑같은 두 요청을 0.5초안에 보낼 필요가 없습니다. Rx없이 이 작업을 하려면 플래그와 최근 검색한 쿼리를 추가하고 새로운 것과 비교해야 할 것입니다. 코드는 많지 않지만, 로직은 점점 자랍니다. RxSwift에서는 두 줄의 코드로 이 작업을 할 수 있습니다. debounce()
는 주어진 스케쥴러에 맞춰서 딜레이를 만들고, distinctUntilChanged()
는 같은 값을 입력하는 것을 막아줍니다. 이전의 버전과 연결해서 작성해보면 아래와 같을 것입니다:
searchBar
.rx.text // RxCocoa의 Observable 속성
.orEmpty // 옵셔널이 아니도록 만듭니다.
.debounce(0.5, scheduler: MainScheduler.instance) // 0.5초 기다립니다.
.distinctUntilChanged() // 새로운 값이 이전의 값과 같은지 확인합니다.
.subscribe(onNext: { [unowned self] query in // 여기서 새로운 값에 대한 구독을 합니다.
self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // 도시를 찾기 위한 “API 요청” 작업을 합니다.
self.tableView.reloadData() // 테이블 뷰를 다시 불러옵니다.
})
.addDisposableTo(disposeBag)
아름답습니다! 하지만… 무언가 빼먹은 게 있습니다. 만약 사용자가 무언가를 입력해서 테이블 뷰가 업데이트 되었고, 검색어를 지워서 새로운 값이 빈 값이 주어진다면 어떨까요? 맞습니다, 빈 값을 쿼리로 던져야 할 것입니다… 우리의 경우엔 그러지 않기를 원하기 때문에 그것을 막아야 합니다. 우리는 filter()
를 사용할 것입니다. 하지만 이런 질문이 들 것입니다: “왜 내가 하나의 값에 필터를 써야하지? filter()
는 컬렉션에 작동한다고!!!” 이건 매우 좋은 질문입니다! 하지만 Observable을 값이나 객체로 보지 말아 주세요. 이것은 값의 흐름이고, 결국엔 일이 일어날 것입니다. 그러므로 쉽게 함수 블록의 사용법을 이해할 수 있을 것입니다. 주어진 값을 필터링하기 위해 문자열 배열을 사용하여 값을 처리합니다. 간단하게 해보겠습니다:
searchBar
.rx.text // RxCocoa의 Observable 속성
.orEmpty // 옵셔널이 아니도록 만듭니다.
.debounce(0.5, scheduler: MainScheduler.instance) // Wait 0.5 for changes.
.distinctUntilChanged() // 새로운 값이 이전의 값과 같은지 확인합니다.
.filter { !$0.isEmpty } // 새로운 값이 정말 새롭다면, 비어있지 않은 쿼리를 위해 필터링합니다.
.subscribe(onNext: { [unowned self] query in // Here we subscribe to every new value, that is not empty (thanks to filter above).
self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // 도시를 찾기 위한 “API 요청” 작업을 합니다.
self.tableView.reloadData() // 테이블 뷰를 다시 불러옵니다.
})
.addDisposableTo(disposeBag)
오늘은 여기까집니다! 질문은 언제나 환영입니다! 전체 프로젝트는 깃헙에서 확인하실 수 있습니다!
전체 소스 코드는 Droids on Roids의 깃헙에서 볼 수 있고 여기서 다른 RxSwift 예제들을 확인할 수 있습니다!
레포에는 많은 예제 프로젝트들이 있습니다: 몇몇은 이미 언급되었고 몇몇은 아니지만 다음 튜토리얼을 위해서 확인하실 수 있습니다! 치얼스! 😎
RxSwift에 관련된 다음 게시글을 읽어보세요 (영어 원문)
» RxSwift by Examples #2 – Observable and the Bind.