RxSwift (1)
구매해둔 Udemy 강의중 RxSwift 강의가 너무 오래전이기도하고 Combine을 항상 사용해와서 그냥 냅두려다가 강의 내용도 7시간 정도 밖에 안되고 개념 자체는 Combine과 유사해서 그래도 간단하게 알아두면 좋을듯 해서 내용을 적어보도록 한다.
ReactiveX 공식 사이트에서 RxSwift를 확인 할 수 있다.
그리고 RxSwift의 경우 Framework가 아닌 Library 이다.
Framework와 Library의 차이
Framework와 Library의 차이를 찾아보면서 가장 먼저 보게 된 건 제어 흐름(Control Flow)의 주체가 누구냐는 것이었다.
둘 다 다른 곳에서 만들어진 기능을 가져와 사용하는 건 똑같은데, 내 코드가 필요할 때 호출하느냐, 아니면 Framework가 정해진 시점에 내 코드를 호출하느냐에서 차이가 생긴다.
1. Library
Library는 필요한 기능을 가져다 쓰는 도구 상자에 가깝다.
내가 애플리케이션의 흐름을 직접 가지고 있고, 필요한 시점에 Library의 기능을 호출한다.
예를 들어 Alamofire를 사용한다면 내가 직접 요청을 시작한다.
1
2
3
4
AF.request(url)
.response { response in
// 결과 처리
}
흐름으로 보면 간단하다.
1
내 코드 → Library 호출 → 결과 반환 → 내 코드
즉 내가 Library를 호출한다.
대표적으로 Alamofire, Kingfisher, SnapKit, RxSwift 등이 있다.
2. Framework
Framework도 여러 기능을 제공한다는 점에서는 Library와 비슷하다.
그런데 Framework는 기능만 제공하는 것이 아니라 애플리케이션이 동작하는 구조나 규칙까지 함께 가지고 있는 경우가 많다.
그래서 개발자는 Framework가 정해놓은 방식에 맞춰 코드를 작성하고, 적절한 시점에 Framework가 내 코드를 호출한다.
대표적인 예가 UIKit의 viewDidLoad()다.
1
2
3
4
5
6
7
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 내가 작성한 코드
}
}
여기서 viewDidLoad()를 내가 직접 호출하는 게 아니다.
화면의 생명주기를 관리하는 UIKit이 적절한 시점에 이 메서드를 호출하고, 그 안에 내가 작성한 코드가 실행된다.
1
Framework → 내 코드 호출 → 내 코드 실행
즉 Framework가 나를 호출한다.
이처럼 원래 내가 가지고 있던 제어 흐름이 Framework 쪽으로 넘어가는 것을 제어의 역전(Inversion of Control, IoC)이라고 한다.
UIKit, SwiftUI, Combine 같은 것들이 대표적인 Framework다.
3. 그래서 둘을 어떻게 구분하면 될까?
처음에는 둘 다 “남이 만들어놓은 코드를 가져다 쓰는 것”이라서 비슷해 보인다.
그런데 호출 방향을 생각하면 꽤 명확해진다.
1
2
3
4
5
6
내 코드 ─────────▶ Library
내가 호출
Framework ───────▶ 내 코드
Framework가 호출
결국 가장 간단하게 기억하면 된다.
Library는 내가 호출하고, Framework는 나를 호출한다.
그리고 여기서 중요한 건 내장되어 있느냐 / 따로 설치하느냐가 아니다.
Apple이 기본으로 제공하는 Framework도 있고, 외부에서 설치하는 Framework도 있다. Library도 마찬가지다.
즉 SPM으로 설치한다고 Library인 것도 아니고, iOS에 기본으로 들어있다고 Framework인 것도 아니다.
핵심은 제어 흐름의 주체가 누구인지다.
Firebase나 Google Maps처럼 SDK 형태로 제공되는 것도 마찬가지다. SDK라는 건 개발에 필요한 여러 구성요소를 묶어 제공하는 개념이라, 그 안에 Framework나 Library 등이 함께 들어갈 수도 있다.
그래서 결국 이것만 기억하면 될 것 같다.
Library → 내가 필요할 때 부른다.
Framework → 내가 규칙에 맞춰 작성하면, Framework가 적절한 시점에 부른다.
RxSwift 설치
RxSwift의 Combine과 달리 별도의 설치가 필요하다.
일반적으로 CocoaPod으로 설치를 예전에 많이 했지만, 이전에도 언급을 한번 했는데 해당 라이브러리 github에 Package.swift 파일이 있다면, SPM으로도 설치가 가능하다.
RxSwift Github에는 Package.swift가 있으므로 SPM으로 설치를 하면 된다.
이렇게 추가를 해준다.
여러개 패키지를 고르라고 해서 이건 ai에게 어떤걸 추가하면 좋을지를 물어보았고 아래와 같이 정리했다.
| 패키지명 | 필수 여부 | 주요 역할 및 특징 | 주요 구성요소 |
|---|---|---|---|
| RxSwift | 필수 | Rx 반응형 프로그래밍의 핵심 로직 제공 | Observable, Single, Operator 등 |
| RxCocoa | 권장 | UIKit/Cocoa UI 요소와 Rx를 연결하여 이벤트를 바인딩 | UIButton.rx.tap, UITextField.rx.text, bind(to:) 등 |
| RxRelay | 권장 | 에러/완료 없이 지속되는 UI 상태 관리용 스트림 제공 | PublishRelay, BehaviorRelay, ReplayRelay |
| RxTest | 선택 (Test Target) | Rx 스트림의 가상 시간(Virtual Time) 기반 단위 테스트 지원 | TestScheduler, TestObserver |
| RxBlocking | 선택 (Test Target) | 비동기 Rx 스트림을 동기식으로 차단하여 결과 검증 지원 | toBlocking() |
일단 여기선 RxSwift, RxCocoa, RxRelay 이 3개만 설치를 했다.
Observable
Observable은 이벤트(값)를 방출(emit)하는 수동적인 데이터 스트림 객체이다. 구독자(Subscriber)가 존재하지 않는 독립적인 상태에서는 아무런 이벤트도 방출하지 않으며, 구독이 시작되는 시점부터 정의된 규칙에 따라 데이터를 순차적으로 전달한다.
1. Observable의 기본 특성
- Observable은 이벤트(값)를 방출(emit)하는 역할을 한다.
- 구독자(Subscriber)가 존재하지 않으면 Observable은 아무것도 방출하지 않는다. 즉, 구독(Subscribe)이 발생하기 전의 Observable 단독 상태는 아무런 동작도 하지 않는다.
2. 주요 Observable 생성 메서드
just- 단 하나의 값만 포함하는 Observable을 생성한다.
- 단일 값을 기반으로 타입을 자동 추론(Type Inference)하지만, 명시적으로 타입을 지정하는 것도 가능하다.
- 로직 설명: 단일 정수
5를 전달하면Observable<Int>가 되며, 명시적으로Double을 타입 지정하면5.0형태의Observable<Double>이 된다.
of- 단일 값뿐만 아니라 여러 개의 값을 인자로 받아 순차적으로 방출하는 Observable을 생성한다.
- 방출 흐름: 인자로 전달된 요소들을 첫 번째 값부터 마지막 값까지 순서대로 하나씩 방출한다.
- 배열 전달 시 주의점: 배열 전체를 하나의 요소로 인지하여 [1, 2]를 첫 번째 이벤트로, [3, 4]를 두 번째 이벤트로 방출한다.
from- 오직 배열(Array) 형태의 요소만 인자로 받는다.
- 로직 설명: 배열로 전달받은 요소들을 하나씩 분해하여 개별적인 이벤트(단일 요소)로 변환한 뒤 순차적으로 방출한다. (배열
[1, 2, 3]전달 시Observable<Int>가 생성됨)
1
2
3
4
5
6
7
8
9
10
11
let justObservable = Observable.just(5)
let doubleObservable: Observable<Double> = Observable<Double>.just(5)
let ofObservable = Observable.of(1)
let multiObservable = Observable.of(1, 2, 3, 4)
let arrayObservable = Observable.of([1, 2], [3, 4])
let fromObservable = Observable.from([1, 2, 3, 4])
3. 핵심 요약
just: 딱 1개의 값만 방출할 때 사용of: 1개 이상의 여러 개별 값을 순차적으로 방출할 때 사용from: 배열 형태의 데이터를 개별 요소 단위로 나누어 방출할 때 사용
Subscriber
Subscriber(구독자)는 Observable을 구독하여 관찰하고, 방출되는 이벤트를 받아 실제로 처리하는 주체이다. Observable은 구독되는 순간부터 이벤트를 방출하기 시작하며, 이벤트의 종류에 따라 적절한 로직을 수행하도록 코드를 구성한다.
1. Subscriber와 Event
- Observable은 구독자(Subscriber)가
subscribe를 통해 구독하기 전까지 이벤트를 방출하지 않는다. - 이벤트는 Enum 타입으로 총 3가지 케이스가 존재하며, 종료 이벤트를 받으면 Observable의 라이프사이클이 끝난다.
next: 새로운 데이터 요소(Element)를 전달error: 에러 발생 시 에러 정보와 함께 전달 (스트림 종료)completed: 모든 데이터 방출이 정상적으로 완료됨을 전달 (스트림 종료)
2. 구독(Subscribe) 사용 방식
- 기본 구독 방식 (
subscribe)Event자체를 전달받아 처리하는 방식이다.event.element를 통해 데이터에 접근할 수 있으며, 이 값은 옵셔널(Optional) 타입이다.
- 편의 메서드 활용 방식 (
subscribe(onNext:onError:onCompleted:))- 각 이벤트 시점에 실행될 클로저를 직접 지정하는 편의 메서드이다.
onNext클로저 내부에서는 옵셔널 추출이 완료된(Unwrapped) 데이터 요소를 직접 전달받아 다룰 수 있다.
3. 추가적인 Observable 생성 메서드
range- 시작값과 개수(count)를 지정하여 연속된 정수 범위를 순차적으로 방출하는 Observable을 생성한다.
- 로직 설명:
start: 1, count: 4지정 시1, 2, 3, 4요소를 순차적으로 방출한 후 완료된다.
empty- 요소를 전혀 방출하지 않고, 구독 즉시
completed이벤트만 방출하는 Observable을 생성한다. - 특정 작업이 즉시 완료되었음을 알리거나 데이터 방출 없이 종료 신호만 필요할 때 사용한다.
- 요소를 전혀 방출하지 않고, 구독 즉시
never- 요소를 방출하지 않을 뿐만 아니라
completed나error이벤트도 전혀 방출하지 않는 Observable을 생성한다. - 스트림이 영원히 종료되지 않아야 하는 상황에 활용된다.
- 요소를 방출하지 않을 뿐만 아니라
deferred(Observable Factory)- 구독(Subscribe)되는 시점까지 Observable의 생성을 지연시키는 팩토리 메서드이다.
- 로직 설명: 구독이 발생할 때마다 생성 클로저가 실행되어 새로운 Observable을 반환한다. 내부 상태(예: 토글 변수)의 변화에 따라 구독 시점별로 서로 다른 이벤트를 방출하는 Observable을 동적으로 만들어낼 수 있다.
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
let multiObserver = Observable.of(1, 2, 3, 4)
multiObserver.subscribe { (event) in
print(event.element ?? event)
}
// result
1
2
3
4
completed
multiObserver.subscribe(onNext: { (element) in
print(element)
}, onError: nil, onCompleted: { print("Multi is Completed") }, onDisposed: nil)
//
1
2
3
4
Multi is Completed
let rangeObserver = Observable<Int64>.range(start: 1, count: 4)
rangeObserver.subscribe(onNext: { print($0) }, onError: nil, onCompleted: { print("Range completed") }, onDisposed: nil)
// result
1
2
3
4
Range completed
let emptyObserver = Observable<Void>.empty()
emptyObserver.subscribe(onNext: { print($0) }, onError: nil, onCompleted: { print("Empty finished") }, onDisposed: nil)
// result
Empty finished
let neverObserver = Observable<Any>.never()
neverObserver.subscribe(onNext: { print($0) }, onError: nil, onCompleted: { print("Completed") }, onDisposed: nil)
var toggle = false
let factory = Observable.deferred { () -> Observable<Int> in
toggle = !toggle
return toggle ? Observable.of(1, 2, 3) : Observable.of(3, 4, 5)
}
for _ in 0...3 {
factory.subscribe { (event) in
print(event.element ?? event)
}
}
// result
1
2
3
completed
3
4
5
completed
1
2
3
completed
3
4
5
completed
그리고 RxSwift의 경우 Event를 들어가서 확인해보면
1
2
3
4
5
6
7
8
9
10
11
12
13
extension Event: CustomDebugStringConvertible {
/// Description of event.
public var debugDescription: String {
switch self {
case let .next(value):
"next(\(value))"
case let .error(error):
"error(\(error))"
case .completed:
"completed"
}
}
}
이렇게 작업이 완료되었을때 completed가 뜬다.
4. 핵심 요약
subscribe: Observable이 방출하는next,error,completed이벤트를 수신하여 처리range: 지정된 범위의 정수를 순차 방출empty: 데이터 없이 즉시 완료 신호만 방출never: 아무 이벤트도 방출하지 않고 종료되지 않는 상태 유지deferred: 구독 시점마다 상황에 맞는 Observable을 동적으로 생성
Disposing
Disposing은 Subscription(구독)을 취소하고 메모리에서 해제하는 과정이다. Observable이 완료되더라도 구독을 적절히 해제하지 않으면 메모리 누수(Memory Leak)가 발생한다.
1. Dispose 개념 및 리소스 해제 (dispose())
- 개념: 구독을 중단하고 관련 리소스를 정해진 시점에 즉시 해제한다.
- 사용법: 구독 객체에서 직접
.dispose()를 호출한다. - 이벤트 처리 시점:
subscribe편의 메서드의onDisposed클로저를 사용하면 구독이 해제되는 시점에 실행할 로직을 정의할 수 있다.
1
2
3
4
5
let rangeObserver = Observable.range(start: 1, count: 4)
let subscription = rangeObserver.subscribe(onNext: { print($0) }, onError: nil, onCompleted: { print("range completed") }, onDisposed: { print("range disposed") })
subscription.dispose()
2. DisposeBag (구독 가방)
- 개념: 여러 개의 Subscription(Disposable)을 하나로 모아 관리하는 객체이다.
- 동작 원리: DisposeBag 인스턴스가 메모리에서 해제(deinit)될 때, 가방에 담긴 모든 구독의
.dispose()를 자동으로 호출한다. (Swift의 Deinit 및 스코프 기반 메모리 관리 방식과 유사) - 사용법: 구독 선언 뒤에
.disposed(by: disposeBag)을 붙여 가방에 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
func range() {
let rangeObserver = Observable.range(start: 1, count: 4)
let bag = DisposeBag()
let subscription = rangeObserver.subscribe(onNext: { print($0) }, onError: nil, onCompleted: { print("range completed") }, onDisposed: { print("range disposed") })
subscription.disposed(by: bag)
}
range()
3. Observable의 종료와 메모리 누수 원인
- 자동 종료 조건: Observable이
.completed또는.error이벤트를 방출하면 시퀀스가 끝나고 더 이상 다음 이벤트를 방출하지 않는다. - 메모리 누수 발생 조건:
completed나error없이 이벤트가 계속 지속되는 경우 (예:never()또는 계속 열려 있는 스트림)- 구독 시
.disposed(by: bag)이나.dispose()를 호출하지 않아 수동 해제조차 되지 않는 경우
- 해결책: 종료되지 않는 스트림이라도
DisposeBag에 등록해 두면, 해당 스코프(함수, 클래스 등)가 종료되어DisposeBag이 메모리에서 해제될 때 함께 안전하게 구독이 해제된다.
아래 after 예제는
Observable.create클로저가 동기적으로 즉시 실행되고 끝나기 때문에, 실제로 Instruments 등으로 찍어보면 메모리를 계속 붙잡고 있는 상태는 아니다. 다만 완료 이벤트도 없고 dispose 수단도 없는 구조 자체가, 실전에서 Timer나 네트워크 콜백처럼 오래 살아있는 비동기 스트림을 만났을 때 그대로 누수로 이어지는 패턴이라 이렇게 부른다.
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// before - error로 종료되는 스트림 + bag 등록
func memoryLeak() {
enum NumError: Error {
case invalidNumber
}
let bag = DisposeBag()
Observable<Int>.create { (observer) -> Disposable in
observer.onNext(1)
observer.onError(NumError.invalidNumber)
observer.onNext(2)
observer.onCompleted()
observer.onNext(3)
return Disposables.create()
}.subscribe(
onNext: { print($0) },
onError: { print("Error", $0) },
onCompleted: { print("observable completed") },
onDisposed: { print("Subscription disposed") }
)
.disposed(by: bag)
}
memoryLeak()
// 출력:
// 1
// Error invalidNumber
// Subscription disposed
// -> onError 이후 onNext(2), onCompleted(), onNext(3)은 전부 무시됨
// after - onError 삭제, bag도 삭제 (진짜 leak)
func memoryLeakAfter() {
enum NumError: Error {
case invalidNumber
}
Observable<Int>.create { (observer) -> Disposable in
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
// onError, onCompleted 없음 -> 시퀀스가 절대 안 끝남
return Disposables.create()
}.subscribe(
onNext: { print($0) },
onError: { print("Error", $0) },
onCompleted: { print("observable completed") },
onDisposed: { print("Subscription disposed") }
)
// .disposed(by: bag) 없음 -> 수동 해제 수단도 없음
}
memoryLeakAfter()
// 출력:
// 1
// 2
// 3
// -> onCompleted, onDisposed 둘 다 안 찍힘
4. Observable.create 사용 시 주의점
Observable.create블록을 통해 직접 Observable을 생성할 때는 클로저의 마지막 반환값으로Disposables.create { ... }형태의 Disposable을 반드시 반환해야 한다.
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
func timerObservable() -> Observable<Int> {
return Observable<Int>.create { observer in
var count = 0
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
observer.onNext(count)
count += 1
}
// 구독이 끊길 때 (dispose 호출 시) 이 클로저가 실행되면서 타이머도 같이 정리된다
return Disposables.create {
timer.invalidate()
print("Timer invalidated")
}
}
}
let bag = DisposeBag()
timerObservable()
.subscribe(onNext: { print($0) })
.disposed(by: bag)
// 5초 뒤 bag을 비우면 dispose가 호출되고, 위에서 등록한 Disposables.create의
// 클로저(timer.invalidate())가 실행되면서 타이머 리소스가 정리된다
5. 핵심 요약
dispose(): 명시적으로 즉시 구독을 취소하고 해제할 때 사용DisposeBag: 객체의 생명주기에 맞춰 구독을 자동으로 일괄 해제할 때 사용 (권장)- 메모리 관리 규칙: 모든 구독(
subscribe) 끝에는 반드시.disposed(by: bag)을 붙여 메모리 누수를 방지한다.
Side Effect
Side Effect는 Observable이 방출하는 이벤트 자체에는 영향을 주지 않으면서, 이벤트가 흘러가는 중간에 별도의 부가 작업을 수행하는 것을 말한다. do 연산자를 통해 구현하며, RxSwift에서 Observable에 적용하는 모든 메서드는 연산자(Operator)라고 부른다.
1. do 연산자
- 개념: 이벤트가 실제 구독자(Subscriber)에게 전달되기 전, 각 이벤트 시점(next, error, completed, subscribe, subscribed, dispose)에 별도의 부가 작업을 끼워 넣을 수 있다.
- 주의점:
do블록 안에서 이벤트 값을 읽을 수는 있지만, 그 값을 직접 변경해서 다음 단계로 전달할 수는 없다. 값 자체는 그대로 구독자에게 전달된다. - 실행 순서:
do에 등록한 클로저가subscribe의 동일한 이벤트 클로저보다 먼저 실행된다. 단, dispose 시점은 반대로subscribe의onDisposed가 먼저 실행되고,do의onDispose가 마지막에 실행된다.
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
35
36
37
38
39
40
func sideEffects() {
let rangeObserver = Observable.range(start: 1, count: 4)
let bag = DisposeBag()
rangeObserver.do(
onNext: { print("Will next", $0) },
onError: nil,
onCompleted: { print("Will Complete Range") },
onSubscribe: { print("Will subscribe") },
onSubscribed: { print("Did subscribe") },
onDispose: { print("Did Dispose subscription") })
.subscribe(
onNext: { print($0) },
onError: nil,
onCompleted: { print("Range completed") },
onDisposed: { print("Subscription disposed") })
.disposed(by: bag)
}
sideEffects()
// result
Will subscribe
Did subscribe
Will next 1
1
Will next 2
2
Will next 3
3
Will next 4
4
Will Complete Range
Range completed
Subscription disposed
Did Dispose subscription
정리하면, do의 각 이벤트 클로저는 해당 이벤트가 구독자에게 도달하기 직전에 먼저 실행되고, dispose만 예외적으로 구독자 쪽(onDisposed)이 먼저 실행된 뒤 do의 onDispose가 마지막에 실행된다.
do는 디버깅 목적으로도 자주 쓰이지만 디버깅 전용은 아니고, 로깅이나 상태 갱신처럼 이벤트 흐름에 영향을 주지 않는 부가 작업 전반에 사용할 수 있다.
2. debug 연산자
- 개념:
do로 매번 로그를 직접 작성하는 대신,debug(_:)를 붙이면 구독 시점부터 dispose 시점까지의 모든 이벤트를 자동으로 콘솔에 출력해준다. - 사용법: 식별자(identifier) 문자열을 인자로 넘기면, 로그 앞에 해당 식별자가 붙어서 여러 스트림을 동시에 디버깅할 때 구분하기 쉽다.
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
func sideEffects() {
let rangeObserver = Observable.range(start: 1, count: 4)
let bag = DisposeBag()
rangeObserver.debug("Range Debug", trimOutput: true).subscribe(onNext: { print($0) }, onError: nil, onCompleted: { print("Range completed") }, onDisposed: { print("Subscription disposed") }).disposed(by: bag)
}
sideEffects()
// result
2026-08-20 04:42:36.731: Range Debug -> subscribed
2026-08-20 04:42:36.731: Range Debug -> Event next(1)
1
2026-08-20 04:42:36.731: Range Debug -> Event next(2)
2
2026-08-20 04:42:36.731: Range Debug -> Event next(3)
3
2026-08-20 04:42:36.731: Range Debug -> Event next(4)
4
2026-08-20 04:42:36.731: Range Debug -> Event completed
Range completed
Subscription disposed
2026-08-20 04:42:36.732: Range Debug -> isDisposed
debug는 do처럼 이벤트마다 클로저를 일일이 작성할 필요 없이, 스트림에 한 줄만 추가하면 구독 시작부터 dispose까지 전 과정을 자동으로 로깅해주는 게 핵심 차이다.
3. 핵심 요약
do: 이벤트 흐름에 영향을 주지 않으면서 특정 시점(next, error, completed, subscribe, subscribed, dispose)에 부가 작업을 끼워 넣을 때 사용debug: 별도 로직 작성 없이 식별자만 붙이면 구독 전체 과정을 자동으로 로깅해주는 디버깅 전용 연산자- 실행 순서 차이: 이벤트/완료는
do가subscribe보다 먼저, dispose는subscribe의onDisposed가do의onDispose보다 먼저 실행된다
RxSwift vs Combine 비교
지금까지 다룬 개념을 기준으로 RxSwift와 Combine을 비교하면 다음과 같다.
| 구분 | RxSwift | Combine |
|---|---|---|
| 성격 | Library (내가 필요할 때 호출) | Framework (Apple 시스템이 호출) |
| 설치 | 별도 설치 필요 (SPM, CocoaPods 등) | iOS 13+ 기본 내장, 설치 불필요 |
| 데이터 스트림 | Observable | Publisher |
| 값 하나만 방출 | Observable.just | Just |
| 여러 값 순차 방출 | Observable.of, Observable.from | Sequence (배열 등에서 .publisher) |
| 구독 | subscribe(onNext:onError:onCompleted:onDisposed:) | sink(receiveCompletion:receiveValue:) |
| 이벤트 종류 | next / error / completed (3종) | .value / .finished / .failure (completion 안에 finished/failure 통합) |
| 구독 해제 객체 | Disposable | AnyCancellable |
| 해제 방식 | 명시적 dispose() 호출 필요, 안 하면 계속 살아있음 | AnyCancellable의 deinit 시 자동 cancel() |
| 여러 구독 일괄 관리 | DisposeBag (담고 있는 인스턴스가 메모리에서 해제될 때 자동 dispose) | Set<AnyCancellable> (담고 있는 인스턴스가 메모리에서 해제될 때 자동 cancel, 원리는 동일) |
| 핸들 미보관 시 동작 | 구독이 계속 살아있음 (leak 위험) | 즉시 cancel() 되어 조기 종료됨 (leak 아님) |
| 부가 작업/디버깅 | do 연산자, debug 연산자 | handleEvents, print() |
| 커스텀 스트림 생성 | Observable.create { observer in ... return Disposables.create { } } | Publisher + Subscription 프로토콜 직접 구현 |
가장 헷갈렸던 지점은 구독을 안 붙잡고 있을 때의 동작 차이다. Combine은 AnyCancellable을 저장하지 않으면 곧바로 deinit되면서 스트림이 취소되지만, RxSwift는 Disposable을 저장하지 않아도 명시적으로 dispose()를 호출하기 전까지 구독이 계속 살아있다. 그래서 RxSwift에서는 DisposeBag으로 관리하는 습관이 훨씬 중요하다.
코드
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// RxSwift
import RxSwift
let bag = DisposeBag()
let rxObservable = Observable<Int>.create { observer in
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
observer.onCompleted()
return Disposables.create {
print("Rx: dispose 호출됨")
}
}
rxObservable
.subscribe(
onNext: { print("Rx onNext:", $0) },
onError: { print("Rx onError:", $0) },
onCompleted: { print("Rx onCompleted") },
onDisposed: { print("Rx onDisposed") }
)
.disposed(by: bag)
// 출력:
// Rx onNext: 1
// Rx onNext: 2
// Rx onNext: 3
// Rx onCompleted
// Rx onDisposed
// Rx: dispose 호출됨
// Combine
import Combine
var cancellables = Set<AnyCancellable>()
let combinePublisher = PassthroughSubject<Int, Never>()
combinePublisher
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Combine finished")
case .failure(let error):
print("Combine error:", error)
}
},
receiveValue: { print("Combine value:", $0) }
)
.store(in: &cancellables)
combinePublisher.send(1)
combinePublisher.send(2)
combinePublisher.send(3)
combinePublisher.send(completion: .finished)
// 출력:
// Combine value: 1
// Combine value: 2
// Combine value: 3
// Combine finished
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 핸들 미보관 시 동작 차이 비교
// RxSwift - dispose 안 하면 계속 살아있음
func rxWithoutDispose() {
Observable<Int>.create { observer in
observer.onNext(1)
return Disposables.create()
}
.subscribe(onNext: { print("Rx:", $0) })
// .disposed(by:) 없음 -> 구독은 살아있지만 참조할 방법이 사라짐
}
// Combine - 저장 안 하면 곧바로 취소됨
func combineWithoutStore() {
let subject = PassthroughSubject<Int, Never>()
subject.sink(receiveCompletion: { _ in },
receiveValue: { print("Combine:", $0) })
// .store(in:) 없음 -> sink가 리턴한 AnyCancellable이 바로 dealloc되며 cancel() 호출됨
subject.send(1) // 이미 취소되어 값이 출력되지 않음
}

