✅ 이번 시간에는 RxSwift의 전반에 대해서 알아볼 예정.
RxSwift의 공부 순서로는 큰 그림을 먼저보고, 디테일을 들여다 볼 것임.
## 주의
- 해당 포스팅은 정보 전달보다 그냥 내가 공부하려고 쓴거라 문서 느낌은 아님
✅ 공식문서 링크
오퍼레이터들이 어떻게 동작하는지에 대한 부분
http://reactivex.io/documentation/ko/observable.html
✅ 목차
1. Observable 사용법
2. Operator - just, from, subscribe
3. Observable 생명주기, Thread 관리
4. Subject - Observable 외부에서 값을 컨트롤 할 수 있을까?
5. RxCocoa를 이용해 UI를 컨트롤 해보자.
6. 추가로 조금 더 알아보자
✅ Observable 사용법 플로우 정리
- Observable create
- subscribe 로 데이터 사용
- Disposable 로 작업 취소
3가지 형태로 흘러감
✅ RxSwift를 사용하는 이유
"RxSwift 는 비동기 작업의 결과를 Completion closure 가 아닌, 함수의 return 값으로 전달하기 위한 유틸리티 중 하나이며,
전달하는 것이 목표"
✅ 내가 생각하는 이유
- UIKit에서 RxSwift없이 Combine으로만 개발할 경우 SnapKit 없이 오토레이아웃 쓰겠다는 것과 비슷한 느낌.
- UIKit의 경우에는 Combine보다 RxSwift를 사용할 줄 알다면 이거 쓰는게 더 편하고 남.
- 특히 ControlEvent 쪽은 RxCocoa가 Combine보다 훨씬 더 나음
- UIKit에서 Rx사용할 줄 안다면 적극적으로 쓰기
- SwiftUI에서도 대부분 async-await로 해결되고 있어서 combine 그렇게 막 다루는 케이스가 있던 것 같지는 않음.
Observable이란?
RxSwift 에서 제공하는 "나중에 생기는 데이터" 타입의 이름이 Observable 이다.
비동기로 인해 생기는 데이터가 나중에 생기는 데이터인데, 이걸 우리는 completion closure가 아닌 return 값으로 받아서 사용함.
즉, 비동기로 생기는 데이터를 Observable 타입으로 래핑해서 사용하는 방법은 어떻게 될까?
✅ Observable의 생명주기
1. Create
2. Subscribe -> 이거 되었을 때 동작한다. (실행된다는 의미)
3. onNext
---- 끝 ---- 옵저버블 재사용 불가하다. 새로운 subscribe 필요
4. onCompleted / onError
5. Disposed
이런 순서로 진행된다.
Subscribe에 나와있듯이 create 만으로는 코드를 사용할 수 없는데, 바로 아래에 나올 코드는 return으로 create후 한번에 처리하였기에, Subscribe없이도 작동하였다. 다만, 이런 경우에는 여러 곳에서 사용할 수 없고 일회적으로 사용된다.
그럼 코드를 보면서 함께 알아볼까?
✅ 아래의 코드는 서버로 부터 데이터를 받아오는 코드인데, RxSwift를 이용하여 작성되었다.
var disposeBag = DisposeBag()
private func loadImage(from url: String) -> Observable<UIImage?> {
return Observable.create { emitter in
let task = URLSession.shared.dataTask(with: URL(string: url)!) { data, _, error in
if let error = error {
emitter.onError(error)
return
}
guard let data = data,
let image = UIImage(data: data) else {
emitter.onNext(nil)
emitter.onCompleted()
return
}
emitter.onNext(image)
emitter.onCompleted()
}
task.resume()
return Disposables.create {
task.cancel()
}
}
}
코드를 살펴보면 알겠지만, 옵저버블 타입으로 반환하고 있는 것을 확인할 수 있다.
이후에 옵저버블을 create하고, 다음에 emitter를 사용하는데 emitter는 실제 데이터를 방출/
전달할 데이터가 있으면 onNext를 통해서 전달하고, 완료되면 onCompleted된다.
그리고 사용이 끝났으면 Disposable 해줘야 함을 잊지말자
Disposable 해야하는 이유는? -> 메모리 누수를 막기 위해서 이거는 쉽게 생각해서 Strong으로 연결되어 있어서 뷰 컨트롤러가 사라져도 reference count가 감소하지 않아서 메모리를 차지하고 있는 현상이 발생하는데, 이를 통해 해결해야 한다. [weak self] 를 사용하는 방법도 존재한다.
여기서 주목할만한 점은, onError가 발생하면 에러 핸들링 후 onCompleted()가 발생하여 종료된다는 점.
✅ Operator - just, from, subscribe
아래 보이는 코드는 옵저버블을 기본적으로 사용하는 코드이다.
그런데, 이것마저 더 간편화할 수 있는 방법은 없을까?
func downloadJson(_ url: String) -> Observable<String?> {
Observable.create { emitter in
emitter.onNext(data)
emitter.onCompleted()
return Disposables.create()
}
}
데이터의 갯수가 하나라면 just를 통해서 처리할 수도 있다.
🟠 just의 경우에는 단일 항목으로 내려보낸다.
func downloadJson(_ url: String) -> Observable<String?> {
return Observable.just(data)
}
데이터가 그러면 여러개라면 from을 통해서 처리할 수 있다.
🟠 from의 경우에는 배열 처럼 여러개의 항복을 내려보낼 수 있다.
func downloadJson(_ url: String) -> Observable<String?> {
return Observable.from([data1, data2])
}
🟠 이번에는 subscribe에 대해서 볼 것인데, 원래는 observable create후 바로 붙여서 사용했지만,
그런데 함수로 분리를 하고 싶다. 그럴 때는 어떻게 사용할까?
_ = downloadJson(MEMBER_LIST_URL)
.subscribe { event in
switch event {
case .next(let json):
// main queue 에서 UI 작업 등
case .completed:
break
case .error:
break
}
}
이렇게 사용할 수 있다. 위에 작성한 함수를 subscribe(구독) 해서 사용하면 된다.
그리고 event가 중요한데, 저 함수에 subscribe를 설정하면, 비동기적으로 event가 발생한다. 이걸 3가지 옵션에 따라 처리할 수 있다.
_ = downloadJson(MEMBER_LIST_URL)
.subscribe(onNext: {print($0)})
✅ Observable 생명주기, 쓰레드 관리
생명주기에 관해서는 위에서도 언급하였지만,
1. Create
2. Subscribe -> 이거 되었을 때 동작한다. (실행된다는 의미)
3. onNext
---- 끝 ---- 옵저버블 재사용 불가하다. 새로운 subscribe 필요
4. onCompleted / onError
5. Disposed
의 사이클을 따른다.
여기서 주목할만한 점은 위에서는 create에 바로 붙여서 사용했으나, 함수로 나누어서 처리할때는, create한다고 사용되지 않는다. 이걸 사용하기 위해서는 subscribe를 해주어야 전달되서 사용 가능하다.
또한 Observable 은 Completed, Error 등으로 끝난 후 재사용할 수 없다.
dispose 등 끝난 후 subscribe 함수를 다시 호출해야만 사용할 수 있다.
Completed or Error 로 끝나도 Disposabled 로 가고, 중간에 취소하더라도 Disposabled 로 간다.
⭐️✅ 순환참조와 메모리 관리
Rx를 사용하게 되면 메모리 누수가 발생할 수 있다. 그런 것을 순환 참조라고 하는데, 한번 알아볼까?
Observable 을 실행한 후, Completed / Error / Disposed 로 끝내게 되면 Observable 이 사라지게 되므로, 참조가 사라져 메모리가 해제된다. 하지만 뷰가 사라졌음에도 모종의 이유로 disposed가 되지 않을 수도 있다. 그렇게 되면 reference count가 감소하지 않아서, 메모리 누수가 발생하게 된다. 그럼 문제가 되겠지?
가장 안전한 방법은 [weak self]를 통해서 메모리 누수를 막는 방법이다.
RxSwift 6.5에서는 아래처럼도 사용 가능.
items.rx
.bind(with: self) { this, item in
this.configure(item)
}.dispseBag()
클로저에 대한 설명은 아래의 사진으로 대체하겠다.
✅ 이번에는 쓰레드 분기에 대해서 알아보자.
사실, DispatchQueue 사용에 능숙해서, 작업에 따라서 쓰레드를 분기하는 작업을 잘 했었는데, rx를 통해서 하는 법도 알아보자.
rx에서 사용하는 스케줄러로는 observeOn, subscibeOn 이렇게 대표적으로 두개가 있다.
🟠 observeOn : 현재 observable 다음의 observable이 실행될 스레드를 바꿔준다.
🟠 subscibeOn : subscribe하는 시점의 스레드를 바꿔준다. 즉, 이 코드는 호출하는 시점과 상관없이, 이 스레드가 사용되는 시점에서 변경한다는 의미라 호출 위치가 중요하지 않다.
위의 사진 하나를 보면 직관적으로 이해 가능
✅ 스트림(stream)의 병합.
옵저버블을 사용하다 보면은 옵저버블을 여러개를 사용하게 되고, 이를 통합해야할 필요가 있을 때가 있다. 우리는 그럼 어떻게 해야할까?
이 강의에서는 대표적으로 자주 사용되는 몇가지를 언급하였다.
아래로 하나씩 보면서 살펴보자
- Merge : 여러개의 독립적인 옵저버블을 하나의 옵저버블에 합쳐서 보여주는 것이다.
❗️데이터 타입이 같아야만 사용할 수 있다.
- Zip : 독립적인 옵저버블을 합쳐주는 것인데, 아래의 그림을 보다시피, 두개의 독립적인 스레드에서의 값이 1:1 매핑되어야 아래로 내려오는 모습을 볼 수 있다.
❗️어떻게 쌍으로 묶을지는 개발자가 지정한다. 그러므로 양 쪽의 Observable 의 데이터 타입이 달라도 된다.
- CombineLatest : 보다시피 아래의 조건에 맞게끔 설정되어 나오는데, 데이터가 1:1 매핑이 아니여도 내려보내주는데, 다른 옵저버블 쪽에서 데이터 정보를 받아다가 그대로 내려준다는 장점이 있다. (가장 많이 사용함)
- disposed(by: disposeBag) : 디스포저블은 밖에다 저장한 변수로 따로 마련해서 viewWillDisAppear할 때, 다운로드 받고 있는 도중에라도 화면이 나가면 다운로드를 취소시킬 수 있어.
배열로 만들 수도 있다. 왜냐면 화면에 디스포즈하는 경우가 많기 때문에 이럴때 디스포져블을 사용하는데 배열이 복잡하니 디스포즈백을 사용한다.
아까 순환참조의 문제를 해결하기 위해 고안된거고, 취소시킬 수도 있다는 점을 기억하자.
✅ 4. Subject - Observable 외부에서 값을 컨트롤 할 수 있을까?
🔸 공식문서
http://reactivex.io/documentation/ko/subject.html
Observable 을 이용해서 UI 를 업데이트하려고 했지만, viewDidLoad() 에서 생성한 Observable 은 onOrder() 함수에서 Observable 컨트롤이 불가능했다.
옵저버블 외부에서 값을 컨트롤하면 얼마나 좋을까? 예를 들어, 사용자가 키오스크를 이용한다고 했을때, 햄버거 추가 버튼을 클릭하면, 그에 맞춰서 값이 변경이 실시간으로 된다면?
그것이 바로 subject!
🟠 subject란? Observable 처럼 subscribe 해서 값을 받을 수 있지만, 외부에서 값을 통제할 수도 있다.
비동기 처리와 관련해서는 분산시스템에서 배운 개념들이 많이 적용되어서 이해하기가 쉬웠다 ㅎ-ㅎ
공식문서에 들어가서 각 옵션에 대해서 살펴보길 바란다.
이 강의에서는 PublishSubject를 이용하였는데,
내가 이해한 바로는 subject는 우리가 UI를 클릭해 변경되는 변수들을 제어할 수 있게 만들어 주는 도구이다.
즉, 변수의 값(프로퍼티)를 변경할 수는 있어도 UI에 보여주려면 또 다른 작업이 필요하게 된다.
테이블 뷰나 컬렉션 뷰 등 reload() 메소드를 통해서 뷰를 재구성 할 수 있지만, UI쪽을 전담해서 도와주는 것도 존재하는데, RxCocoa를 이용해보자!
✅ RxCocoa를 이용해 UI를 컨트롤 해보자.
RxCocoa란? RxCocoa는 uikit에 익스텐션으로 추가한거.
pod에 rxcocoa를 넣어주면 된다.
Subject개념과 연결해서 Rxcocoa를 통해서 외부에서 변경된 내용을 UI에도 적용해보자
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'RxSwift+MVVM' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for RxSwift+MVVM
pod 'RxSwift'
pod 'RxCocoa'
pod 'RxRelay'
pod 'RxOptional'
pod 'RxViewController'
target 'RxSwift+MVVMTests' do
inherit! :search_paths
pod 'RxTest'
pod 'RxBlocking'
end
end
위의 코드는 실습에서 사용한 Podfile 코드이다.
.bind(to:itemCountLabel.rx.text)
.subscribe { self.itemCountLabel.text }
이렇게 축약이 가능한데, 이렇게 사용가능하고 테이블 뷰를 구성하는 셀도 축약이 가능해서 이렇게 되면, dataSource를 연결할 필요가 없어진다.
✅ 2024.08.05 (추가)
- rx 위 코드처럼 셀을 작성하지는 않고 있음.
- 나만 코드를 작성하는게 아니라서 너무 복잡하면 작성하기 힘듦.
- cell은 가급적이면 rx 사용 없이 `configure(위 코드의 inNext)`, `effect(위 코드의 onChanged)`로 클로저를 작성하여 직관적이고 인간 공학적으로 작성
- `TCA`나 `ReactorKit`에서 사용하던 것처럼 잘 읽히도록 작성함.
✅ 추가로 조금 더 알아보자
didSet의 경우 프로퍼티 값이 변경이 완료된 후 실행되는 부분
(참고)
https://www.notion.so/MVVM-RxSwift-da9fa84cd45744d4bea4fcb79269f3a1
https://levenshtein.tistory.com/452
https://www.notion.so/Rxswift-MVVM-20917b6cfb8c4cf592eeeabe62e501a4
https://jinshine.github.io/2019/01/02/RxSwift/2.Observable%EC%9D%B4%EB%9E%80/
'apple > UIKit & ReactiveX' 카테고리의 다른 글
[week4] Filtering Observables (0) | 2021.07.10 |
---|---|
[week3] Subjects (0) | 2021.07.10 |
🐉 RxSwift(Operators) Creating Observables (0) | 2021.07.09 |
[week2] 👀 Observserbles (0) | 2021.07.08 |
[week1] Hello RxSwift 🖐 (0) | 2021.07.07 |