-Today's Learning Content-

  • RxSwift란?
  • Observer & Observable
  • Observable 코드 작성

1. RxSwift

내용 정리

RxSwift란 무엇인지에 대해 학습하고 이를 활용하는 코드들에 대해 학습한 내용들을 정리한다.

1) RxSwift란?

RxSwift는 Swift에서 Reactive Programming을 구현하기 위한 라이브러리이다. Reactive Extensions(약칭 Rx) 패밀리의 일부로, 데이터 흐름 및 비동기 이벤트 처리를 선언적이고 직관적으로 작성할 수 있게 도와준다.

주요 개념:

  1. Reactive Programming:

    • 이벤트 스트림을 기반으로 상태 변화를 관리하고, 선언형 방식으로 데이터를 처리
  2. 비동기 처리의 간소화:

    • RxSwift는 콜백 지옥, 비동기 이벤트의 조합, UI 업데이트 등의 작업을 단순화
  3. 핵심 구성 요소:

    • Observable: 데이터 스트림을 생성하고 이벤트를 방출
    • Observer: Observable이 방출하는 이벤트를 구독하고 반응
    • Operators: 데이터 스트림 변환 및 조작 도구

2) Observer & Observable의 개념

먼저 ObserverObservable의 사전적 의미는 아래와 같다.

의미 그대로 옵저버 패턴이란, 어떤 객체의 상태가 변화할 때 이를 관측하고 있던 구독자들에게 변화한 이벤트를 전달 시켜주는 디자인 패턴이다.
이 때, 이벤트를 발생시키는 객체를 Publisher(=Observable = Subject), 구독자를 Observer(= 관찰자)라고 한다.

이러한 디자인 패턴은 어떠한 객체에 변화가 일어났는지 그 때마다 확인하는 것이 번거롭기 때문에, 객체에 변화가 있을 때 특정 액션을 취할 객체들이 항상 객체를 관측하고 있다가 특정 객체가 변화하면 곧바로 변화를 알아차리게 할 수 있도록 하기 위해서 만들어진 것이다.

비유하자면 우리가 특정 사이트에 가입을 할 때, 선택 사항으로 마케팅 수신 동의를 할 수가 있는데, 이를 하지 않는 것은 RxSwift적 개념으로 볼 때 구독(Subscribe)을 하지 않는 것이고, 만약 동의를 한다면 구독을 한다고 볼 수 있다.
그래서 가입한 사이트가 새로운 이벤트를 개최하면 마케팅 수신을 동의한 구독자(Observer)들에게 연락을 보내고(이벤트 방출) 이를 본 구독자들이 이벤트에 참여하거나 불참하는 등 어떠한 액션을 취하게 되는 것이라고 비유할 수 있다.

RxSwift는 이벤트를 발행하는 주체가 구독자가 누구인지 구체적인 타입으로 몰라도 되고, 구독자들만 이벤트를 발행하는 주체를 구체적으로 알고 있는 느슨한 결합구조를 가진다.
또, iOS에서는 보통 ViewData를 구독하며, Data가 변화가 일어났을 때 그 DataView에 적용시키는 구조로 많이 활용한다.

3) Observable

Observable데이터를 생산하고 방출하는 역할을 하는 클래스이다.. 데이터는 이벤트 스트림 형태로 제공되며 세가지 이벤트 타입을 방출할 수 있다.

  • next: 새 데이터 항목
  • error: 오류 발생
  • completed: 스트림 종료

Observable 을 구독하는 것을 subscribe 라고 하며, 이를 구독하고 관찰하는 관찰자를 Observer 라고 한다. 그리고 구독을 해제하는 것을 dispose 라고 한다.

Observable은 위에서 말했 듯 클래스 타입이며 아래와 같은 형식이다.

// Observable 클래스의 정의
public class Observable<Element> : ObservableType { ... }

위의 정의에서 ObservableType은 클래스만 채택할 수 있는 프로토콜로, RxSwift의 핵심 프로토콜 중 하나이다.
이는 Observable의 일반적인 동작을 추상화하며, RxSwift의 데이터 스트림을 표현하기 위한 기본 구조를 정의하고 있다.

// ObservableType의 정의
public protocol ObservableType: AnyObject {
    associatedtype Element

    func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable where Observer.Element == Element
}

여기서 subscribe메소드가 Observer(구독자)와의 연결을 담당하며, Disposable을 반환하여 구독을 취소하거나 메모리 누수를 방지하는 역할이다.

또, ObservableTypeRxSwiftOperator와 결합하여 데이터를 처리하기 때문에 RxSwift의 모든 Operator 연산은 ObservableType을 통해 동작한다.

Observable은 데이터를 스트림 형태로 제공한다고 하였는데, 이 말은 즉, 옵저버블이 시간적인 개념이 포함된다는 것을 의미한다.
예전에 만들었던 카운터 앱을 예시로 생각해보자. 우리는 증가 혹은 감소 버튼으로 레이블의 값을 변화 시켰었다.

카운터 앱 동작 예시

만약 count라는 변수가 있고, 어떤 UILabelcount를 구독하고 있는 옵저버라고 생각해보자.
우리는 '증가' 버튼과 '감소' 버튼을 눌러서 count의 값을 변화 시킬 수 있다. 그러면 count는 값이 변화하면서 이벤트를 방출하고 이를 구독하던 UILabel이 이벤트를 받아 화면에 count와 같은 값을 보여줄 것이다.
그런데 우리가 '증가' 버튼을 5번 누르고, '감소' 버튼을 10번을 누른다고 했을 때, 그 동작들은 한 순간에 이루어지지 않고 시간의 흐름에 따라 이루어진다.

'증가' 버튼을 먼저 눌렀다면 count

1 -> 2 -> 3 -> 4 -> 5

의 순서로 변화했을 것이고 이를 구독하던 UILabel도 같은 순서로 변화했을 것이다.
'감소' 버튼을 눌렀을 경우에도 마찬가지이다.

이처럼 옵저버블이 이벤트, 즉 데이터를 방출하는 것은 시간의 흐름에 따라 다르고, 이 때문에 데이터 스트림이라고 하는 것이다.

2. Observable 코드 연습

내용 정리

위에서 학습한 Observable의 내용을 바탕으로 실제 코드로 어떻게 작성할 수 있는지 예제를 통해 학습해 보자.

1) DisposeBag()

DisposeBag은 특정 옵저버블을 구독하는 옵저버들을 모아두는 가방이다. 그러나 그냥 모아두는 가방은 아니고, 특정 옵저버블이 더이상 이벤트를 발생시키지 않아 필요가 없어졌을 때, 이 옵저버블을 구독하는 옵저버들을 한 번에 쉽게 옵저버블을 구독 해제할 수 있도록 모아두는 역할을 한다.

먼저 옵저버블을 구독하는 subscribe메소드를 다시 살펴보자

func subscribe<Observer: ObserverType>(_ observer: Observer) -> 
											Disposable where Observer.Element == Element

여기서 포인트는 반환 타입인데, 옵저버의 엘리멘트 속성을 충족하는 Dispoable 타입인 것을 알 수 있다.
Dispose, Disposable의 사전적 의미는 아래와 같다.

RxSwift에서는 주로 '처리하다', '처리할 수 있는' 등의 의미로 쓰이는데, 한 마디로 옵저버블을 구독하고 있는 옵저버들을 '처리하는' 역할인 것이다.

Disposable의 정의를 살펴보면...

/// Represents a disposable resource.
public protocol Disposable {
    /// Dispose resource.
    func dispose()
}

심플하게 dispose() 메소드를 가진 프로토콜이다. 위에서 dispose가 처리하다 라는 뜻을 가진다는 것을 확인 했으니 유추할 수 있는 점은, 특정 옵저버블을 subscribe 메소드를 이용해 구독하면 구독을 취소할 수 있는 타입으로 만들어 준다는 것이다.

이런 코드가 왜 필요할까? 그것은 옵저버블이 클래스 타입이기 때문일 것이다. 만약 옵저버들의 구독 취소를 하는 기능이 없으면, 옵저버블이 메모리에서 해제 되더라도 옵저버들의 구독이 남아있기 때문에 RC가 0이 되지 않고 순환 참조가 발생할 것이다.
즉, 메모리 누수가 발생하는 것을 방지하기 위해 Diposable 타입을 반환하여 구독을 취소할 수 있게 만드는 것이다.

메모리 누수 방지를 위해 Disposable 타입을 반환하는 건 알겠다.
그럼 개발자는 메모리 누수 방지를 위해 옵저버블이 메모리에서 해제되는 타이밍을 찾아서 옵저버들에게 각각 dispose()메소드를 사용 해야하는건가?

당연히 아니다.

이 때 가장 처음에 말한 DisposeBag이 사용되게 된다.
RxSwift에서는 특정 옵저버블을 구독하면 항상 마지막에 DisposeBag을 타입으로 가지는 변수에 옵저버를 추가하는 코드를 추가해줘야 한다.

// 예시
let disposeBag = DisposeBag()

let observable = Observable<String>.create { observer in
    observer.onNext("hello")
    return Disposables.create()
}

observable.subscribe(onNext: { data in
    print(data)
}).disposed(by: disposeBag) // 옵저버를 가방에 저장

이렇게 하면 옵저버블이 메모리에서 해제될 때 disposeBag에 있는 모든 옵저버들의 구독이 취소되게 된다.

어떻게 이런 동작이 가능할까? 코드를 뜯어보자.

먼저 DisposeBag의 코드를 살펴보면 deinit이 정의된 모습을 볼 수 있다.

public final class DisposeBag: DisposeBase {

	// 생략
    
    // state
    private var disposables = [Disposable]()
    private var isDisposed = false
    
    /// This is internal on purpose, take a look at `CompositeDisposable` instead.
    private func dispose() {
        let oldDisposables = self._dispose()

        for disposable in oldDisposables {
            disposable.dispose()
        }
    }
    
    private func _dispose() -> [Disposable] {
        self.lock.performLocked {
            let disposables = self.disposables
            
            self.disposables.removeAll(keepingCapacity: false)
            self.isDisposed = true
            
            return disposables
        }
    }
    
    deinit {
        self.dispose()
    }
}

생략을 했는데도 코드가 꽤 길지만... 어쨌든 우리가 .disposed(by:)메소드를 이용해 DisposeBag에 옵저버를 저장하면 disposables라는 배열에 저장되고, DisposeBag이 메모리에서 해제되면 dispose()메소드를 통해 배열에 담긴 모든 옵저버들을 dispose하는 것이다.

이런 동작으로 옵저버들의 구독을 한 번에 관리할 수 있게 된다.
그렇다면 DisposeBag이 메모리에서 해제되는 시점은 언제일까?

바로 DisposeBag을 관리하는 뷰 컨트롤러가 메모리에서 해제되는 시기이다. 일반적으로 DisposeBagUIViewController와 함께 사용되기 때문에 뷰 컨트롤러가 메모리에서 해제될 때 DisposeBag도 함께 해제되므로 구독이 자동으로 정리된다.

만약 뷰 컨트롤러가 아닌 다른 곳에서 사용하더라도 마찬가지로 DisposeBag을 관리하는 객체가 메모리에서 해제되면 함께 해제되는 것이다.

이처럼 DisposeBag은 옵저버들의 구독 해제를 쉽게 관리할 수 있기 때문에 잘 활용하는 것이 좋다. 만약 DisposeBag을 사용하지 않는다면 직접 dispose()를 호출하여 구독을 해제해야 하는데, 혹시라도 이것을 깜빡하거나 놓치는 부분이 생기면 메모리 누수가 발생할 수 있기 때문에 불안정하다.
DisposeBag을 사용하면 자동으로 비교적 안전하게 옵저버의 구독을 취소할 수 있으므로 웬만하면 사용하는 것을 권장한다고 한다.

2) Observable.create

Observable.create는 옵저버블을 생성하는 가장 기본적인 방법이다. DisposeBag을 설명하며 살짝 코드를 보여주기도 하였는데, 기본적인 사용 방법은 아래와 같다.

let observable = Observable<String>.create { observer in
    observer.onNext("hello")
    observer.onError(MyError())
    observer.onCompleted()
    return Disposables.create()
}

이 때, 반환 값이 Disposables.create()인데, 코드를 살펴보면 결국 Disposable타입을 반환하는 것을 알 수 있다.

코드를 살펴보면 private으로 정의한 NopDisposable이라는 구조체가 Disposable 프로토콜을 채택하고 싱글톤 패턴으로 사용되는 모습을 볼 수 있는데, 문서 주석을 읽어보면 아무 일도 하지 않는 1회용 제품을 만든다라고 쓰여있다.
아마 Disposable은 프로토콜이기 때문에 객체를 만들 수 없어서 이를 객체화 시키기 위해 구조체를 만들고 싱글톤 패턴을 이용하여 사용하는 것으로 추측된다.
다만 이렇게 만들어진 객체는 특별한 일(구조체가 메소드나 계산 프로퍼티 등을 소유하고 있지 않으니까)을 하지 않고 그저 Disposable의 역할을 할 뿐이기 때문에 Nop이라고 표현한 것이 아닐까 생각한다.

왜 객체를 만들어야 하냐면 우리가 위에 작성한 코드만 보아도

let observable = Observable<String>.create { observer in
	// ...
    return Disposables.create()
}

이렇게 하나의 변수를 만들어 그것을 옵저버블로 만들기 때문이 아닐까 생각한다.

어쨌든 Observable.create는 이렇게 사용할 수 있다.
혹시 다르게도 사용할 수 있을까 궁금해서 실험해 보았다.

var observable: Observable<Int>

observable = Observable<Int>.create { obserber in
    obserber.onNext(10)
    return Disposables.create()
}

observable.subscribe(onNext: { data in
    print(data)
}).disposed(by: disposeBag)

observable = Observable<Int>.create { obserber in
    obserber.onNext(20)
    return Disposables.create()
}

observable.subscribe(onNext: { data in
    print(data)
}).disposed(by: disposeBag)

이렇게 사용해도 문제없이 10, 20이 출력되는 것을 알 수 있었다.
위의 코드와 다른 점은 옵저버블을 var로 정의하여 변수로 선언해주고, 타입만 정의하여 create를 나중에 사용한 것이다.
이것을 시험해 본 이유는 RxSwift의 목적이 데이터 바인딩을 통해 옵저버블이 방출하는 이벤트를 감지하고 특정 액션을 취하는 것이라면, 처음부터 어떤 데이터를 방출할 것인지 정해지는 것이 아니라 나중에 정의하게 되거나 추가하는 경우가 있지 않을까 싶어서 실험해 보았다.
예상은 했지만 당연하게도 잘 작동이 되는 것을 보고 RxSwift에 대해 조금은 더 이해가 되는 것 같기도 했다.(아님 말고...)

3) subscribe

subscribe는 '구독'이라는 의미를 가지고 있다. 말 그대로 특정 옵저버블을 '구독'하기 위해 사용하는 메소드인데, 이 메소드가 없으면 옵저버블은 존재 가치가 퇴색되어 버린다. 왜냐하면 이벤트를 아무리 방출해도 이것을 관측하는 옵저버가 없다면 아무런 영향을 미치지 못하기 때문이다.

Observable은 두 가지 타입이 있는데, Cold ObservableHot Observable이 있다. 둘의 차이는 데이터가 흐르기 시작하는 타이밍에 있다.

Cold Observable은 어떤 옵저버가 subscribe를 시작한 시점부터 데이터가 흐르기 시작한다. 그러나 Hot Observable은 구독과 무관하게 데이터가 계속 흐르고 있고, 어떤 옵저버가 subscribe를 시작하면 해당 시점의 데이터부터 관측할 수 있게 되는 것이다.

우리가 위에서 선언한 코드들은 모두 Cold Observable에 해당한다. 왜냐하면 subscribe를 선언한 이후부터 이벤트 방출이 시작되기 때문이다.

그렇다면 Hot Observable은 어떻게 구현할까?

다음 시간에 알아보자.

4) 다양한 옵저버블 생성 방법

옵저버블을 생성하는 방법은 create 외에도 여러가지가 있는데, 그 중 일부를 소개한다.

1️⃣ Observable.just

  • 간단하게 단일 데이터를 방출하는 Observable을 생성할 때는 just를 활용할 수 있다.
// 단일 데이터를 방출하는 just 활용.
let observable = Observable.just("crois")

// 간단하게 onNext 만 선언할 수도 있습니다.
observable.subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)

2️⃣ Observable.of

  • 여러 개의 데이터를 방출하는 Observable을 생성할 때는 of를 활용할 수 있다.
// 복수 데이터를 방출하는 of 활용.
let observable = Observable.of("red", "blue", "yellow")

observable.subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)

3️⃣ Observable.from

  • 배열에서 각 요소를 순차적으로 방출하는 Observable을 생성할 때는 from을 활용할 수 있다.
// 배열로 부터 데이터를 방출하는 from 활용.
let observable = Observable.from([1, 2, 3, 4, 5])

observable.subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)

4️⃣ Observable.interval

  • 일정한 시간 주기로 데이터를 방출하는 Observable을 생성할 때는 interval을 활용할 수 있다.
// Scheduler는 쓰레드를 지정
// take는 반복 횟수 지정
let observable = Observable<Int>.interval(.seconds(1), scheduler: SerialDispatchQueueScheduler(qos: .default)).take(5)

observable.subscribe { event in
    print(event)
}.disposed(by: disposeBag)

5) 결론

RxSwift는 비동기 이벤트와 데이터 스트림을 간단하게 처리할 수 있도록 도와주는 라이브러리이다. ObserverObservable의 상호작용, DisposeBag을 통한 메모리 관리, 다양한 Operator의 조합은 RxSwift를 이해하는데 필수적인 요소들이다.

오늘은 처음 배우는 날이었기 때문에 가볍게 다뤄보았고, 내일부터는 스케줄러, 오퍼레이터, Trait 등 좀 더 자세하게 RxSwift에 대해 다뤄볼 예정이다.

-Today's Lesson Review-

오늘은 새로운 심화 강의를 지급받아 학습을 진행하였다.
기존에 알고 있던 내용을 좀 더 심도있게 다루기도 하고 새로운 내용을 배우기도 하였다.
이 중 RxSwift에 대한 내용이 너무 방대해서
여러 일자에 나눠서 블로그를 정리해야 할 것 같다.
어려운 개념이기도 해서 이해하는데 시간이 오래 걸릴 것 같다...
profile
이유있는 코드를 쓰자!!

0개의 댓글