[RxSwift] RxSwift란 무엇일까

Updated:

Raywenderlich RxSwift 책을 공부하면서 작성한 글입니다.

Chapter1 Hello, RxSwift

RxSwift란 무엇일까?

RxSwift is a library for composing asynchronous and event-based code 
by using observable sequences and functional style operators, 
allowing for parameterized execution via schedulers.

책의 정의에 따르면 위와 같다고 한다. 이게 무슨 말이냐하면

RxSwift(Reactive eXtension Swift)란 관찰 가능한 연속성(순차적)형태함수형태의 연산자를 사용하여 비동기 및 이벤트 기반의 코드를 구성하기 위한 라이브러리로 스케줄러를 통해 매개변수화 된 실행을 가능하게 한다고 한다.

음.. 무슨말인지 잘 이해가 되지 않는다.

이를 쉽게 말하면 RxSwift 본질적으로 코드가 새로운 데이터에 반응하고 순차적이고 독립적인 방식으로 처리할 수 있게 함으로써 비동기식 프로그래밍을 단순하게 해주는 것이라고 한다.

일단은 비동기식 프로그래밍을 단순하게 하기 위해서 사용한다고 알아두고 넘어가야겠다.

비동기 프로그래밍

iOS 애플리케이션에서 다음과 같은 작업을 할 수 있을 것이다.

  • button 클릭
  • text field가 포커스를 잃었을 때, keyboard가 사라지는 애니메이션
  • 인터넷에서 크기가 큰 이미지 파일을 받는 경우
  • 디스크에 데이터를 저장하는 경우
  • 오디오를 실행하는 경우

위와 같은 작업들은 한 번에 이뤄질 수 있다. 그러나 이때 프로그램은 서로의 실행에 있어서 block 하지 않는다. iOS는 기기의 CPU core를 활용해 여러 thread를 사용하여 다른 실행 contexts를 수행하며, 다양한 API들을 수행할 수 있도록 제공해준다.

그러나 이때 동시에 이러한 작업들을 실행하는 것은 쉽지 않으며, 특히 같은 데이터를 사용하는 경우에는 더욱 어려워진다. 왜냐하면 어떠한 코드가 먼저 데이터를 수정하고, 어떠한 데이터가 나중에 값을 읽어야하는지 확실하게 알기 어렵기 때문이다.

Apple은 Cocoa와 UIKit에서 비동기 프로그래밍을 위해서 여러 APIs들을 제공한다.

  • NotficationCenter
  • Delegate pattern
  • Grand Central Dispatch
  • Closures
  • Combine

동기 코드

var array = [1,2,3]
for number in array {
  print(number)
  array = [4,5,6]
}
print(array)

위와 같은 코드가 있다고 할 때 결과는 어떻게 될까?

위의 동기적인 코드는 두 가지를 보장한다. 동시에 실행된다는 것과, array는 for문을 반복하는 동안 변하지 않는다는 것을 보장하게 된다. 따라서 아래와 같은 결과가 나오게 된다.

1
2
3
[4,5,6]

비동기 코드

var array = [1,2,3]
var currentIndex = 0

@IBAction private func printNext() {
  print(array[currentIndex])
  if currentIndex != array.count - 1 {
    currentIndex += 1
  }
}

위와 같은 코드에서 사용자가 버튼을 눌렀을 때 과연 array의 모든 요소가 출력될 수 있을지는 확실할 수 없다. 다른 비동기 코드에 의해 마지막 요소를 출력하기 전에 삭제가 될 수도 있고, 새로운 요소가 삽입될 수도 있기 때문이다. 또한 여기서는 currentIndex는 printNext() 함수에 의해서만 변경되지만 다른 코드에서도 변경이 이뤄질 수 있기 때문에 문제가 발생할 수 있다.

즉 비동기 코드에서의 핵심 문제는 작업 순서, 그리고 변동가능한 데이터를 공유하는 것이다.

비동기 프로그래밍 용어

RxSwift는 아래와 같은 문제들을 해결하려한다. (1,2,3)

그리고 RxSwift는 아래와 같은 방식으로 문제를 해결하려고 한다. (4,5)

1️⃣ State(shared mutable state)

상태의 예를 들면 다음과 같다.

laptop의 상태 는 메모리에 있는 데이터, 디스크에 저장된 데이터, 사용자 입력에 반응하는 모든 artifact, 클라우드 서비스에서 데이터를 가져온 후 남는 모든 추척 등의 합계라고 할 수 있다.

특히 여러 비동기 구성 요소 간에 공유되는 경우 앱의 상태를 관리하는 것이 바로 RxSwift가 해결할려는 문제 중 하나이다.

2️⃣ Imperative programming

명령적 프로그래밍 은 프로그램 상태를 변경하기 위해 명령문을 사용하는 프로그래밍 패러다임이다. 명령 코드를 사용해 app에 작업을 수행하는 시간과 방법을 정확하게 알려주어야한다.

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  setupUI()
  connectUIControls()
  createDataSource()
  listenForChanges()
}

3️⃣ Side effects

앞선 두 문제에 대해서 부작용이 발생할 수 있다.

부작용코드의 현재 범위를 벗어나는 상태에 대한 변경 사항을 나타낸다. 위의 코드에서 connectUIControls()로 인해서 UI 상태가 변경되므로 부작용이 발생하게 된다. 해당 함수를 실행하기 전과 이후에 다르게 동작하기 때문이다.

디스크에 저장된 데이터를 수정하거나 화면의 레이블 텍스트를 업데이트할 때마다 부작용이 발생하게 된다. 부작용 자체가 문제는 아니다. 부작용을 일으키는 것이 프로그램의 궁금적인 목표라고 할 수 있고, 프로그램이 실행 완료된 후에는 어떻게 해서든 상태가 변경되는 것이 맞다.

발생하는 부작용을 잘 처리하는 것이 중요한 관점이다. 어떠한 코드가 부작용을 일으키는지, 그냥 단순히 프로세스와 data를 산출하는 지를 알아야한다.

이에 RxSwift는 4,5번에서 어떠한 방식으로 이를 해결하는지 나타낼 것이다.

4️⃣ Declarative code

명령형 프로그래밍 에서는 원한다면 상태를 변경할 수 있다. 반면에 함수형 프로그래밍 에서는 부작용을 일으키는 코드를 최소화 하는 것을 목표로한다. 둘 중에 하나만 완벽하게 택하는 것이 아니므로 둘 사이 중간쯤에 있는 것이고, 이때 RxSwift는 명령형 코드와 함수형 코드의 좋은 측면을 결합한 것이다.

명령형 코드 를 사용하여 동작을 정의할 수 있다. 그리고 RxSwift는 관련된 이벤트가 있을 때마다 이러한 동작을 실행하고 사용할 수 있는 불변의 고립된 data를 제공해준다.

굳이 RxSwift를 사용하지 않고 비동기 코드로 작업할 수 있지만, RxSwift를 사용하여 이러한 과정을 단순 루프와 같이 가정하고 사용할 수 있다. 불변 데이터로 작업하고 있고, 코드를 순차적이고 결정론적인 방법으로 실행이 가능해지는 것이다.

5️⃣ Reactive systems

반응형 시스템은 웹 또는 iOS 애플리케이션을 다루게 된다.

  • Responsive(응답): 최신 앱 상태를 나타내는 UI를 항상 최신 상태로 유지하는 것
  • Resilient(복원력): 각 동작은 독립적으로 정의되며, 유연한 오류 복구를 제공한다
  • Elastic(정교한): 코드는 다양한 workload를 처리하며, 종종 lazy pull-driven 데이터 수집, 이벤트 조절이나 자원 공유와 같은 기능을 구현한다
  • Message-driven: 구성 요소는 재사용성 및 독립성 개선을 위해 메시지 기반 통신을 하며, 라이프사이클과 클래스 구현을 분리한다

RxSwift가 어떠한 문제를 해결하려는지, 그리고 이러한 문제를 어떻게 접근하는지 대략적으로 알아 볼 수 있었으니 이제 Rx의 구성요소들과 그것들이 어떻게 함께 작용하는지 알아보자.

Foundation of RxSwift

Rx 코드는 크게 observables, operators, schedulars로 구성되어 있다.

Observables

Observable<Element> 는 Rx 코드의 기초이다.

제네릭 타입인 Element 타입 데이터의 불변 스냅샷을 전달할 수 있는 일련의 이벤트를 비동기적으로 생성한다.

쉽게 말해, consumer가 시간이 지남에 따라서 다른 객체가 방출하는 이벤트 또는 값을 subscribe할 수 있다.


Observable 클래스를 사용하면 하나 이상의 observer가 실시간으로 이벤트에 반응하여 앱의 UI를 업데이트 하거나 그렇지 않으면 새 데이터와 들어오는 데이터를 처리 및 활용할 수 있게 한다. Observable protocol이 준수해야하는 것은 아래와 같다.

  • next event : 최신(또는 “다음”) 데이터 값을 “전달”하는 이벤트이다. observer들이 값을 “수신” 하는 방법이다. Observable은 종료 이벤트가 방출될 때까지 이러한 값을 무한정으로 방출할 수 있다.
  • completed event : 이 이벤트는 success와 함께 이벤트 순서를 종결한다. Observable은 lifecycle을 성공적으로 마쳤고, 더 이상 추가적으로 이벤트를 방출하지 않는다.
  • error event : 에러와 함께 Observable이 종료되고, 더 이상 추가적인 이벤트를 방출하지 않는다.

Observable은 위의 3가지 타입의 이벤트만 방출하거나 수신할 수 있다.

Finite observable sequence

Observable sequence는 0개 이상의 값을 방출하며, 결국 성공적으로 종료되거나 오류와 함께 종료된다.

iOS 애플리케이션에서 인터넷으로부터 파일을 다운로드 받는 코드를 생각해보자.

  • 먼저 다운로드를 시작하고 들어오는 데이터를 관찰하기 시작한다
  • 파일의 일부가 도착할 때 data chunk를 반복적으로 수신한다
  • 만약 네트워크 연결이 중단되면, 다운로드가 중지되고 연결 시간이 초과되며 오류가 발생한다
  • 또는 모든 파일의 데이터를 다운로드하는 경우 성공적으로 완료된다

이러한 동작 흐름은 전형적인 observable의 Lifecycle을 나타낸다.

API.download(file: "http://www...")
   .subscribe(
     onNext: { data in
      // Append data to temporary file
     },
     onError: { error in
       // Display error to user
     },
     onCompleted: {
       // Use downloaded file
})

API.download() 는 Oservable<Data> 객체를 반환하는데 이 객체는 네트워크를 통해서 fetch 된 데이터 일부를 Data 값으로 방출한다. 이때 onNext 클로저를 제공하여 next event를 수신한다. onError 클로저를 제공하여 에러가 발생된 경우 이를 처리해준다. onCompleted 클로저를 제공하여 정상적으로 완료된 경우 핸들링 해준다.

Infinite observable sequence

자연적으로나 강제적으로 종료되는 파일 다운로드와 같은 활동들과는 다르게 무한대로 나오는 sequence들이 있다. UI event가 바로 infinite observable sequences라고 할 수 있다.

예를 들어 기기의 회전 방향에 따른 변화에 반응해야하는 코드를 생각해보자.

  • UIDeviceOrientationDidChange 를 NotificationCenter로 부터 노티를 받기 위해서 observer 클래스를 생성할 수 있다
  • orientation 변환에 따른 callback method를 제공해준다.

이러한 예시는 자연적으로 끝나지 않는다. 기기가 있는 한 방향은 계속 변경될 수 있으며, sequence가 infinite하고 stateful하기 때문에 항상 observing을 시작한 시점부터 초기 값이 있다.

UIDevice.rx.orientation
  .subscribe(onNext: { current in
    switch current {
    case .landscape:
      // Re-arrange UI for landscape
    case .portrait:
      // Re-arrange UI for portrait
} })

Operators

ObservableType과 Observable 클래스의 유형 및 구현에는 비동기 작업 및 이벤트 조작의 개별 부분을 추상화하는 많은 방법들이 포함되어 있고 이러한 방법은 보다 복잡한 논리를 구현하기 위해 함께 구성될 수 있다. 이러한 방법은 매우 분리가능하고, 구성 가능하기 때문에 일반적으로 “연산자”라고 한다.

이러한 연산자들은 비동기식 입력을 받아 부작용을 일으키지 않고 출력만 내기 때문에 퍼즐 조각처럼 쉽게 맞출 수 있고 더 큰 그림을 그릴 수 있다.

Rx 연산자는 Observable가 방출하는 이벤트에 적용하여 식이 최종 값으로 결정될 때까지 입력 및 출력을 결정적으로 처리하여 부작용을 일으키는데 사용할 수 있다. 위의 방향 예시에서 연산자를 다음과 같이 사용할 수 있다.

UIDevice.rx.orientation
  .filter { $0 != .landscape }
  .map { _ in "Portrait is the best!" }
  .subscribe(onNext: { string in
    showAlert(text: string)
  })

모든 UIDevice.rx.orientation은 .landschape 혹은 .portrait 값을 재생하게되는데, RxSwift에서 방출되는 데이터를 filter와 map을 사용할 수 있다.

먼저 filter.landscape 가 아닌 값들만 통과를 시켜준다. 만약 기기가 .landscape 모드인 경우에는 subscription code는 동작하지 않게 된다. .portrait 인 경우에는 map 연산자에 의해서 String으로 값을 방출해준다. 그리고 마지막 subscribe가 nextEvent 의 결과로서 String 값을 받아서 이를 화면에 표시해주게 된다.

Operator는 매우 composable하며, data를 input으로 받아들여 결과로 output을 내기 때문에 chaining하기 쉽다.

Schedulars

스케줄러전송 대기열 또는 작업 대기열에 해당하는 Rx이다. 특정 작업의 실행 context를 정의할 수 있다.

RxSwift는 사전에 정의된 스케줄러와 함께 제공이 되는데, 99% 케이스를 커버가능하기 때문에 거의 커스텀한 스케줄러를 만들 필요가 없다.

스케줄러는 매우 강력하며, 예를 들어 GCD를 사용하여 지정된 대기열에서 코드를 연속적으로 실행하는 SerialDispatchQueueScheduler에서 다음 이벤트를 관찰하도록 지정할 수 있다. ConcurrentDispatchQueueSchedular는 코드를 동시에 실행하는 반면, OperationQueueScheduler는 지정된 OperationQueue에서 subscribe 일정을 지정할 수 있게 해준다.

RxSwift 덕분에 다양한 스케줄러에서 동일한 구독의 여러 작업을 예약하여 용도에 맞는 최상의 성능을 달성할 수 있다. RxSwift는 subscriptions(아래 그림에서 왼쪽)과 schedulers(아래 그림에서 오른쪽) 사이에서 dispatcher처럼 행동하며, 작업들을 올바른 context로 보내고 서로의 출력이 원할하게 작동하도록 할 것이다.

App architecture

RxSwift는 App에서 아키텍처를 어떠한 식으로도 바꾸지 않는다. RxSwift는 대부분 event, 비동기 데이터 sequence, 통신을 다룬다. 따라서 프로젝트를 처음부터 다시 할 필요는 전혀 없다.

MVC, MVP, MVVM 어떠한 패턴을 사용해도 상관없지만 MVVM과 특히 잘 어울린다. 왜냐하면 ViewModel을 사용하면 view controller의 코드에서 UIKit 컨트롤에 직접 바인딩할 수 있는 Observable properties를 노출할 수 있기 때문이다.

RxCocoa

RxSwift는 일반적인 플랫폼에 Rx 규격을 구현한 것이다. 따라서 Cocoa나 UIKit에 특화된 class들에서는 아무것도 모를 수 밖에 없다.

RxCocoa 는 특별히 UIKit과 Cocoa를 위한 개발에 도움을 주는 모든 class를 보유하고 있는 RxSwift와 같이 사용되는 library이다. 일부 고급 class를 제공하는 것 외에도 RxCocoa는 즉시 다양한 UI event를 subscribe할 수 있도록 많은 UI 구성 요소에 reactive extension을 추가한다. 예를 들어 RxCocoa를 사용하여 다음과 같이 UISwitch의 상태를 변경할 수 있다.

toggleSwitch.rx.isOn
  .subscribe(onNext: { isOn in
    print(isOn ? "It's ON" : "It's OFF")
  })

RxCocoa 는 UISwitch 클래스에 rx.isOn 프로퍼티를 추가하여 event를 subscribe할 수 있는 sequence로 만들어 준다. 게다가 RxCocoa 는 Rx namespace를 UITextField, URLSession, UIViewController 등에 추가하고 이 namespace의 reactive extension을 정의할 수 있게 해준다.

RxSwift와 Combine

RxSwift와 Combine은 많은 공통 언어와 유사한 개념을 공유한다.

RxSwift 는 서버 사이드 Swift에 적합한 리눅스에서도 작동하는 다중 플랫폼 크로스 언어 표준으로 인해 자체적으로 독창적인 개념, operator 이름 및 type의 다양성을 갖추고 있는 더 오래되고 잘 확립된 프레임 워크이다. 오픈소스이기 때문에 코어에 기여하고, 특정부분이 어떻게 작동하는지 정확히 알 수 있다. 또한 iOS 8까지 Swift를 지원하는 모든 apple 플랫폼 버전과 호환된다.

Combine 은 유사한 개념을 다루지만 특별히 Swift와 Apple 자체 플랫폼에 맞춰진 Apple에서 만든 새로운 프레임워크이다. Swift 표준 라이브러리와 많은 공통 언어를 공유하기 때문에 API는 매우 친숙하게 느껴진다. 그러나 iOS 13, macOS 10.15 이상에서만 지원하고 오픈소스가 아니며, 리눅스를 지원하지 않는다.

RxSwift와 Combine은 매우 유사하기 때문에 Combine, RxSwift 지식은 서로에게 전달될 수 있다. 또한 RxSwift의 Observables와 Combine의 Publishers를 혼합하거나 일치시킬 수 있다.

참고

  1. raywenderlich chapter 1

Categories:

Updated:

Leave a comment