오늘 글은 .. 기승전결이 있기 때문에 .. 차근차근 .. 따라가보자 .. !
로그인 화면을 만들어보자.
이렇게 이름을 입력하는 텍스트 필드와, 텍스트 필드에 입력하는 값에 따라서 유효성 검사를 하는 경고 레이블, 그리고 버튼으로 UI를 만들 수 있다.
이 때의 로직은, 텍스트 필드에 8글자 이상 입력해야 경고 레이블이 사라지고 버튼이 활성화 되는 것이다.
그렇다면 우리는 아래와 같이 코드를 입력할 수 있다.
nameTextField.rx.text // String?
.orEmpty // String
.map { $0.count >= 8 } // Bool
.bind(to: stepButton.rx.isEnabled, validationLabel.rx.isHidden)
.disposed(by: disposeBag)
- 텍스트 필드의 입력값에 따라서 옵셔널 String 형태를 orEmty를 통해 String으로 반환하고, map 함수를 통해서 8글자 이상을 기준으로 Bool형을 반환하면,
- 이 때의 Bool값을 가지고 stepButton의 활성화 유무와 validationLabel의 보이고 사라지는 것의 유무를 다룰 수 있다.
🤔 왜 subscribe가 아니라 bind로 하지?
subscribe와 bind는 서로 다른 개념이 아니다. subscribe에서 좀 더 특화된 기능을 사용하기 위해 만들어진 것이 bind라고 할 수 있는데 ..
이벤트가 발생하고 next, error, complete로 처리가 될텐데 ..
여기서 생각해볼 것은 버튼을 탭하거나, 텍스트 필드에 값을 입력하는 것 등의 UI 관련된 작업이 error, complete가 발생할까?
.. 그렇지 않다 .. !!
그렇기 때문에 우리는 error 이벤트를 핸들링 할 필요가 없다 .. !!!!
-> 그래서 나온 개념이 bind !!!!!
(next만 다룰 수 있는 subscribe라고 생각할 수 있다.)
이 때, 만약 버튼의 배경색상도 관리하고 싶다고 한다면? 아래와 같이 코드를 추가할 수 있다.
nameTextField.rx.text
.orEmpty
.map { $0.count >= 8 }
.withUnretained(self)
.bind { vc, value in
let color: UIColor = value ? .systemPink : .lightGray
vc.stepButton.backgroundColor = color
}
.disposed(by: disposeBag)
마찬가지로 텍스트 필드에 입력한 글자 수에 따라서 8글자 이상이면 버튼의 배경색은 .systemPink로 지정하고 그렇지 않으면 .lightGray로 지정한다.
위의 코드를 한번에 작성하면 아래와 같다.
nameTextField.rx.text
.orEmpty // String
.map { $0.count >= 8 }
.bind(to: stepButton.rx.isEnabled, validationLabel.rx.isHidden)
.disposed(by: disposeBag)
nameTextField.rx.text
.orEmpty
.map { $0.count >= 8 }
.withUnretained(self)
.bind { vc, value in
let color: UIColor = value ? .systemPink : .lightGray
vc.stepButton.backgroundColor = color
}
.disposed(by: disposeBag)
그런데 .. 우리는 한가지 생각을 할 수 있다 .. 겹치는 코드를 줄일 수 있지 않을까 .. ?
왜냐하면.
이므로.
그러면 아래와 같이 중복되는 코드를 하나의 변수로 관리할 수 있을 것이다.
let validation = nameTextField.rx.text
.orEmpty // String
.map { $0.count >= 8 }
validation
.bind(to: stepButton.rx.isEnabled, validationLabel.rx.isHidden)
.disposed(by: disposeBag)
validation
.withUnretained(self)
.bind { vc, value in
let color: UIColor = value ? .systemPink : .lightGray
vc.stepButton.backgroundColor = color
}
.disposed(by: disposeBag)
그러고 이제 우하하 .. ! 끝 .. !
이럴 수 있는ㄷㅔ .. 과연 그럴까 ... ?
아래의 실험을 해보자.
let testA = stepButton.rx.tap
.map { "안녕하세요." }
testA
.bind(to: validationLabel.rx.text)
.disposed(by: disposeBag)
testA
.bind(to: nameTextField.rx.text)
.disposed(by: disposeBag)
testA
.bind(to: stepButton.rx.title())
.disposed(by: disposeBag)
이렇게 코드를 작성하고 .map 코드에 break point를 걸고 코드를 실행하면 ..
버튼을 한번 누를 때마다 3번 break가 걸리는 것을 확인할 수 있다.
그 말은?!
앞서 공통적인 코드를 합쳐놓은 것은 단순히 코드를 간소화한 것이지, 불필요하게 리소스가 낭비되고 있다는 것을 알 수 있다.
= 메모리 하나에서 관리하고 있는 것이 아니라 구독한 개수만큼의 메모리에서 관리하고 있는 것이다.
그럼 결국 의미 없는 코드 간소화인데 ..
일단 왜 이렇게 동작하는가?! 를 살펴보면 .. !!
🔥 이것은 Observable의 특성 중 하나로, stream을 공유하지 않는다.
let sampleInt = Observable<Int>.create { observer in
observer.onNext(Int.random(in: 1...100))
return Disposables.create()
}
sampleInt.subscribe { value in
print("sampleInt: \(value)")
}
.disposed(by: disposeBag)
sampleInt.subscribe { value in
print("sampleInt: \(value)")
}
.disposed(by: disposeBag)
sampleInt.subscribe { value in
print("sampleInt: \(value)")
}
.disposed(by: disposeBag)
위의 Observable(Custom Observable)을 실행해보자.
그럼 위와 같은 결과가 출력 되는 것을 볼 수 있다.
✅ 이것을 통해서 우리가 알 수 있는 것은 !! observable은 독립적인 stream을 갖고 있다는 것이다.
어떻게 해결할건데.
자, 그럼 이제 해결을 해보자. (일단 저 위의 실험 코드를 수정해보자.)
어떻게? share을 통해서 !!
observable에 share()을 통해서 stream을 공유하도록 지정할 수 있다.
let testA = stepButton.rx.tap
.map { "안녕하세요." }
.share() // -> stream을 공유하게 되므로 1번 호출
testA
.bind(to: validationLabel.rx.text)
.disposed(by: disposeBag)
testA
.bind(to: nameTextField.rx.text)
.disposed(by: disposeBag)
testA
.bind(to: stepButton.rx.title())
.disposed(by: disposeBag)
그러면, map에 break point를 걸었을 때,
버튼을 탭하면 break가 한번만 호출되는 것을 확인할 수 있다.
그런데 !! 여기서 !!! 이 stream을 공유할 수 있는 것이 어디에 구현되어 있는가?
🔥🔥🔥 바로 !! Subject에 share가 구현되어 있다. 🔥🔥🔥
그 말은 즉, subject는 stream을 공유하고 있다는 것이다.
이것 역시 실험으로 확인해보자.
let subjectInt = BehaviorSubject(value: 0)
subjectInt.onNext(Int.random(in: 1...100))
// stream을 공유
subjectInt.subscribe { value in
print("subjectInt: \(value)")
}
.disposed(by: disposeBag)
subjectInt.subscribe { value in
print("subjectInt: \(value)")
}
.disposed(by: disposeBag)
subjectInt.subscribe { value in
print("subjectInt: \(value)")
}
.disposed(by: disposeBag)
BehaviorSubject(Subject의 한 종류)로 subject를 하나 만들고 이를 구독해서 이벤트 결과를 출력해보자.
위와 같이 같은 결과값이 출력 되는 것을 볼 수 있다.
그러면 여기까지의 내용을 정리하면 아래와 같다.
Share ?
- 일반적으로 subscribe를 할 때마다 새로운 시퀀스가 생성 된다.
- 하나의 Observable을 subscribe하는 곳이 여러 군데라면 그만큼 호출 되면서 stream이 생기게 된다.
- 따라서, 여러 subscribe를 하게 될 경우, 불필요한 리소스가 발생할 수 있기 때문에 내부적으로 모든 subscribe가 하나의 subscribe를 공유할 수 있도록 해야 한다.
- replay와 scope를 통해 버퍼 사이즈와 유지 상태를 결정할 수 있다.
<예시>
- 서버통신을 하는 경우, share()가 구현이 되어 있지 않다면 네트워크 요청이 여러 번 일어나게 된다.
- 따라서, 불필요한 콜이나 리소스 낭비가 생기지 않도록 subscribe를 공유할 수 있게 처리한다.
이제 Bind를 한번 살펴보자.
Bind는 Subscribe의 한 형태로, next 이벤트만 다룬다고 생각할 수 있다.
그리고 Subscribe와 다르게 Main Thread의 작업을 보장한다.
만약 bind를 좀 더 UI에 특화된 형태로 만들고 싶다면?
-> stream을 공유할 수 있게 하면 된다.
-> 이 형태가 바로 !! Drive이다.
drive는 bind와 다르게 stream 공유가 가능하다.
drive 코드 내부적으로 share(replay: 1, scope: .whileConnected)가 구현되어 있다.
drive를 사용해서 bind 코드를 수정할 수 있다.
let testA = stepButton.rx.tap
.map { "안녕하세요." } // -> 3번 호출 (1대1로 대응되므로) / 여기까지는 RxSwift (근데 나는 UI에 특화된 형태로 바꾸고 싶다.)
.asDriver(onErrorJustReturn: "Error") // Driver로 쓸 수 있도록
// .share() // -> stream을 공유하게 되므로 1번 호출
testA
// .bind(to: validationLabel.rx.text)
.drive(validationLabel.rx.text) // 역할상 subscribe와 같다.
.disposed(by: disposeBag)
testA
// .bind(to: nameTextField.rx.text)
.drive(validationLabel.rx.text)
.disposed(by: disposeBag)
testA
// .bind(to: stepButton.rx.title())
.drive(validationLabel.rx.text)
.disposed(by: disposeBag)
주석 처리를 통해서 알 수 있는 것처럼 driver의 경우 내부적으로 share 코드가 구현되어 있으므로 따로 share를 명시하지 않아도 된다.
Relay는 Subject에서 좀 더 UI에 특화된 형태이다.
Relay는 next 이벤트를 accept라는 키워드로 emit할 수 있다.
코드로 나타내면 아래와 같다.
class SubjectViewModel {
var contactData = [
Contact(name: "SoKyte", age: 25, number: "01099998888"),
Contact(name: "HuRee", age: 25, number: "123456789"),
Contact(name: "Jack", age: 21, number: "876543201")
]
// var list = PublishSubject<[Contact]>()
var list = PublishRelay<[Contact]>()
func fetchData() {
// list.onNext(contactData)
list.accept(contactData)
}
func resetData() {
// list.onNext([])
list.accept([])
}
func newData() {
let new = Contact(name: "후리방구", age: -4, number: "뿡뿡")
// list.onNext([new]) // = 을 의미, 기존의 정보는 없어지고 후리방구만 남게 된다.
list.accept([new])
contactData.append(new)
}
func filterData(_ query: String) {
let filterData = query != "" ? contactData.filter { $0.name.contains(query) } : contactData
// list.onNext(filterData)
list.accept(filterData)
}
}
Relay는 drive와 짝꿍으로 사용할 수 있는데 .. 위의 로그인 화면을 예시로 생각해보자.
MVVM 구조로 나타냈을 때, 경고 레이블에 들어갈 text를 ViewModel에서 관리할 수 있고, 사용자의 입력 값에 따라서 그 문구를 다르게 표현할 수 있을 것이다.
ViewModel.swift
import Foundation
import RxSwift
import RxCocoa
final class ValidationViewModel {
// validation 문구
let validText = BehaviorRelay(value: "닉네임은 최소 8자 이상 필요해요") // 초기값이 필요한, UI에 특화된 형태 : Behavior Relay
}
그리고 사용자의 인터렉션을 받을 수 있는 Controller를 아래와 같이 작성할 수 있다.
viewModel.validText
.asDriver()
.drive(validationLabel.rx.text)
.disposed(by: disposeBag)
이렇게 초기값을 validationLabel에 보여줄 수 있고
상황에 따라서 viewModel.validText.accep("어쩌구 저쩌구")를 통해서 이벤트를 emit할 수도 있다.
그래서 결론적으로 !!
로그인 화면의 bind 로직은 아래와 같다.
즉, 내용을 다시 정리해보면 !!
Rx의 내용은 아래의 개념으로 나누어진다고 볼 수 있다.
색상별로 짝꿍 !!!
'Swift > RxSwift' 카테고리의 다른 글
[🔥Rx뿌셔] Hot Observable VS Cold Observable (0) | 2022.10.31 |
---|---|
[RxSwift] Rx+MVVM으로 로그인 화면을 만들어보자. (0) | 2022.10.31 |
[🌱SeSAC] Observable/Observer, Subject, Relay (0) | 2022.10.26 |
[🔥Rx뿌셔] Subject VS Observable (0) | 2022.10.26 |
[🌱SeSAC] Disposable, Observable, Subject (0) | 2022.10.26 |