“RxSwift 4시간에 끝내기 (종합편)” 영상을 바탕으로 정리를 진행했습니다. 영상을 보고 싶으신 분들은 하단 링크를 참고해주세요.
RxSwift 4시간에 끝내기(1) 에 이어지는 포스팅입니다.
Schedular
“이미지를 불러오는 동안 스크롤이 안돼!”
왜 이미지를 불러오는 동안 스크롤이 안되었을까요?
비동기적으로 처리가 되고 있지 않기 때문입니다.
버튼을 누른 순간 이미지를 불러온다고 하면, 아마도 IBAction 메서드 내부에서 동작을 하는 코드였겠죠?
IBAction 내부에서 동작하는 코드는 main thread에서 돌아가게 됩니다. 즉, 이미지를 불러오고 스크롤을 하는 모든 동작들이 main thread에서 실행되고 있는거죠.
main thread는 Serial Queue 입니다. 동작들이 병렬적으로 돌아갈 수 없다는 걸 뜻합니다.
비동기적으로 처리하기 위해서는 Concurrent Queue를 사용하는 스레드로 옮겨주어야 합니다.
스레드를 옮기는 방식은 총 2가지가 있습니다.
1.
observe(on: _)
observe(on: _)의 괄호 안에는 Schedular를 넣어주어야 합니다. 해당 Schedular는 ReactiveX에서 미리 만들어두고 제공해주는 것이기 때문에 그대로 가져와서 사용하면 됩니다.
스트림 중간에 하단 코드를 넣어주면 observe(on:_) 코드 다음 줄부터 observe(on:_) 내부에 들어가는 Schedular에서 돌아가도록 바뀝니다.
.observe(on: ConcurrentDispatchQueueScheduler.init(qos: .default))
Swift
복사
하지만, 이미지 세팅을 진행하기 위해서는 main thread로 돌아와야 합니다.
이미지 세팅을 진행하는 부분은 아마 subscribe 부분일 겁니다. 그렇다면, 우리는 subscribe 직전에 스레드를 바꿔주는 코드를 집어 넣으면 됩니다.
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] image in
self?.imageView.image = image
})
Swift
복사
만약, 데이터를 가져오는 것처럼 오래 걸리는 일이 just() 부터 일어나면 어쩌죠?
이 경우에는 observe(on:_)으로 다룰 수 없습니다. 다른 방법이 필요합니다.
첫 줄부터 영향을 끼칠 수 있는 방식이 필요합니다. 바로, subscribe(on:_) 입니다.
2.
subscribe(on: _)
해당 Operator는 observe(on:_)과 다르게 어느 위치에 있던지 상관없습니다.
왜 상관이 없나요?
해당 Operator를 쓸 때부터 이 Schedular를 사용해서 전체를 해당 Schedular로 돌리겠다는 뜻이 됩니다.
Observable.just("Hello world")
.map { "\($0)!"}
.subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .default))
.observe(on: MainScheduler.instance)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
Swift
복사
subscribe(on:_)에 의해서 해당 스트림은 처음부터 ConcurrentDispatchQueueSchedular로 돌아가게 됩니다.
하지만, 중간에 있는 observe(on:_)에 의해서 subscribe 이전에는 main thread로 바뀝니다.
해당 Operator들을 사용하기 위해서 Schedular를 여러 알 필요는 없습니다.
중요한 Schedular는 main thread에서 돌아갈 수 있도록 도와주는 MainSchedular.instance와 동시에 일을 수행할 수 있도록 도와주는 ConcurrentDispatchQueueSchedular 입니다.
Schedular 예시 코드에서 side-effect가 있는 부분을 봤을 겁니다.
side-effect란 외부에 영향을 주는 부분입니다. 우리는 영향을 주는 부분을 subscribe에 작성합니다.
.subscribe(onNext: { [weak self] image in
self?.imageView.image = image
})
Swift
복사
Operator 부분에서는 밖이랑 전혀 상관없이 코드를 진행하다가 subscribe에서 밖에 있는 imageView에 접근해서 imageView의 image 프로퍼티를 변경합니다.
이렇게, side-effect를 허용해주는 부분은 2군데 입니다.
1.
subscribe
2.
do
물론, 다른 부분에서도 외부 요소에 접근을 해도 되지만 side-effect를 만드는 것은 좋지 않은 일이니깐요.
RxSwift 응용해보기
RxCocoa는 Cocoa framework의 View를 다룰 때 좋을 만한 extension들이 추가로 들어 있습니다.
우리는 이전에 Stream에 데이터를 넣어서 흘려 보내고 이를 subscribe에서 받아서 사용한다는 걸 배웠습니다. Stream에는 데이터말고도 이벤트도 흘려 보낼 수 있습니다.
이벤트를 보낼 수 있다는건 click 이벤트나 text change 이벤트를 Stream으로 보내서 subscribe에서 받아서 사용할 수 있다는걸 의미합니다. 즉, UI 이벤트를 스트림으로 보내서 비동기적으로 처리할 수 있다는겁니다.
*RxCocoa는 RxSwift에서 확장된 기능이기 때문에 RxCocoa가 RxSwift라거나, RxSwift의 기본이라고 생각하면 안됩니다.
텍스트 필드에서 텍스트를 받아서 스트림으로 흘려 봅시다.
textField.rx.text
Swift
복사
rx를 쓰면 해당 컴포넌트가 취급하는 데이터를 비동기로 받겠다는 뜻이 됩니다. 따라서, 텍스트 필드에서 취급하는 text 데이터를 비동기로 받을 수 있게 됩니다. 즉, text가 Observable Stream으로 보내지게 됩니다.
Stream으로 보내지니깐 해당 값을 Subscribe할 수 있겠네요.
textField.rx.text
.subscribe(onNext: { text in
print(text)
})
.disposed(by: disposeBag)
Swift
복사
그럼, 비동기 Stream에 텍스트 필드에서 발생하는 텍스트가 잘 전달되는지 확인해볼까요?
잘 들어오는군요. ⸜(ˊᗜˋ)⸝
글자가 계속 이벤트로 들어오니깐 해당 글자가 우리가 정한 조건에 맞는지 확인할 수 있겠네요?
일단, 들어온 글자가 이메일 조건에 맞는지 확인하는 메서드를 하나 만들었습니다.
private func checkEmailValid(_ text: String) -> Bool {
return text.contains("@") && text.contains(".")
}
Swift
복사
그리고 해당 조건을 만족하면 라벨을 하나 숨겨보겠습니다.
textField.rx.text
.map { self.checkEmailValid($0!) }
.subscribe(onNext: { label.isHidden = $0 })
.disposed(by: disposeBag)
Swift
복사
이메일 형식에 부합하면 라벨이 사라집니다.
위의 코드에서 한 가지 더 수정하고 싶은 부분이 있습니다. 바로, text를 강제 언래핑을 통해서 가져오는 부분입니다. 매번 강제 언래핑을 할 순 없습니다.
RxCocoa에서는 orEmpty를 제공해줍니다. orEmpty를 사용하면 String?이 아닌 String 타입으로 데이터가 내려옵니다. 즉, 더이상 강제 언래핑을 하지 않아도 됩니다.
textField.rx.text.orEmpty
.map { self.checkEmailValid($0) }
.subscribe(onNext: { label.isHidden = $0 })
.disposed(by: disposeBag)
Swift
복사
만약, password를 위한 텍스트 필드도 사용한다고 해볼게요.
이번에는 이메일과 패스워드 텍스트 필드에 모두 조건에 부합하는 값이 들어오면 버튼을 enable 시켜봅시다.
이메일과 패스워드 텍스트 필드가 조건에 맞는 값을 가지고 있는지 확인해주는 부분에서 두 가지 모두 true 값을 반환 받았다면 isEnable를 true로 설정해주면 됩니다.
그렇다면, 두 가지 조건으로부터 오는 값을 병합해서 볼 필요가 있습니다.
이메일, 패스워드에서 오는 이벤트를 다 받고, 어느 한 쪽이라도 바뀌면 다시 두 가지 결과를 조합해서 Enabled에 값을 설정해줘야 합니다. 이 상황에 맞는 Operator는 combineLatest 일 것 같네요.
먼저, 패스워드 조건에 대한 메서드를 하나 만들어 주겠습니다.
private func checkPWValid(_ text: String) -> Bool {
return text.count > 7
}
Swift
복사
이제 이메일과 패스워드 이벤드를 병합해보겠습니다.
Observable.combineLatest(
textField.rx.text.orEmpty,
pwTextField.rx.text.orEmpty,
resultSelector: { self.checkEmailValid($0) && self.checkPWValid($1)}
)
Swift
복사
일단, 텍스트 필드로부터 오는 텍스트 이벤트를 받아서 해당 텍스트가 조건에 부합하는가 확인하는 부분에 넣어줬습니다. 두 가지 조건이 모두 만족하면 true가 스트림으로 보내질 것이고, 아니라면 false가 스트림으로 보내질 겁니다.
그리고, 결과는 버튼의 isEnabled에 바인드했습니다.
Observable.combineLatest(
textField.rx.text.orEmpty,
pwTextField.rx.text.orEmpty,
resultSelector: { self.checkEmailValid($0) && self.checkPWValid($1)}
)
.bind(to: button.rx.isEnabled)
.disposed(by: disposeBag)
Swift
복사
성공적으로 로그인을 할 수 있겠네요. ⸜(♡'ᗜ'♡)⸝
우리는 combineLatest를 사용해서 스트림의 병합을 진행했습니다. combineLatest말고도 다양한 병합 연산자가 존재합니다. merge나 zip같은 연산자가 말이죠.
왜 두 Operator는 이 상황에서 사용하지 않았을까요?
merge 연산자는 두 스트림에서 데이터를 받고 데이터가 전달되는대로 그대로 전달합니다. 그냥 두 스트림에서 들어오는 값들을 순서대로 전달하기 위해서 사용합니다.
zip 연산자는 두 스트림에서 데이터를 받았을 때, 두 스트림에서 오는 각각의 데이터가 모두 준비되면 그제서야 값을 내려줍니다. 한 스트림에서 오는 데이터가 바뀌어도, 다른 스트림에서 오는 데이터가 바뀌지 않으면 값이 전달되지 않습니다.
즉, 두 연산자 모두 현재 상황에 맞지 않습니다.
코드를 이렇게 작성했더니 뒤죽박죽 될 수 있었던 코드가 한 메서드에서 모두 처리됩니다. 한 메서드에 로직을 다 적을 수 있을 정도로 간단하게 바뀌었습니다. 하지만, 좀 더 정리를 해보겠습니다.
현재, 코드를 보면 input이 있고, output이 있습니다.
input으로 우리가 볼 수 있는건 이메일 입력, 패스워드 입력 이겠네요. output은 라벨 히든, 로그인 버튼 활성화 입니다.
우리가 Observable를 생성했다고 해서 무조건 바로 subscribe할 필요는 없습니다. 나중에 필요할 때에 subscribe해주면 됩니다. 따라서, 이 방식대로 코드를 수정해보겠습니다.
let emailValid = textField.rx.text.orEmpty.map { self.checkEmailValid($0) }
let pwValid = pwTextField.rx.text.orEmpty.map { self.checkPWValid($0) }
Swift
복사
먼저, 라벨 부분과 병합 부분에서 중복을 없애기 위해서 Valid까지 확인해서 Observable<Bool>를 가지는 두 프로퍼티를 만들었습니다.
그리고 각각을 이전에 만들었던 코드에 대체시킵니다.
let emailValid = textField.rx.text.orEmpty.map { self.checkEmailValid($0) }
let pwValid = pwTextField.rx.text.orEmpty.map { self.checkPWValid($0) }
Observable.combineLatest(emailValid, pwValid) { $0 && $1 }
.bind(to: button.rx.isEnabled)
.disposed(by: disposeBag)
emailValid
.subscribe(onNext: { label.isHidden = $0 })
.disposed(by: disposeBag)
Swift
복사
emailValid, pwValid라는 input Observable를 만들고, 해당 Observable를 사용해서 output를 냈습니다.
이전보다 훨씬 깔끔한 코드가 되었네요.
Subject
우리는 이전 챕터에서 emailValid를 사용해서 변수로 가지고 있었습니다.
이번에는 Subject를 사용해서 Valid 여부를 지켜보다가 나오는 값으로 라벨의 히든처리를 하는 코드를 작성해보려고 합니다.
먼저, Subject가 뭔지부터 알고 갑시다. Subject는 4가지가 존재합니다.
1.
BehaviorSubject
그림에서 볼 수 있듯이 BehaviorSubject는 default 값을 처음에 넣어줍니다. 그리고 subscribe를 하면 처음에 넣어준 값을 전달해줍니다.
중간에 subscribe가 발생하게 되면 마지막(최근) 값인 초록색 데이터를 default 값으로 전달해줍니다.
Subject는 subscribe를 할 수 있습니다.
그말인즉슨, Subject는 Observable이라는 말입니다. 근데 Observable과는 다른점이 있습니다.
바로, 스스로 데이터를 발생시킬 수 있다는 점입니다.
Observable은 just, from Operator를 사용해서 데이터를 생성해주어야 했습니다. 이는 이미 가지고 있던 데이터를 준 겁니다. 하지만, Subject는 이후에 데이터가 발생하면 외부에서 데이터를 넣어줄 수 있습니다. 즉, 데이터를 넣어줄 수도 subscribe 해줄 수도 있는거죠.
2.
PublishSubject
PublishSubject는 가장 간단한 형태입니다. BehaviorSubject와는 다르게 default값을 가지지 않습니다. 그냥 subscribe를 하면 아무런 값을 주지 않습니다. 나중에 데이터가 발생하면 그 때 전달됩니다.
3.
ReplaySubject
ReplaySubject는 이벤트 발생 중간에 subscribe를 하면 이전에 들어온 데이터들을 모두 받을 수 있습니다.
4.
AsyncSubject
AsyncSubject는 끝이 나야 전달이 됩니다. Completed된 시점에 가장 마지막에 전달된 데이터가 내려갑니다.
예를 들어서, 사용자가 좋아요를 눌렀다는걸 서버에 보내주려고 합니다. 좋아요를 했다는걸 상태로만 가지고 있다가 사용자가 해당 페이지에서 나갈 때 좋아요 했다는걸 서버로 보내주는 겁니다. 이럴 때, AsyncSubject를 사용하면 되겠죠?
그러면, 아까 Observable로 만들었던 코드를 Subject로 바꿔 봅시다.
이메일, 패스워드의 valid 값을 받을 Subject를 먼저 만들었습니다.
let emailSubject = BehaviorSubject<Bool>(value: false)
let pwSubject = BehaviorSubject<Bool>(value: false)
Swift
복사
그리고 emailValid 스트림에 있는 데이터를 emailSubject 스트림으로 전달합니다.
emailValid
.bind(to: emailSubject)
.disposed(by: disposeBag)
Swift
복사
onNext로도 전달할 수 있습니다.
emailValid
.subscribe(onNext: { isValid in
emailSubject.onNext(isValid)
})
.disposed(by: disposeBag)
Swift
복사
Subject는 데이터를 전달할 통로만 만들어두고 나중에 외부에서 넣어줄 수 있도록 해줍니다. 외부에서 통제할 수 있는 Observable인 셈이죠.
input에서 들어오는 값은 Subject에 저장하고, output은 저장된 Subject를 보고 적절하게 행동 처리를 할 수 있기 때문에 메소드를 분리할 수 있게 됩니다.
+⍺
우리가 네트워크를 통해서 데이터를 가져와서 해당 데이터를 통해서 라벨이 보일지 안보일지를 정한다고 해봅시다. 네트워크에 요청을 보내고 데이터를 가져오는 부분은 main thread가 아닌 다른 thread에서 돌고 있을겁니다.
그렇기 때문에, 이전에 공부했던 observe(on:_)를 사용해야 합니다.
즉, UI와 관련된 부분은 항상 이 작업을 진행시켜주어야 한다는 뜻입니다. 짱 귀찮네요.
이 귀찮은 작업을 대신 해주는 것이 바로 driver 입니다.
observe(on:_)를 사용하지 않고도 main thread에서 해당 프로세스가 도는 것을 보장해줍니다.
Observable.combineLatest(emailValid, pwValid) { $0 && $1 }
.asDriver(onErrorJustReturn: false)
.drive(button.rx.isEnabled)
.disposed(by: disposeBag)
Swift
복사
해당 API가 UI에 영향을 끼치는 API라고 판단이 된다면 Observable이 아니고 Driver를 반환해주면 됩니다.
UI 관련 부분은 또다른 문제를 가지고 있습니다.
우리가 아는 Observable은 이벤트를 방출하다가도 complete되는 시점을 가지고 있습니다.
하지만, UI 이벤트는 아닙니다. 아까 봤던 텍스트 필드도 텍스트가 들어오면 데이터를 계속 발생시켜야 합니다.
그럼, 입력을 안하고 있으면?
입력을 안하고 있으면 대기를 타고 있습니다. 즉, complete를 내지 않습니다. dispose가 되지 않는다는 뜻이죠.
만약, side-effect를 내는 코드를 작성한다고 했을 때, 외부에 있는 컴포넌트를 self키워드를 사용해서 접근했을 겁니다. 이 부분에서 문제가 발생합니다.
subscribe의 self에 의해서 reference count가 증가했을 겁니다. 하지만, VC가 화면 밖으로 사라져도 reference count가 0이 안되기 때문에 메모리에서 해제될 수가 없는겁니다.
이걸 해소하기 위해서 [weak self]를 사용해야 합니다.
약한 참조를 사용해서 reference count를 애초에 증가시키지 않으면 됩니다. 물론, 명확하게 complete가 되는 just, from은 이렇게까지 고려해주지 않아도 괜찮습니다.
completed가 되면 다 삭제 되기 때문이죠.
간단한 방식이 있다면 viewWillDisappear에서 뷰가 사라질 때 disposeBag에 있는 disposable를 싹 다 사라지게 만들면 됩니다.
override func viewWillDisappear(_ animated: Bool) {
disposeBag = DisposeBag()
}
Swift
복사
하지만, 뷰가 사라지지 않았다면 데이터는 계속 발생해야 합니다. 즉, UI 이벤트는 error나 completed에 의해서 이벤트 발생을 멈추면 안됩니다.
Subject는 next, error, completed를 모두 발생시킵니다. UI 이벤트를 다루기엔 적합하지 못하다는 겁니다. 하지만, Subject와 비슷하면서 error, completed를 발생시키지 않는 방식이 존재합니다.
바로, “Relay” 입니다.
Relay는 error, completed를 보낼 수 없습니다. 따라서, Relay는 Stream이 종료되지 않습니다.
UI가 error, completed가 되었다고 더이상 업데이트가 안된다면?
안됩니다. 에러가 났다고 input stream이 끊어지면 input를 넣었을 때 아무런 처리를 할 수 없습니다.
이런 경우가 UI에서 발생하면 안됩니다.
그래서 UI용 Subject를 만든겁니다. Relay는 UI와 연결시에 사용합니다.