What is Reactive Programming?

Reactive Programming에 대해 The introduction to Reactive Programming you’ve been missing를 번역하는 식으로 공부해 보았다.

Reactive Programming이란?

Reactive Programming은 비동기적 데이터 흐름을 다루는 프로그래밍이다.

이러한 방식은 전혀 새로운 것이 아니다. 우리는 이미 많은 비동기적 흐름(Stream)을 가진 이벤트들을 발견(Observe)하고 그것에 따른 처리를 한다. 흐름들은 비용이 크게 들지 않고 어디에나 존재하며 변수, 유저의 입력, 프로퍼티, 캐쉬, 그리고 자료구조 등 어떠한 것도 이벤트의 흐름이 될 수 있다. 당신은 이 흐름을 듣고 이에 따라 대응할 수 있다.

그 중에서도 이러한 스트림을 결합, 생성 및 필터링하는 놀라운 기능의 도구 상자가 제공된다.

여기서 functional이라는 마법이 발휘된다. 한 흐름은 다른 흐름의 입력 값이 될 수 있고 심지어 여러 흐름이 입력 값으로서 역할을 할 수 있다. 두 흐름을 합칠 수 있고 당신이 걸러내고 싶은 흐름만 뽑을 수도 있다.

흐름은 Reactive에서 매우 중요한 개념이기 때문에 자세히 살펴보자. 다음 그림은 익숙한 “버튼 클릭” 이벤트이다.

한 흐름은 시간순으로 정렬되어 진행되는 이벤트의 배열이다. 그리고 이 흐름은 , 에러, 또는 완료 신호(이벤트)발행(emit)한다.

우리는 이렇게 내보내진 이벤트들을 비동기적으로 감지할 것이며 이는 각각의 이벤트가 발행될 때 실행되는 함수를 정의함으로써 이루어진다. 흐름을 “듣는” 행위는 구독(Subscribing)이라고 불린다. 우리가 정의한 함수들은 옵저버(Observer)들이다. 그리고 그 흐름은 Subject 또는 Observable로 불리며 감지가 가능하다. 이것이 명확한 Observer Design Pattern이다.

위 그림을 ASCII로 작성해보았다. 우리는 튜토리얼에서 이러한 방식을 사용할 것이다.

1
2
3
4
5
6
--a---b-c---d---X---|->

a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline

위의 그림과 같기 때문에 지루하다. 새로운 것을 해보자: 우리는 원래 클릭 이벤트 흐름에서 나오는 새로운 클릭 이벤트의 흐름들을 만들 것이다.

먼저, 버튼이 클릭되는 횟수를 가리키는 counter 흐름을 만들어 보자. 일반적인 Reactive 라이브러리에서 각 흐름은 map, filter, scan 같은 함수들을 제공한다. 만약 당신이 이러한 함수를 호출했다고 하자.

1
clickStream.map(f)

이 함수는 클릭 흐름을 기반으로 새로운 흐름을 반환하며 본래의 흐름을 수정하지 않는다. 이 특성이 불변성(Immutability)라 불리며 이것은 마치 팬케익과 시럽처럼 Reactive 흐름들과 함께 간다. 이는 우리가 아래와 같이 연쇄적 함수(chain function)을 가능하게 해준다.

1
clickStream.map(f).scan(g)

다시 흐름을 보면 이런 형식이 나오게 된다.

1
2
3
4
5
  clickStream: ---c----c--c----c------c-->
vvvvv map(c becomes 1) vvvv
---1----1--1----1------1-->
vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->

map(f) 함수는 f 함수에 따라 각각의 발행된 값(이벤트)을 새로운 흐름으로 대체한다. 우리의 경우 c를 1로 대체한다. scan(g) 함수는 이전의 값들을 모두 결합하는 역할을 하며 여기서는 click의 counter 기능을 한다. 그래서 counterStream은 그 전까지의 모든 클릭의 횟수를 발행한다.

Reactive의 진정한 힘을 보이기 위해 우리가 “double click” 이벤트들의 흐름을 원한다고 해보자. 더 재밌게 하기 위해 multiple clicks(3 이상)을 double click으로 처리하고 싶다고 가정하자. 원래의 방식인 명령적(imperative)이고 stateful한 방식으로 한다면 아마도 당신은 더럽고 상태를 보관하는 변수들과 시간 간격을 다루는 시시한 것들을 보게될 것이다.

그러나, Reactive하게 한다면 매우 쉽다. 사실, 4 라인의 코드밖에 들지 않는다. 그러나 지금은 코드는 무시하고 흐름을 표현하는 다이어그램 이미지를 보자.

회색 박스 안의 함수들은 한 흐름을 다른 흐름으로 변환하는 역할을 한다. 먼저 250ms 안의 클릭들을 buffer(stream.throttle(250ms)) 함수를 통해 모은다. 지금은 이 함수가 무엇을 뜻하는 지 상관하지 말고 Reactive에 집중하자. 그 결과로 lists들의 흐름인 두번째 그림이며 이번에는 map() 함수를 통해 각각의 list의 길이를 구하는 함수를 적용해 새 흐름을 만들자. 마지막으로 filter(x ≥ 2) 함수를 통해 2개 이상인 이벤트만 남겨 놓을 수 있다. 이제 이벤트를 subscribe(“listen”)하여 우리가 원하는 대로 반응(react)할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//
// ViewController.swift
// RxStudy
//
// Created by Alpaca on 2018. 2. 28..
// Copyright © 2018년 Alpaca. All rights reserved.
//
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {

@IBOutlet weak var myButton: UIButton!

let disposeBag = DisposeBag()

override func viewDidLoad() {
super.viewDidLoad()

myButton.rx.tap
.buffer(timeSpan: 1, count: 3, scheduler: MainScheduler.instance)
.map { $0.count }
.filter { $0 >= 2 }
.subscribe { [weak self] in
guard let touchCount = $0.element else { return }
self?.myButtonTapped(touchCount: touchCount)
}.disposed(by: disposeBag)
}

func myButtonTapped(touchCount: Int) {
print("touchCount: \(touchCount)")
}
}

제가 RxSwift 라이브러리를 사용해 만든 예입니다. timeSpan은 변형했고, buffer로 충분히 묶을 수 있는 것 같아 throttle은 사용하지 않았습니다. 피드백 부탁드립니다.

위의 튜토리얼은 Reactive에 대한 빙산의 일각일 뿐이며 다양한 종류의 흐름들에 적용할 수 있는 function들이 많다.

왜 Reactive Programming의 도입을 고려해야 하는가?

Reactive Programming은 당신의 코드의 추상화 수준을 높여주어 하찮지만 많은 구현 디테일을 신경쓰는 대신 비즈니스 로직을 정의하는 이벤트들의 상호 의존성을 높이는 데 집중하게 한다. Reactive Programming의 코드는 더 명확하다.

데이터 이벤트와 연관된 다양하고 많은 UI 이벤트가 발생하는 현대의 웹 앱이나 모바일 앱 같은 경우 이점은 보다 명확해진다. 앱은 유저에게 높은 반응성의 경험을 가능하게 하는 실시간 이벤트들을 가지고 있다. 우리는 이것들을 제대로 다룰 도구가 필요하며 Reactive Programming이 그 답이 될 것이다.


알파카의 추가 공부

이 글로는 왜 Reactive Programming이라고 불리는 지 잘 이해가 되지 않아 더 조사해 본 것으로는 다음과 같다.

Reactive Programming은 사용자 입장에서 실시간 반응이 이루진다고 할 수 있다. 예를 들어 어떤 단어를 넣어 검색을 할 때 그에 따라 검색어 제안이 나타나는 것을 말한다. 이것은 기존의 Observer 패턴을 통해 감지하고 그에 따른 Asynchronous한 처리를 하면 가능하다. 그러나 그에 따른 코드량의 증가와 분산이 존재한다.

여기서 Functional Programming의 이점이 도입된다. 위 글의 저자가 놀라운 함수 모음함을 말한 이유가 이것으로 추측된다. 함수형 프로그래밍은 상태를 저장하지 않고 순수 함수를 사용해 로직을 구성한다. 여기에서 생기는 이점은 상태를 변경하여 결과가 달라지는 Side-Effect를 최소화 할 수 있다는 것이다.

Swift는 함수형 패러다임을 지원하는 언어기 때문에 고차함수를 이미 제공한다. 그런데 Reactive Programming에서 흐름(Observable / Stream)을 Sequence의 형태로 다루고 있기 때문에 방출되는 비동기적 이벤트를 마치 iterable 컬렉션처럼 다룰 수 있다. map, reduce, filter 등 기본적으로 제공하는 고차함수로 방출된 이벤트들을 우리가 원하는 대로 변형시키고, 묶고, 걸러내어 사용하는 것이다. 또한 수많은 Rx 라이브러리가 제공하는 많은 Operator들을 자유자재로 다룰 줄 안다면 자유자재로 흐름을 제어할 수 있다.

Reference