
내용 정리
Trait은 옵저버블 중에서도 특별한 상황에 맞게 제공되는 옵저버블을 의미한다. RxSwift에서Trait은 코드의 의도를 명확히 하기 위해 사용된다.
Trait의 종류와 특징에 대해 학습하고, 어떻게 사용할 수 있을지 공부해보자.
Single은 오직 하나의 값만을 방출하는 옵저버블이다. 하나의 값을 방출하거나 오류를 방출하면 스트림이 종료된다.
주로 비동기 네트워크 요청이나 파일 읽기 등의 작업에 사용된다.
Single은 아래의 두 가지 타입을 가진다.
onSuccess: 옵저버블의 onNext와 같은 개념onFailure: 옵저버블의 onError와 같은 개념func fetchUser() -> Single<User> {
return Single.create { single in
// 비동기 작업
API.fetchUser { user, error in
if let user = user {
single(.success(user))
} else if let error = error {
single(.failure(error))
}
}
return Disposables.create()
}
}
Maybe는 하나의 값을 방출하거나, 값을 방출하지 않고 그냥 complete 되거나, 에러를 방출하는 옵저버블이다.
약간 옵셔널 같은 느낌이다. 이 옵저버블은 값을 방출할 수도 있고 방출하지 않을 수도 있다...
이런 특징 덕분에 주로 캐싱된 데이터를 반환하거나 값이 없을 수도 있는 경우 Maybe를 사용한다.
Maybe는 아래의 3가지 타입을 가진다.
onSuccess: 옵저버블의 onNext와 같은 개념onError: 옵저버블의 onError와 같은 개념onCompleted: 옵저버블의 onCompleted와 같은 개념func fetchCachedData() -> Maybe<Data> {
return Maybe.create { maybe in
let data = getCachedData()
if let data = data {
maybe(.success(data))
} else {
maybe(.completed)
}
return Disposables.create()
}
}
completable은 값은 방출하지 않고 무언가 완료되는 시점만 알 수 있는 옵저버블이다. 이 옵저버블을 사용하면 작업이 완료 되었는지 에러가 발생했는지, 두 가지 정보만 알 수 있다.
completable은 주로 완료 여부만 중요한 작업, 예를 들어 파일을 저장하는 등의 작업에서 주로 사용된다.
completable은 아래의 두 가지 타입을 가진다.
onCompleted: 옵저버블의 onCompleted와 같은 개념onError: 옵저버블의 onError와 같은 개념func saveData(data: Data) -> Completable {
return Completable.create { completable in
do {
try saveToDisk(data)
completable(.completed)
} catch {
completable(.error(error))
}
return Disposables.create()
}
}
내용 정리
Operator는 RxSwift에서 제공하는 연산자이다. 타입을 바꾸거나 조작하는 핵심 도구로, 옵저버블의 체인의 중간에서 사용된다.
map은 스트림에서 방출되는 값을 변형시키는 연산자이다.
예를 들어 스트림에서 1이라는 데이터가 방출되었을 때, map 연산자가 map(x = 10 * x)라면 최종적으로 방출되는 결과 값은 10이 된다.
이런 식으로 데이터를 변형시킬 수 있는 것이 map 연산자이다.
let observable = Observable.of(1, 2, 3)
observable
.map { $0 * 2 } // 각 요소를 두 배로 변환
.subscribe(onNext: { print($0) })
그런데 map처럼 데이터를 변환하는 다른 연산자가 있다.
flatMap이라는 연산자이다.
먼저 flatMap의 사용법을 보자
let observable = Observable.of(1, 2, 3)
observable
.flatMap { number in
return Observable.just(number * 2) // 각 요소를 Observable로 변환
}
.subscribe(onNext: { print($0) })
위에서 확인한 map과 차이가 없다.
정말 차이가 없다면 왜 연산자를 두 개나 만들었을까? 당연히 서로 동작 방식과 사용처가 다르기 때문이다.
코드의 주석을 읽었다면 눈치 챘을 수도 있겠지만 map과 flatMap은 각각 아래와 같은 특징을 가진다.
1. map
2. flatMap
글로만 읽으면 이해가 어려울 수 있다.
위의 내용을 시각화 한다면 아래처럼 표현할 수 있다.
Observable(1, 2, 3) --[map]--> Observable(2, 4, 6)
Observable(1, 2, 3) --[flatMap]--> Observable(Observable(2), Observable(4), Observable(6))
\----> 병합된 Observable(2, 4, 6)
정리하자면 map은 단일 값 변화를 위해 사용하기에 적합하고, flatMap은 비동기 작업이나 옵저버블의 변환이 필요한 경우에 적합하다.
| 특징 | map | flatMap |
|---|---|---|
| 반환 방식 | 요소를 단순히 변환 | 요소를 옵저버블로 변환하고 병합 |
| 반환 타입 | 변환된 요소가 포함된 단일 옵저버블 | 여러 옵저버블을 병합한 단일 옵저버블 |
| 주요 사용 | 단순 데이터 변환 | 비동기 작업 처리, 네트워크 요청 |
| 동작 예 | 1 -> 2 | 1 -> Observable(2) |
zip은 두 개의 스트림에서 나오는 값을 한 쌍씩 묶어서 방출하는 것이다. 즉, zip을 사용하기 위해서는 스트림이 2개가 되어야 하기 때문에 옵저버블이 2개여야 한다.
사용 예시는 아래와 같다
let observable1 = Observable.of(1, 2, 3)
let observable2 = Observable.of("a", "b", "c", "d")
Observable.zip(
observable1,
observable2
).subscribe(onNext: { print($0) })
/*
출력:
(1, "a")
(2, "b")
(3, "c")
*/
위의 코드에서 observable1는 요소가 3개이고 observable2는 요소가 4개인데, 결과 값은 3개의 요소만 출력된 것을 볼 수 있다.
이처럼 zip은 짝이 맞춰줘야만 값을 방출하기 때문에 특정 옵저버블이 요소가 더 많을 경우 더 적은 옵저버블의 수에 맞춰 방출된다.
filter는 특정 조건에 맞는 항목만 방출할 수 있도록 도와주는 연산자이다.
let numbers = Observable.of(1, 2, 3, 4, 5)
numbers
.filter { $0 % 2 == 0 } // 짝수만 필터링
.subscribe(onNext: { print($0) })
여러 옵저버블을 결합하는 연산자로 merge와 combineLatest가 있다.
먼저 merge의 예시를 보자
let observable1 = Observable.of(1, 2, 3)
let observable2 = Observable.of(4, 5, 6)
Observable.merge(observable1, observable2)
.subscribe(onNext: { print($0) })
// 출력: 1, 4, 2, 5, 3, 6
merge는 여러 옵저버블을 결합하여 각 옵저버블이 방출하는 값을 시간 순서대로 방출한다. 때문에 어떤 옵저버블을 먼저 넣었는지 순서는 무관하다.
주의할 점은 병합하는 옵저버블의 데이터 타입이 모두 동일해야 한다는 점이다.
let observable1 = Observable.of(1, 2, 3, 4, 5)
let observable2 = Observable.of("A", "B", "C")
Observable.combineLatest(observable1, observable2) { number, letter in
return "\(number)\(letter)"
}
.subscribe(onNext: { print($0) })
// 출력: 1A, 2A, 2B, 3B, 3C, 4C, 5C
combineLatest는 코드로 봐도 헷갈릴 수 있는데, 여러 옵저버블에서 가장 최근에 방출된 값을 결합하여 새로운 값을 생성하는 연산자이다. 때문에 최소 한 개 이상의 옵저버블이 값을 방출해야만 결과를 생성할 수 있다.
위의 코드를 보면 observable1은 5가지 요소를 가지고, observable2는 3가지 요소를 가진다. 얼핏 보면 zip과 비슷해 보이지만, 결과가 다른 것을 볼 수 있다.
combineLatest는 가장 최근에 방출한 값을 기준으로 병합하기 때문에 observable2가 마지막에 방출한 C라는 값을 계속 병합하게 되고, 때문에 출력 결과에 4C, 5C와 같은 형태로 출력되는 것이다.
merge는 여러 옵저버블의 값을 그대로 병합할 때 사용하고, combineLatest는 여러 옵저버블의 값을 결합하여 새로운 값을 생성할 때 사용하면 좋다.
concat은 여러 옵저버블을 순차적으로 연결하여 처리해주는 연산자이다. 앞의 옵저버블이 완료된 후 다음 옵저버블이 실행되도록 하는 역할이다.
즉, concat을 사용하면 여러 옵저버블을 연결할 수 있고, 먼저 실행되는 옵저버블이 끝날 때까지 다음 옵저버블이 실행되지 않으므로 순서가 보장된다.
주의할 점은 연결하려는 옵저버블의 데이터 타입이 모두 동일해야 한다는 점이다.
let observable1 = Observable.of(1, 2, 3)
let observable2 = Observable.of(4, 5, 6)
Observable.concat(observable1, observable2)
.subscribe(onNext: { print($0) })
// 출력: 1, 2, 3, 4, 5, 6
concat은 여러 비동기 작업을 순차적으로 실행해야 하거나 데이터 흐름이 순서를 유지해야할 때 유용하게 사용할 수 있다.
withLatestFrom은 주 옵저버블이 값을 방출할 때, 보조 옵저버블의 최신 값을 결합하여 방출해주는 연산자이다.
이 연산자는 주 옵저버블이 값을 방출할 때마다 동작하며, 이 때 보조 옵저버블의 최신 값을 제공 받아 두 값을 결합하여 방출한다.
let trigger = PublishSubject<Void>()
let data = PublishSubject<String>()
trigger
.withLatestFrom(data)
.subscribe(onNext: { print($0) })
data.onNext("A") // 보조 Observable이 값을 방출
data.onNext("B") // 보조 Observable이 값을 방출
trigger.onNext(()) // 트리거 Observable이 방출, 최신값 "B" 사용
trigger.onNext(()) // 트리거 Observable이 방출, 최신값 "B" 사용
// 출력: B, B
withLatestFrom은 UI 이벤트(버튼 클릭 등)를 트리거로 사용하면서 최신 데이터를 가져오고 싶을 때나 별도의 액션이 발생할 때 데이터 스트림을 결합하고 싶은 경우 사용하면 좋다.
share는 이름의 뜻처럼 옵저버블의 이벤트를 공유하여, 여러 구독자가 동일한 데이터 스트림을 사용할 수 있도록 한다.
이 연산자를 사용하면 이벤트 생성 비용이 줄어 리소스를 절약할 수 있고, 여러 구독자가 동일한 이벤트 스트림을 공유하게 된다.
기본적으로 share(replay: 0)를 사용하며, 구독 시점 이후에만 이벤트를 방출하는 특징을 가진다.
let observable = Observable<Int>.create { observer in
print("New Subscription")
observer.onNext(1)
observer.onNext(2)
observer.onCompleted()
return Disposables.create()
}
let shared = observable.share()
shared
.subscribe(onNext: { print("Subscriber 1: \($0)") })
shared
.subscribe(onNext: { print("Subscriber 2: \($0)") })
/*
출력:
New Subscription
Subscriber 1: 1
Subscriber 1: 2
Subscriber 2: 1
Subscriber 2: 2
*/
만약 share로 이벤트 스트림을 공유하지 않을 경우에는 구독자마다 새로운 스트림이 생성되어 동일한 이벤트가 여러번 실행되게 된다.
옵저버블을 사용하며 유용한 기능을 제공하는 연산자들도 있다. 대표적인 예시로 delay는 이름처럼 특정 작업을 진행하는데 딜레이를 줄 수 있다.
let observable = Observable.of(1, 2, 3)
observable
.delay(.seconds(2), scheduler: MainScheduler.instance)
.subscribe(onNext: { print($0) })
위와 같이 구현하면 2초 뒤에 아래의 작업들이 진행된다.
여기서 scheduler라는 것이 등장했는데 이것은 무엇일까?
이제부터 같이 알아보자
내용 정리
Scheduler는 옵저버블 스트림에서 스레드 또는 큐를 지정할 수 있도록 돕는 도구이다. RxSwift는 기본적으로 비동기 작업을 처리하기 때문에 상황에 따라 적절한Scheduler를 설정하는 것이 중요하다.
Scheduler의 종류는 아래와 같다.
MainScheduler
ConcurrentDispatchQueueScheduler
DispatchQueue 기반의 스케줄러이다.qos를 설정하여 우선순위를 정해줄 수 있다.SerialDispatchQueueScheduler
DispatchQueue에서 직렬 작업을 실행하는 스케줄러이다.OperationQueueScheduler
OperationQueue 기반 작업에 사용되며 병렬 작업을 수행할 수 있다.qos를 설정하여 우선순위를 정해줄 수 있다.subscribeOn과 observeOn은 스케줄러를 사용하여 옵저버블이 처리되어야 할 스레드를 제어하기 위해 사용된다.
subscribeOn은 옵저버블의 생성 및 이벤트 방출이 실행될 스레드를 지정한다.
이 때, 호출 위치와 관계없이 항상 상류(upstream)에서 작동된다.
옵저버블의 초기화와 이벤트 생성이 지정된 스레드에서 실행되며, 일반적으로 데이터 처리, 네트워크 요청 등 비동기 작업을 백그라운드 스레드에서 실행하기 위해 사용한다.
// subscribeOn 사용 예시
Observable.of(1, 2, 3)
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) // 백그라운드에서 작업 시작
.subscribe(onNext: { print($0) })
observeOn은 옵저버블의 이벤트를 처리하는 스레드를 지정한다.
구독자가 이벤트를 수신하고 처리하는 스레드를 조작하며, 호출 위치 이후의 모든 작업에 영향을 미치게 된다.
UI 업데이트를 메인 스레드에서 처리하는데 주로 사용되며, 데이터 변환 후 결과를 특정 스레드에서 처리하기에 적합하다.
// observeOn 사용 예시
Observable.of(1, 2, 3)
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) // 백그라운드에서 작업 시작
.observeOn(MainScheduler.instance) // 메인 스레드에서 결과 처리
.subscribe(onNext: { print($0) })
오늘은 지난 시간에 이어 RxSwift에 대해 더 자세히 공부해 보았다.
어떻게 된건지 공부하면 할 수록 공부할게 더 늘어난다...
내용도 너무 많고 어려워서 언제 익숙해질 수 있을지 모르겠다.
그래도 열심히 해봐야지...