본문 바로가기

Swift/RxSwift

[🌱SeSAC] Rx복습, RxAlamofire/RxDataSource

728x90

Rx .. 무시무시 어마무시 .. !!

 

MVVM Pattern 

MVVM 패턴의 가장 큰 핵심은 양방향 데이터 바인딩이다. 

 

양뱡향 데이터 바인딩을 위해서 아래와 같은 Observable 클래스를 만들었다.

import Foundation

class CObservable<T> {
    private var listener: ((T) -> Void)?
    
    var value: T {
        didSet {
            listener?(value)
        }
    }
    
    init(_ value: T) {
        self.value = value
    }
    
    func bind(_ closure: @escaping (T) -> Void) {
        closure(value)
        listener = closure
    }
}

 

Q : 왜 이런 클래스를 만들었을까? 

A : 위에서 말했지만, 단방향 데이터 흐름을 양방향으로 만들기 위해서이다.

 

Swift에서 데이터는 단방향으로 흘러간다.

그리고 MVC 패턴에서는 View와 Controller가 붙어 있기 때문에 양방향으로 관리할 필요가 없었다.

 

그러나 MVVM 패턴에서는 ViewModel이 등장하게 된다.

ViewModel은 Model과 View(= Controller) 사이에 위치하여 View(= Controller)에서 변경이 일어나게 되면, 이것을 뷰모델에 전달하여 로직을 처리하고 뷰모델에서 처리된 모델의 변화를 뷰에게 알려서 앱의 흐름을 관리한다.

그렇기 때문에 데이터는 단방향이 아니라, 양방향으로 흘러야 한다.

---> CObservable을 통해 bind를 사용하여 (Rx없이) 양방향으로 데이터의 흐름을 관리했다.

 

(Rx에서는 양방향 데이터 흐름을 도와주는 Operator가 존재한다.)

 


Rx

Rx는 반응형으로 관리한다.

반응형이라는 것에서 알 수 있듯, 어떠한 이벤트를 방출하는 것이 있다면 이를 감지하는 것이 있어서 실시간으로 데이터가 왔다 갔다 하게 되는 것이다.

 

Observable 

이벤트를 방출한다.

사용자가 어떠한 버튼을 누르거나, 앱에 진입했을 때 서버 통신 등의 작업을 요구하는 등의 이벤트가 여기에 해당한다.

 

Observer

위에서 방출한 이벤트를 처리한다.

 

그리고 여기서 이벤트는 크게 세가지로 나눌 수 있다.

  • Next
  • Error
  • Complete

방출된 이벤트에 대해서 처리를 하기 위해서는 구독이 되어 있어야 한다.

 

 

Subject, Relay

Observable은 이벤트 방출만 할 수 있고,

Observer는 전달 받은 이벤트에 대해서 처리만 할 수 있다.

 

그런데, 때에 따라서 관찰하는 객체와 관찰을 받는 객체를 한번에 처리할 필요성이 있다. 

그래서 등장한 것이 Subject이다.

그리고 이 중에서 더 UI에 특화된 형태로 발전한 것이 Relay라고 할 수 있다.

 

 


저번주에 진행한 프로젝트의 코드를 보면서 복습을 해보자.

 

버튼을 탭해서 Label의 Text를 수정한다고 할 때, 아래와 같이 코드를 작성할 수 있다.

#1 

// case 1.
button.rx.tap
    .subscribe { [weak self] _ in
        self?.label.text = "안녕 반가워 "
    }
    .disposed(by: disposeBag)

 

 

 

#2 

 

// case 2.
button.rx.tap
    .withUnretained(self)
    .subscribe { vc, _ in
        vc.label.text = "안녕 반가워 "
    }
    .disposed(by: disposeBag)

1의 코드에서 [weak self]를 Rx에서 제공하는 operator를 통해 위와 같이 개선할 수 있다.

 

 

#3

 // case 3. 
 // UI 업데이트는 메인 쓰레드에서 동작되어야 한다. 
 // 네트워크 통신이나 파일 다운로드 등의 백그라운드 작업도 같이 진행될 수 있다. 
 button.rx.tap
     .map {}
     .map {}
     .map {}
     .observe(on: MainScheduler.instance) // 메인 쓰레드로 동작하게 변경
     .map {}
     .map {}
     .map {}
     .withUnretained(self)
     .subscribe { vc, _ in
         vc.label.text = "안녕 반가워 "
     }
     .disposed(by: disposeBag)

이번에는 단순히 탭해서 Label의 Text를 바꾸는 것이지만, 실제로 서비스에서는 버튼을 탭해서 서버 통신을 하고 그 결과를 UI에 반영하게 될 수도 있다.

이 때는 서버 통신은 global 큐에서 동작하게 되고, UI 업데이트는 main 쓰레드에서 동작해야 하므로 위와 같이 MainSchedular.instance로 변경해야 한다. 

 

 

#4

// case 4. bind
// subscribe, mainScheduler, error X
// UI 동작의 경우 사용이 적합
button.rx.tap
    .withUnretained(self)
    .bind { vc, _ in
        vc.label.text = "안녕 반가워 "
    }
    .disposed(by: disposeBag)

이 때, 메인 쓰레드에서 동작할 수 있도록 보장하는 구독의 형태가 바로 bind이다.

그렇기 때문에 bind는 UI 동작에서의 사용이 적합하다. 

 

 

#5

// case 5.
button.rx.tap
    .map { "안녕 반가워" }
    .bind(to: label.rx.text, label2.rx.text)
    .disposed(by: disposeBag)

bind는 여러 개로 연결이 가능하다.

그러므로 버튼을 탭했을 때, ---> ControlEvent Type

.map { }을 통해 String으로 데이터를 변경하고 ---> String

이 데이터를 여러 개의 Label에 적용할 수 있다. 

 

 

#6

// case 6.
// driver traits: bind + stream 공유 (리소스 낭비 방지)
// 어떻게 가능? driver 객체가 share를 포함/내부에 구현되어 있다. 
button.rx.tap
    .map { "안녕 반가워" }
    .asDriver(onErrorJustReturn: "")
    .drive(label.rx.text, label2.rx.text)
    .disposed(by: disposeBag)

이 때, bind는 stream을 공유하지 않는다.

보다 UI에 적합한 형태로 stream을 공유하도록 하는 driver를 사용해서 리소스 낭비를 방지할 수 있다.

 

 

.debug()

operator의 한 종류로 일종의 print() 역할을 한다.

데이터의 흐름이 변하게 될 때, 중간에 debug()를 사용하게 되면 그 흐름이 어떻게 변하고 있는지를 콘솔창에서 확인할 수 있다.

 

개발자가 개발을 하면서 확인 용도로 사용하는 것이므로,

나중에 Main 브랜치에 합치거나 출시에 올리게 될 때에는 삭제하는 것이 좋다.


RxAlamofire

Rx에서 Alamofire를 wrapping하여 제공하는 네트워크 라이브러리이다.

 

Unsplash API를 사용해서 apple에 해당하는 결과값을 반환한다고 할 때,

RxAlamofire를 통해서 코드를 작성하면 아래와 같다.

let url = APIKey.searchURL + "apple"

request(.get, url, headers: ["Authorization" : APIKey.authorization])
    .data()
    .decode(type: SearchPhoto.self, decoder: JSONDecoder())
    .subscribe(onNext: { value in
        print(value.results[0].likes)
    })
    .disposed(by: disposeBag)

 

request를 통해 메서드의 파라미터로 전달한 속성에 맞는 서버 통신을 진행한다.

응답 결과를 data의 형태로 받아서 이를 SearchPhoto로 decode한다. 

 

그리고 이 이벤트에 대한 결과를 subscribe하여, 제대로 통신이 이뤄졌다면

onNext로 데이터가 전달 될 것이다.

만약, 서버 통신에서 문제가 발생하게 된다면, onError로 이벤트가 방출될 것이고 그 코드 블럭 내에서 에러에 대한 핸들링을 하면 된다.

 

 

RxDataSource

DiffableDataSource와 비슷한 흐름으로 코드가 구현된다.

 

lazy var dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Int>>(configureCell: { dataSource, tableView, indexPath, item in
    
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
    cell.textLabel?.text = "\(item)"
    return cell
})

위와 같이 dataSource를 전역적으로 선언한 다음에

 

private func testRxDataSource() {
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
    
    dataSource.titleForHeaderInSection = { dataSource, index in
        return dataSource.sectionModels[index].model
    }
    
    Observable.just([
        SectionModel(model: "title", items: [1, 2, 3]),
        SectionModel(model: "title", items: [1, 2, 3]),
        SectionModel(model: "title", items: [1, 2, 3])
    ])
        .bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)
}

이렇게 함수를 작성한 다음, 코드를 실행하면 

섹션이 3개인, 각 섹션의 타이틀이 title이고 섹션 안의 요소가 1, 2, 3인 테이블 뷰가 만들어진다.

 

🔥 여기서 주의할 점은 

DataSource를 모두 등록한 다음에 bind를 진행해야 한다.