[RxSwift][Operator] Scan
27 Apr 2020
요 며칠 RxSwift로 빡시게? 개발중인 프로젝트가 하나 있는데요. 이리저리 웹서핑을 하다가 Scan operator에 대해서 좀 더 깊게 알아봐야 할 필요성이 느껴졌습니다. 단순히 Swift의 Reduce와 비슷한? 정도로만 생각 하기에는 다른 매력적인 부분이 있습니다.
Scan
개발은 거의 99% 영어로 이루어집니다. 한국말을 모국어로 사용하는 사람으로서, Scan을 딱 들었을 때 이게 어떤 일을 하는지 바로 감이 오기란 쉽지 않습니다. 예전에 Rx의 driver 를 포스팅할 때가 생각 나는군요. 왜 Rx는 UI작업을 하는 Observable을 Driver로, Subscribe를 Drive로 표현했는지 말이죠. 다시 설명드리지만 우리 예전 데스크탑 PC를 사용할 때, HW 인 어떤 Drive를 사용하기 위해서는 Driver가 반드시 필요했습니다. 같은 의미로 Event를 받아 작동시키고자 하는 UI가 HW Drive라면, 이를 작동시키기 위한 Driver는 어떤 이 UI에 공급되는 event이겠지요 (Driver) 따라서 UI를 위해 event를 emit하는 Observable을 Driver로 불렀던 것 입니다.
개발을 할 때는 그 함수 혹은 그 operator가 왜 그렇게 명명되었는지를 이해하는 것이 그 동작을 이해하는 데 있어 가장 중요한 부분입니다. Scan은 우리 아는 그 Scan을 떠올리시면 됩니다. 만약 문서 A를 스캐너 유리판 위에 올린 다음 scan을 하면 A라는 문서가 저장된 파일이 나옵니다. Operator로서 Scan은 input A가 parameter인 accumulator를 거쳐 B로 출력되게끔 합니다. Reduce와 비슷하지만 차이점은 Reduce는 결과만을 출력한다면, Scan은 각 element들이 accumulator에 의해 변형되는 각각의 결과를 모두 출력한다는데 그 차이가 있습니다.
Shallow Dive
아래 소스를 보시죠
Observable.of(1,2,3,4,5).scan(0, accumulator: +).subscribe(onNext: { (value) in
print(value)
}).disposed(by: self.disposeBag)
Element를 하나씩 꺼내서 각 element를 가장 최근에 배출된 element와 더하여 그 결과를 출력하게 됩니다. 처음에는 가장 최근에 배출된 값이 없으니 초기값 0 + 1로 1이 출력이 되겠네요. 그 다음으 1 + 2로 3, 3+3으로 6… 결국 10+5 로 15를 출력하고 dispose되게 됩니다.
1
3
6
10
15
만약 Reduce로 위와 같은 소스를 짠다면 결과는 아래와 같이 나오겠죠?
Observable.of(1,2,3,4,5).reduce(0, accumulator: +).subscribe(onNext: { (value) in
if self.lbScanOpResult2 != nil {
self.lbScanOpResult2.text = String(format: "Result %d", value)
}
}).disposed(by: self.disposeBag)
15
Deep Dive
Scan을 좀 더 유용하게 사용할 수는 없을까요? 아래 소스를 보시죠.
@IBOutlet weak var btnScanOp: UIButton! {
didSet {
btnScanOp.rx.tap.scan(false) { lastValue, newValue in
return !lastValue
}.subscribe(onNext: { (value) in
print(value)
self.lbScanOpResult.text = String(format: "Result %d", value)
}).disposed(by: self.disposeBag)
}
}
참고로 btnScanOp가 emit하는 결과는 항상 void 입니다. 하지만 이 Void 결과가 Scan을 만나면 아래와 같은 변형이 일어나게 됩니다.
Tap -> Void -> flase
Tap을 했습니다. 결과는 Void가 오죠, 위에서 newValue는 Void 이지만, lastValue는 초기값이므로 false 입니다. 결과로 !false 즉 true가 return 됩니다. 다음 tap을 하면 이제는 newValue는 역시… Void 입니다. 하지만 lastValue는 true이고 다시 !true인 false를 리턴하게 됩니다.
결과가 Void인 tap의 gesture를 유의미한 정보로 바꾸는데 Scan이 한 몫하는 것을 볼 수 있습니다. 직전의 값을 가지고 있는 Scan을 활용하여 tap을 눌렀을 때 true, false를 번갈아 가면서 출력할 수 있게된 것 입니다. 그렇다면 아래와 같은 적용도 가능해 보입니다.
btnScanOp3.rx.tap.asDriver().scan(false) { lastValue, newValue in
return !lastValue
}.drive(self.lbScanOpResult3.rx.isHidden).disposed(by: self.disposeBag)
위의 예제는 scan의 결과를 가지고 특정 뷰의 hide 유무를 결정하도록 만든 예제 입니다.
Conclusion
사실 Scan operator를 어떤 방법으로 쓸지 좀 막막했습니다. 단순히 위의 marble diagram을 보면 각각의 observable을 더하는 용도로만 사용하나 싶다가도 금새 그럴리 없어라는 결론을 얻게 되죠. 지금은 btn이 Void결과값만을 emit하여 newValue가 무의미하지만 newValue가 존재하는 observable을 이용시에는 다양한 적용이 가능합니다.