Ch12. 🦕 Beginning RxCocoa
✅ 이번 시간에는 RxCocoa에 대해서 알아볼 예정이야.
RxSwift 공부하면서 적용이 어려웠는데 RxCocoa를 통해 더 잘 사용할 수 있었으면 좋겠다 ㅎㅎ
하,, 이거 작성하다가 중간에 파일 한번 날라갔다;;;;;;;;;;;;;;;;;;;;;;
🟠 커리큘럼은
위의 커리큘럼을 보다보면 문법이 바뀐것이 있어서 error가 종종 나는데, 내 코드를 참고하면 좋다
🟠 예제 코드의 위치
https://github.com/raywenderlich/rxs-materials/tree/editions/4.0
크게 건들인 부분은 없어서 기존 rxswift 개념을 바탕으로 코드 리뷰 형식으로 진행해보자.
✅ 코드 리뷰 및 코드의 이해
1️⃣ ApiController 로부터 데이터를 가져오기 위한 코드를 viewDidLoad 에 작성.
❗️변경된 문법이 있습니다. 커리큘럼의 코드를 그대로 사용하면 에러가 나서 코드를 수정하였습니다.❗️
searchCityName.rx.text // textField와 연결
.filter { ($0 ?? "").count > 0 } // 입력값이 존재하면 넘어가게끔
.flatMapLatest { text in // 가장 마지막에 구독된 스트림의 값만으로 갱신
return ApiController.shared.currentWeather(for: text ?? "Error")
.catchErrorJustReturn(ApiController.Weather.empty) // 에러나면 빈 배열 리턴
}
.observeOn(MainScheduler.instance) // 메인 스케줄러 지정
.subscribe(onNext: { data in // 구독
self.tempLabel.text = "\(data.temperature)℃"
self.iconLabel.text = data.icon
self.humidityLabel.text = "\(data.humidity)%"
self.cityNameLabel.text = data.cityName
})
.disposed(by: disposeBag) // dismiss될 때 구독 날리게끔 + 리소스 낭비 막음
위의 코드를 보면 기존에 rxswift를 공부했어서 쉽게 이해할 수 있었다.
🔸 위 코드의 단점 🔸
- 검색한 결과에 따라서 날씨가 나타나지만, 글자 하나를 작성할 때마다 API가 호출된다.
- subscribe 에서 각 Label 마다 직접적으로 연결해주고 있다.
2️⃣ RxCocoa bind(to:) 를 통한 데이터 바인딩을 적용하자.
// 1 - rxcocoa text field
let search = searchCityName.rx.text
.filter { ($0 ?? "").count > 0 }
.flatMapLatest { text in
return ApiController.shared.currentWeather(for: text ?? "Error")
.catchErrorJustReturn(ApiController.Weather.empty)
}
.share(replay: 1, scope: .whileConnected) // 스트림 하나를 공유하게끔
.observeOn(MainScheduler.instance)
// 2 - rxcocoa binding
search.map { "\($0.temperature)℃" } // $0(data).temperature 의 의미
.bind(to: tempLabel.rx.text) // tempLabel에 rx형식으로 붙이기
.disposed(by: disposeBag)
search.map { "\($0.humidity)%" }
.bind(to: humidityLabel.rx.text)
.disposed(by: disposeBag)
search.map { $0.cityName }
.bind(to: cityNameLabel.rx.text)
.disposed(by: disposeBag)
search.map { $0.icon }
.bind(to: iconLabel.rx.text)
.disposed(by: disposeBag)
위의 코드도 rxswift를 알고 있어서 금방 이해가 되었다.
subscribe 부분을 한 곳에 작성하지 않고 그냥 따로 빼어서 작성하는 것이다.
다만 이렇게 빼게 되면은 여러개의 스트림이 만들어져 낭비가 생기므로 share을 사용해서 스트림을 공유하게 한다.
결과값을 보여주고 싶은 레이블에 바인드하는 과정이라고 생각하면 좋다...!
🔸 특징 🔸
3️⃣ RxCocoa Trait( driver, asDriver )를 통한 코드 개선
우선 들어가기에 앞서...
💡 Trait이란?
- Trait은 UI와 함께 사용되도록 독점적으로 생성된 observable 항목의 특수한 구현을 제공한다. Trait는 직관적이고 작성하기 쉬운 코드를 작성하는데 도움이 되는 Observable의 특수 클래스다. (특히 UI 작업할 때)
- Traits 는 observable sequence 객체가 인터페이스 영역과 소통할 수 있도록 도와준다.
💡 Trait특징
- Trait은 에러를 방출할 수 없다.
- Trait은 메인 스케줄러에서 관찰한다 (observing)
- Trait은 메인 스케줄러에서 구독한다 (subscribing)
-> 즉, Trait을 사용하면 .observeOn(MainSchduler.instance) 호출을 생략 가능하다
- Trait은 부수작용을 공유한다 (side effect)
💡 Trait 프레임워크의 주 요소
- ControlProperty : 데이터와 UI를 연결할때 rx extension을 통해서 사용한 적이 있음
- ControlEvent : 텍스트필드에서 글자를 입력할때 return을 누르는 것과 같이 UI구성요소에서의 확실한 이벤트를 듣기 위해 사용한다. ControlEvent는 구성요소가 UIControlEvents를 현재 상태에 계속 두고 사용할 때 사용 가능하다.
- Driver : 에러를 방출하지 않는 특별한 observable이다. 모든 과정은 UI변경이 background 쓰레드에서 이뤄지는 것을 방지하기 위해 메인 쓰레드에서 이뤄진다.
💡 Trait 마무리하며...
- Trait를 억지로 사용할 필요는 없다. 처음에는 순수히 Subject나 Observable만 쓰는 것도 나쁘지 않다. 하지만 만약 컴파일링 중에 또는 UI와 관련된 어떤 예정된 법칙을 체크하고 싶을 때, Trait은 아주 강력한 기능을 제공하며 시간 절약에도 좋다.
- Trait을 사용하면 .observeOn(MainScheduler.instance) 호출에 대해 잊어버려도 좋다. 또한 background 쓰레드에서 UI를 생성할 필요도 없다.
- Driver와 ControlProperty가 지금은 어려워 보일 수 있다. 천천히 하나씩 확인해보자.
// 1 - rxcocoa text field with Trait
let search = searchCityName.rx.controlEvent(.editingDidEndOnExit).asObservable()
// 유저의 편집이 끝났을 때만 호출
.map { self.searchCityName.text }
.flatMap { text in
return ApiController.shared.currentWeather(for: text ?? "Error")
}
.asDriver(onErrorJustReturn: ApiController.Weather.empty)
// 구독하는거라고 생각 asDriver = observeOn + subscribe
// 2 - rxcocoa binding with Trait
search.map { "\($0.temperature)℃" }
.drive(tempLabel.rx.text) // asDriver에서 bind -> drive로 변경
.disposed(by: disposeBag)
search.map { "\($0.humidity)%" }
.drive(humidityLabel.rx.text)
.disposed(by: disposeBag)
search.map { $0.cityName }
.drive(cityNameLabel.rx.text)
.disposed(by: disposeBag)
search.map { $0.icon }
.drive(iconLabel.rx.text)
.disposed(by: disposeBag)
🔸 코드의 특징 및 설명 🔸
- controlEvent(.editingDidEndOnExit) 메소드를 통해 텍스트필드가 입력이 완료되었을 때만 observable 을 생성한다.
- 입력이 완료된 후 클릭하는 것이기 때문에 입력값은 반드시 유효하다고 생각할 수 있다. (빈칸을 걸러내는 부분을 고려하지 않아도된다.)
- 유저가 Search 버튼을 탭 했을때만 날씨 정보를 받으므로, 불필요한 네트워크 요청이 없기때문에 기존 flatMapLatest 에서 catchErrorJustReturn() 호출을 삭제할 수 있다.
- controlEvent(.editingDidEndOnExit) 메소드를 통해 텍스트필드가 입력이 완료되었을 때만 observable 을 생성한다.
- 입력이 완료된 후 클릭하는 것이기 때문에 입력값은 반드시 유효하다고 생각할 수 있다. (빈칸을 걸러내는 부분을 고려하지 않아도된다.)
- 유저가 Search 버튼을 탭 했을때만 날씨 정보를 받으므로, 불필요한 네트워크 요청이 없기때문에 기존 flatMapLatest 에서 catchErrorJustReturn() 호출을 삭제할 수 있다.
✅ 여기까지 코드리뷰를 통해서 내가 느낀 rxswift 및 rxcocoa
이전에 공부한 적이 있어서 코드가 크게 어렵진 않았다. 다만 프로젝트에 적용해보려고 할때 너무 어려워서 손도 못댔는데, 왜 그랬을지 생각해 보았다. 이유는 바로 네트워크 작업때문이라고 생각한다. 우선 rx를 이용하여 네트워크 작업이 들어간 후에 다른 작업들을 하려다 보니 단순히 텍스트 필드 작업에 간단히 적용되는 rx여도 겁부터 먹게 되었는데, 막상 해보니까 이제는 자신감을 갖고 적용할 수 있겠다는 생각이 든다
✅ RxCocoa 와 dispose 하기 - disposeBag 과 순환참조에 대하여
- 기존 코드에 MainViewController 에 disposeBag 을 두고, 모든 구독의 dispose 를 관리하고 있다.
- 클로저 내부에서 weak, unowned 키워드를 사용하지 않는 이유는?
- 단일 뷰 컨트롤러이고, Main VC 에서는 앱이 구동되는 동안 항상 스클니에 띄워져있기 때문에 메모리 낭비를 고려할 필요가 없기 때문이다.
✅ RxCocoa 의 extension 살펴보기
-> 여기 내용은 다음장에서 더 구체적으로 볼 예정이야
32개의 extension중 2가지만 간단히 봐보자
(참고)
https://jellysong.notion.site/W5-RxCocoa-590170add02443399ec7a8cf19d1701b