본문 바로가기
iOS/RxSwift

Ch2. Observable

by 헤콩 2023. 2. 24.
반응형

본 게시물은 Florent Pillet, Junior Bontognali, Marin Todorov, Scott Gardner - RxSwift.  Reactive Programming with Swift (2017, Razeware LLC) 책을 기반으로 작성되었습니다.

RxSwift를 공부하는 데에 이 책을 읽으면 좋겠다고 생각하였고, 단지 읽기만 하는 것보다 한국어로 직접 정리해놓으면 더 기억하기 좋을 것 같아 게시물을 작성하게 되었습니다.

영어 원서로 된 책을 제가 읽히는대로 의역 및 정리하였기 때문에 잘못된 해석이 있을 수 있습니다. 잘못된 해석은 댓글로 알려주시면 바로 수정하겠습니다 :)

 


What is an observable?

Observable은 Rx의 심장이라고 할 수 있습니다.

우리는 앞으로 Observable이 무엇인지, 어떻게 만드는지, 그리고 어떻게 그것을 다루는지에 대해 알아볼 것입니다.

 

우리는 Rx에서 Observable, Observable Sequence, 그리고 Sequence가 같은 의미에서 사용되는 것을 볼 수 있습니다. 그리고 가끔 Stream이라는 것도 볼 수 있습니다. 가끔 RxSwift를 사용하는 다른 개발자들이 Stream을 사용하는 것을 볼 수 있는데, 보통은 Sequence라고 부릅니다.

 

In RxSwift, everything is a Sequence or something that works with a sequence.

 

RxSwift에서는 모든 것이 Sequence이거나 Sequence와 함께 동작하는 것입니다. 그리고 Observable은 특별한 힘을 가진 Sequence일 뿐이지만, 가장 중요한 사실은 비동기이라는 것입니다.

 

Observable은 일정 시간 동안 이벤트를 생성하는데, 그 과정을 방출(emitting) 이라고 합니다. 이러한 이벤트들은 number이나 instances of a custom type과 같은 값을 가지거나, 탭과 같은 인식된 제스쳐를 가질 수 있습니다.

이것들을 개념화시키는데 가장 좋은 방법은 marble diagram을 사용하는 것입니다.

 

위에서 보이는 ➡️ 화살표는 시간을 나타내고, 숫자가 매겨진 원들은 elements of a sequence를 나타냅니다.

여기서 Element 1이 활성화? 방출?되고 시간이 지나면 2와 3이 활성화? 방출? 됩니다.

이는 Observable의 전반적인 라이프사이클에서 어느 시점에나 발생할 수 있습니다.

* 그냥 영어 그대로 받아들이며 해석하는 건 쉬웠는데, 막상 한국어 단어로 번역 하려니 emitted라는 말을 문맥 흐름상 활성화라고 해야할지 방출이라고 해야할지 발생시킨다고 해야할지 반환시킨다고 해야할지 적당한 한국어 단어를 고르지 못했습니다ㅠㅠ

 

 

 

Lifecycle of an observable

위에서 보여준 마블 다이어그램에서, Observable은 3개의 element들을 방출했습니다. Observable이 element를 방출할 때, 그것은 next 이벤트를 발생시킵니다.

 

아래에 또 다른 마블 다이어그램이 있습니다. 이번에는 화살표 끝에 위아래로 수직인 선이 있는데, 이것은 이 observable의 끝을 의미합니다.

 

이 observable은 3개의 탭 이벤트를 방출하고 나서 끝냅니다. 이것을 우리는 completed 이벤트라고 합니다.

예를 들어, 저 3개의 탭 이벤트들이 발생하고 나서 이 탭 이벤트가 발생한 view가 화면에서 사라졌다고 가정해봅시다. 그럼 여기서 중요한 것은, 탭 이벤트를 발생시키는 view가 화면에서 사라졌기 때문에 observable이 멈추고 더이상 아무것도 방출시키지 않는다는 것입니다.

보통 이러한 상황이 표준적인 종료라고 할 수 있지만, 가끔 문제가 생겨 잘못된 종료가 발생할 수 있습니다.

 

이 마블 다이어그램에서는 error가 발생했습니다. 이 error는 여기서 빨간색 X로 표시되었습니다. Observable은 error를 포함시킨 error 이벤트를 발생시킵니다. 평상시에 발생하는 종료이벤트인 completed 이벤트와 크게 다를 바가 없이, error 이벤트가 발생하면 종료시키는 것과 동시에 더이상 어떠한 것도 방출시키지 않습니다.

 

 

그럼 요약을 해볼까요?

  • Observable은 element를 포함한 next 이벤트를 방출합니다.
  • 이것은 error 이벤트가 발생하며 종료되거나 completed 이벤트가 발생하며 종료될 때까지 계속됩니다.
  • 한번 Observable이 종료하면, 이것은 더이상 이벤트를 방출시키지 않습니다.

 

RxSwift 소스 코드에서 직접 예를 들어보면, 이러한 이벤트들은 열거형 케이스로 표시됩니다.

public enum Event<Element> {
    case next(Element)
    case error(Swift.Error)
    case completed
}

 

여기서 우리는 .next 이벤트 instance of some Element를 포함하고, .error 이벤트 Swift.Error 인스턴스를 포함하고, .completed 이벤트 어떠한 데이터도 포함하지 않고 단지 종료시키는 이벤트임을 알 수 있습니다.

이제 우리는 Observable이 무엇인지, 무엇을 하는지 이해할 수 있게 되었습니다. 그리고 우리는 Observable을 생성하여 실제로 어떻게 작동하는지 알아볼 것입니다.

 

 

 

Creating observables

example(of: "just, of, from") {
    // 1
    let one = 1
    let two = 2
    let three = 3
    
    // 2
    let observable: Observable<Int> = Obervable<Int>.just(one)
}

 

위 코드에서 수행하는 것은 아래와 같습니다.

1. 다른 예시에서 사용할 integer constant를 정의합니다.

2. one이라는 정수와 just 메서드를 사용해서 Int 타입의 Observable Sequence를 생성합니다.

 

여기서 사용된 Observable 메서드인 just 메서드는 역할에 적절한 이름이 부여된 메서드라고 생각됩니다. 이 메서드는 단지 하나의 element를 가진 Observable Sequence를 생성하기 때문입니다.

하지만, Rx에서 메서드는 '연산자'라고 합니다. 우리가 좀 더 매의 눈을 가지고 있다면, 우리는 다음에 볼 연산자를 추측할 수 있습니다.

 

그럼 아래 코드를 위 예시의 끝에 추가해봅시다.

여기서 우리가 몇몇 integer를 넣어줌으로써 타입이 Observable<Int>라는 것을 알 수 있기 때문에, 이번에는 변수의 타입을 명시하지 않았습니다. 

let observable2 = Observable.of(one, two, three)

 

그리고 만약  Int 배열 타입의 Observable를 선언하고 싶다면 아래와 같이 코드를 작성해주면 됩니다.

let observable3 = Observable.of([one, two, three])

 

그리고 just 연산자로도 배열을 단일 요소로 사용할 수 있습니다. 처음에는 약간 이상해 보일 수 있지만, 이 배열은 여러 콘텐츠가 아니라 하나의 element로 볼 수 있기 때문에 just 메서드로도 가질 수 있습니다.

 

우리는 또 다른 연산자인 from으로도 Observable을 생성할 수 있습니다.  from 연산자는 오직 배열만 취급합니다.

let observable4 = Observable.from([one, two, three])

 

그럼 이제 Observable을 Subscribing해볼까요?

 

 

 

Subscribing to observables

iOS 개발자라면, 우리는 NotificationCenter에 친숙할 것입니다. NotificationCenter는 RxSwift의 Observable과는 다른 observer들에게 알림을 보냅니다. 아래를 보면, UIKeyboardDidChangeFrame notification의 observer가 있습니다.

let observer = NotificationCenter.default.addObserver(
    forName: .UIKeyboardDidChangeFrame,
    object: nil,
    queue: nil
) { notification in
    // Handle receiving notification
}

 

RxSwift에서 Observable을 Subscribing하는 것은 위와 상당히 비슷합니다. 단지 addObserver( )대신 subscribe( )를 사용하면 됩니다. 일반적으로 .default 싱글톤 인스턴스만 사용되는 NotificationCenter와 달리, Rx에서 Observable은 각기 다 다릅니다.

 

가장 중요한 것은, Observable은 그것이 Subscriber를 가지기 전까지 이벤트를 전달하지 않습니다. Observable을 구독하는 것은 Swift 표준 라이브러리의 Iterator에서 next( )를 호출하는 것과 매우 비슷합니다.

let sequence = 0..<3
var iterator = sequence.makeIterator()

while let n = iterator.next() {
    print(n)
}

/* 결과
0
1
2
*/

 

하지만 Observable을 구독하는 것은 이것보다 더 간단합니다. 우리는 각 이벤트 타입에 대한 핸들러도 추가할 수 있습니다. next 이벤트는 Observable에서 방출되는 element를 핸들러로 전달합니다. 그리고 error 이벤트는 error 인스턴스를 가지고 있습니다.

 

아래 예시를 보겠습니다.

example(of: "subscribe") {
    let one = 1
    let two = 2
    let three = 3
    
    let observable = Observable.of(one, two, three)
}

 

이것은 앞서 봤었던 예시와 비슷하지만, 이 예시에서는 of 연산자를 사용합니다.

그럼 Observable을 구독하기 위해 위 코드에 아래 코드를 추가해보겠습니다.

observable.subscribe { event in
    print(event)
}

 

subscribe 연산자에 대해서 알기 위해 ⌥(option)키를 누른 상태에서 subscribe를 클릭하면 아래와 같이 우리는 subscribe가 Int 타입의 이벤트를 받고 아무것도 반환하지 않는 escaping closure를 필요로 하는 것을 볼 수 있습니다. 그리고 subscribe는 Disposable을 반환할 것입니다.

 

func subscribe(_ on: @escaping (Event<Int>) -> Void) -> Disposable

 

그리고 Observable로부터 방출되는 각 이벤트를 print하는 위 예시의 결과는 아래와 같습니다.

 

next(1)

next(2)

next(3)

completed

 

이 Observable은 각각의 next 이벤트들을 발생시키고, completed 이벤트를 발생시키며 종료됩니다.

Observable들을 가지고 프로그래밍을 하다보면, 우리는 보통 이벤트 그 자체보다 next 이벤트에 의해 발생되는 element들을 다루게 됩니다. 이를 보기위해, 위에서 작성했던 subscribe 코드를 아래와 같이 바꾸겠습니다.

observable.subscribe { event in
    if let element = event.element {
        print(element)
    }
}

 

Event는 element 프로퍼티를 가지고 있습니다. next 이벤트만 element를 가지기 때문에 이 프로퍼티는 optional value입니다. 그래서 우리는 그게 nil이 아니라면 optional binding (if let) 을 사용해서 그 element를 upwrap시킵니다.

그렇다면 이제 next(1)처럼 next이벤트까지 나오는게 아니라, 아래와 같이 element들만 print될 것입니다.

 

1

2

3

 

이러한 패턴은 너무나도 자주 사용되기 때문에 RxSwift에서 바로 사용할 수도 있습니다. RxSwift에는 Observable이 발생시키는 next, error, completed 이벤트 유형에 대한 구독 연산자가 있습니다. 따라서 위에서 작성한 subscribe 코드를 아래와 같이 쓸 수 있습니다.

observable.subscribe(
    onNext: { element in
        print(element)
    }
)
// Xcode 환경에서 코드를 작성할 때, 
// 다른 이벤트들(error, completed)에 대한 처리를 요구할 수 있지만 
// 지금은 무시하겠습니다.

 

이제 우리는 다른 걸 신경쓸 필요 없이 next 이벤트 요소만 처리하면 됩니다. onNext 클로져는 argument로 next 이벤트를 받기 때문에, 우리는 이게 다른 이벤트 유형인지 또다시 검사할 필요가 없습니다.

 

우리는 지금까지 element를 가진 Observable을 어떻게 생성하는지 배웠습니다.

하지만 element가 없는 Observable은 어떻게 생성할까요? empty 연산자는 비어있는 Observable Sequence를 생성합니다. 그리고 이것은 completed 이벤트만 발생시키게 됩니다.

 

그럼 이에 대한 예시를 보겠습니다.

example(of: "empty") {
    let observable = Observable<Void>.empty()
}

 

Observable은 타입을 추론할 수 없는 경우, 특정한 타입으로 선언되어야 합니다. 그렇기 때문에 위 예시의 비어있는 Observable의 경우, Void로 유형을 명시해주었습니다. 그리고 아래 코드를 작성하겠습니다.

observable.subscribe(
    // 1
    onNext: { element in
        print(element)
    },
    // 2
    onCompleted: {
        print("Completed")
    }
)

 

아래는 위 예시에 표시된 숫자에 대한 설명입니다.

 

1. 이전 예시들에서 했던 것처럼 next 이벤트를 처리합니다.

2. completed 이벤트는 element를 가지지 않기 때문에, 단순하게 메시지를 출력합니다.

 

따라서 이 예시에서는 "Completed" 메시지만 출력되게 됩니다.

 

그럼 이렇게 비어있는 Observable은 어디서 사용될까요? 이것은 즉시 종료되는 observable이나 의도적으로 비어있는 Obsevable을 반환하는 데에 편리하게 사용됩니다.

 

empty 연산자와 반대로, never 연산자 아무것도 발생시키지 않고 절대 종료되지도 않는 observable을 생성합니다. 이것은 무한한 지속을 나타내는데, 아래 예시와 함께 살펴보겠습니다.

example(of: "never") {
    let observable = Observable<Any>.never()
    observable.subscribe(
        onNext: { element in
            print(element)
        },
        onCompleted: {
            print("Completed")
        }
    )
}

 

이 예시는 Completed 메시지는 물론이고 정말 아무것도 print하지 않습니다. 이게 어떻게 동작하는 걸까요? 

이것은 후에 Challenges 섹션에서 다루겠습니다.

 

지금까지 우리는 Observable로 명시된 변수의 경우만 살펴보았습니다. 하지만 어떠한 값의 범위 내에서 Observable를 생성하는 것도 가능합니다. 그럼 마지막으로 아래 예시를 살펴보겠습니다.

example(of: "range") {
    // 1
    let observable = Observable<Int>.range(start: 1, count: 10)
    observable.subscribe(
        onNext: { i in
            // 2
            let n = Double(i)
            let fibonacci = Int(((pow(1.61803, n) - pow(0.61803, n)) / 2.23606).rounded())
            print(fibonacci)
        }
    )
}

 

1. start 값과 순차적으로 생성할 개수를 뜻하는 count 값을 가지는 range 연산자를 사용해서 Observable를 생성합니다. 

2. 방출된 element에 대한 n번째 피보나치 값을 계산하고 출력합니다.

 

사실, 방출된 element를 변환하는 코드를 작성하는데엔 onNext 핸들러보다 더 나은 방법이 있지만, 그건 Chapter 7. Transforming Operators 에서 다루겠습니다.

 

never 연산자를 사용한 예시를 제외하고, 우리는 지금까지 completed 이벤트를 자동으로 발생시키고 자연스레 종료되는 Observable을 다뤘습니다. 그래서 우리는 생성에 대한 부분과 Observable을 Subscribe하는 데에 좀 더 초점을 둘 수 있었습니다. 하지만 이건 Observable을 Subscribe하는 부분에서 중요한 것을 볼 수 없게 하였습니다.

그래서 이제는 그 부분을 마주할 차례입니다.

 

 

 

Disposing and terminating

앞서 말한 내용 중에 Observable은 subscription을 받을 때까지 어떠한 것도 방출시키지 않는다는 사실을 기억하시나요? subscription은 Observable이 error나 completed 이벤트를 발생시키고 종료할 때까지 next 이벤트들을 방출하도록 하는 방아쇠 역할을 합니다. 우리는 이 subscription을 해제하여 observable을 수동으로 종료시킬 수도 있습니다.

그럼 아래 예시를 봅시다.

example(of: "dispose") {
    let observable = Observable.of("A", "B", "C") // (1)
    let subscription = observable.subscribe { event in // (2)
        print(event) // (3)
    }
}

 

(1) 여러 String을 가진 Observable을 생성합니다.

(2) observable을 subscribe합니다. 이 때 반환되는 Disposable 객체를 subscription이라는 로컬 변수(상수)로 저장합니다.

(3) 방출되는 이벤트들을 출력합니다.

 

이후에 subscription을 명시적으로 취소하기 위해서는 dispose( )를 호출하면 됩니다. subscription을 이렇게 해제하고 나면, observable은 더이상 이벤트를 방출시키지 않을 것입니다. 위의 예시에 아래 코드를 추가해보세요.

subscription.dispose()

 

만약 subscription이 여러개 있을 때 각자 따로따로 관리해주는건 당연히 귀찮은 일이기 때문에, RxSwift에는 DisposeBag 타입이 있습니다. 이 DisposeBag는 .addDisposableTo 메서드를 사용해서 Disposable 객체들을 가지게 됩니다. 그리고 이후에 할당해제시킬 때 dispose( )를 호출합니다.

그럼 아래 예시를 살펴보겠습니다.

example(of: "DisposeBag") {
    let disposeBag = DisposeBag() // (1)
    
    Observable.of("A", "B", "C") // (2)
      .subscribe { // (3)
        print($0)
      }
      .addDisposableTo(disposeBag) // 4
}

 

(1) dispose bag를 생성합니다.

(2) observable을 생성합니다.

(3) default argument name인 $0을 사용해서 방출되는 이벤트를 프린트하는 observable을 subscribe합니다.

(4) subscribe하고 나서 반환되는 Disposable을 disposeBag에 넣습니다.

 

이 패턴은 우리가 자주 쓰이게 될 것입니다. observable을 생성하고 구독하고 바로 dispose bag에 집어 넣는 패턴을 말이죠.

 

도대체 왜 Disposable을 이렇게 귀찮게 관리해줘야 할까요?

우리가 이 disposeBag에 넣는 걸 해주지 않는다면 어떻게 될까요?

우리는 subscription을 해제해줘야 할 때마다 dispose 메서드를 호출해야 하거나, 어떤 시점에서 observable를 종료시켜줘야 하는 상황에서 우리는 아마 메모리 누수가 발생할 것입니다. 물론, 그래도 크게 걱정할 필요는 없습니다. 어차피 Swift 컴파일러가 우리에게 누락된 Disposable이 있다고 경고해줄 거니까요 :)

 

지금까지 예시들을 보면, 우리는 특정한 next 이벤트를 가진 observable을 생성해왔습니다. 하지만 또 하나의 observable 생성 방법으로는 create 연산자를 사용하는 방법이 있습니다.

아래 예시를 통해 살펴보겠습니다.

example(of: "create") {
    let disposeBag = DisposeBag()
    
    Observable<String>.create { observer in
        // something to do..
    }
}

 

이 create 연산자는 subscribe라는 하나의 파라미터를 필요로 합니다. observable에서 subscribe를 호출함으로써 그 역할을 구현할 수 있도록 하기 때문입니다. 즉, 그것은 subscriber들에게 방출될 이벤트 처리 방식을 정의합니다.

⌥(option)키를 누른 상태에서 create 연산자를 클릭하면, 아래와 같이 나옵니다.

 

static func create(_ subscribe: @escaping (AnyObserver<T>) -> Disposable) -> Observable<T>

 

이 subscribe 파라미터는 escaping closure으로써, AnyObserver를 전달받고 Disposable을 반환시킵니다. AnyObserver는 Observable에 값을 추가하는 것을 유연하게 처리할 수 있도록 하는 generic type으로, subscriber들에게 방출됩니다.

그럼 이 create 메서드 구현을 아래와 같이 바꿔볼까요?

Observable<String>.create { observer in
    observer.onNext("1") // (1)
    observer.onCompleted() // (2)
    observer.onNext("?") // (3)
    
    return Disposable.create()
}

 

(1) observer에 next 이벤트를 추가합니다. 

(2) observer에 completed 이벤트를 추가합니다.

(3) observer에 또 다른 next 이벤트를 추가해봅니다.

(4) disposable 객체를 반환시킵니다.

 

여기서 두 번째 next 이벤트("?")는 subscriber들에게 방출 될까요??

결과를 확인해보기 위해서 위 예시의 아래줄에 다음 코드를 추가해보겠습니다.

.subscribe(

    onNext: { print($0) },
    onError: { print($0) },
    onCompleted: { print("Completed") },
    onDisposed: { print("Disposed") }
    
).addDisposableTo(disposeBag)

 

그럼 이 결과는 아래와 같습니다.

두 번째 next 이벤트는 observable이 이미 completed 이벤트를 발생시키고 종료하였기 때문에 방출되지 않습니다.

 

1

Completed

Disposed

 

그럼 observer에 error를 추가하면 어떻게 될까요? 아래 코드를 위 예시의 최상단에 넣어보겠습니다.

enum MyError: Error {

    case anError

}

 

그리고 아래 코드를 첫 번째 observer.onNext와 observer.onCompleted 사이에 넣어보겠습니다.

observer.onError(MyError.anError)

 

그럼 아래와 같이 error를 발생시키고 종료되는 결과가 발생하게 됩니다.

 

1

anError

Disposed

 

만약 우리가 completed 이벤트나 error 이벤트를 발생시키지 않고, disposeBag에 subscription을 추가하지 않으면 어떤 일이 발생할까요?? 그럼 한 번, observer.onError와 observer.onCompleted, addDisposableTo(disposeBag)를 주석처리해서 그 결과를 보겠습니다.

enum MyError: Error {
    case anError
}

let disposeBag = DisposeBag()

Observable<String>.create { observer in
    observer.onNext("1")
    // observer.onError(MyError.anError)
    // observer.onCompleted()
    observer.onNext("?")
    
    return Disposable.create()
}
.subscribe(
    onNext: { print($0) },
    onError: { print($0) },
    onCompleted: { print("Completed") },
    onDisposed: { print("Disposed") }
)
// .addDisposableTo(disposeBag)

 

이렇게 되면 우리는 메모리 누수가 발생하게 됩니다!! observable은 절대 끝나지 않을 거고, disposable은 절대 해제되지 않을 거예요. 그리고 결과는 아래와 같을 것입니다.

 

1

?

 

DisposeBag에 구독을 던져놓으면, ViewController가 할당 해제될 때 자동으로 폐기됩니다.
단, rootVC 같은 특정 ViewController에서는 이런 작용이 일어나지 않습니다. rootVC는 앱이 종료되기 전까지는 해제되지 않기 때문입니다.

 

 

 

Creating observable factories

subscriber들을 기다리는 observable을 생성해놓는 것보다, 각 subscriber에게 새로운 observable을 공금해주는 observable factory를 만드는 게 더 좋을 수 있습니다.

그럼 아래 예시를 보겠습니다.

example(of: "deferred") {
    let disposeBag = DisposeBag()
    
    var flip = false // (1)
    
    let factory: Observable<Int> = Observable.deferred { // (2)
        flip = !flip // (3)
        
        if flip { // (4)
            return Observable.of(1, 2, 3)
        } else {
            return Observable.of(4, 5, 6)
        }
    }
}

 

(1) observable이 반환될 때 사용될 Bool 타입의 flip을 선언합니다.

(2) deferred 연산자를 사용한 Int 타입의 Observable factory를 생성합니다.

(3) factory가 subscribe될 때마다 flip을 반대 값으로 바꿉니다.

(4) flip 값에 따라서 다른 observable을 반환합니다.

 

그리고 이 factory를 4번 반복해서 subscribe하며 출력해보면, 아래와 같은 결과가 나오게 됩니다.

 

123

456

123

456

 

 

 

 

우리는 지금까지 Observable를 어떻게 생성하는지, 어떻게 다루는지, 어떻게 해제 시켜야 메모리 누수가 발생하지 않는지 등을 배웠습니다. 다음 시간에는 Subject에 대해 알아보겠습니다.

 

 

 

반응형

'iOS > RxSwift' 카테고리의 다른 글

Ch7. Transforming Operators  (0) 2023.02.24
Ch5. Filtering Operators  (0) 2023.02.24
RxSwift. Traits가 뭘까? (Single, Maybe, Completable)  (0) 2023.02.24
Ch3. Subjects  (0) 2023.02.24
Ch9. Combining Operators  (1) 2021.01.13

댓글