본 게시물은 곰튀김님의 유튜브 강의 RxSwift 4시간 끝내기를 참고해 작성했습니다.
RxSwift에 대해 전반적인 개념을 이해하고, 코드에 빠르게 적용하기 위한 목적입니다.
1교시 : RxSwift를 이용한 비동기 프로그래밍
* RxSwift 공식 사이트 : http://reactivex.io/
ReactiveX
CROSS-PLATFORM Available for idiomatic Java, Scala, C#, C++, Clojure, JavaScript, Python, Groovy, JRuby, and others
reactivex.io
RxSwift는 MS사의 ReactiveX 시리즈 중의 하나로, RxJava, RxPY등 다양한 언어에서 지원을 하고 있다.
따라서 RxSwift를 익혀두면 다른 언어에서도 ReactiveX를 쉽게 적용할 수 있다.
RxSwift를 사용하는 이유
RxSwift 란 ? Observable stream을 이용한 비동기 프로그래밍 API
RxSwift는 비동기 프로그래밍 API이다.
그런데 비동기 프로그래밍은 swift에서도 지원하고 있고, PromiseKIt 라이브러리도 있다.
그런데 RxSwift 를 사용해야 하는 이유는 바로 2가지가 있다.
1) 기본으로 제공하는 비동기 코드보다 손쉽게 비동기 프로그래밍을 할 수 있다.
2) PromiseKit 라이브러리와 다르게 Operator를 사용한다.
그러면 코드를 통해 일반 코드랑 RxSwift 의 차이점을 알아보자.
- 기본 제공 : Sync/Async
- Sync : 하나의 작업만 실행
- Async : 동시에 두 개 이상의 작업을 실행
[예제] URL을 통해 이미지를 받아오는 작업과 타이머를 실행시키는 두 가지 작업을 실행한다.
- 동기 방식의 경우 타이머를 실행하고 이미지를 불러오면 타이머가 멈춘 뒤 syncLoadImage() 코드 실행이 완료되면 타이머가 다시 시작된다.
func syncLoadImage(from imageUrl: String) -> UIImage? { guard let url = URL(string: imageUrl) else { return nil } guard let data = try? Data(contentsOf: url) else { return nil } let image = UIImage(data: data) return image }
- 비동기 방식의 경우 타이머를 실행 중에 이미지를 불러와도 타이머가 멈추지 않는다.
-> 이미지를 불러오는 작업을 별도 쓰레드로 처리해주기 때문.
func asyncLoadImage(from imageUrl: String, completed: @escaping (UIImage?) -> Void) { DispatchQueue.global().async { let image = syncLoadImage(from: imageUrl) completed(image) } }
비동기 방식을 swift로 구현하기 위해서는 GCD (Grand Central Dispatch) 라는 개념을 알아야 한다.
GCD는 queue에 넣은 작업을 스레드로 적절하게 분배해주는 역할을 한다.
* DispatchQueue가 GCD에서 사용하는 queue이고,
* global()은 queue의 종류이다.
* sync, async를 선택해서 사용할 수 있다.
일반적으로 코드는 main thread에서 실행된다(특히 UIKit 관련된 코드는 메인 쓰레드에서 실행되지 않으면 오류가 발생).
그리고 이렇게 async로 빠진 코드는 background thread에서 실행된다.
sync를 사용하면 해당 블록을 실행하는 동안 main thread 작업은 실행되지 않는다.
큐의 종류는 global(), main()이 있다.
global()은 concurrent 특성, 그러니까 큐에 먼저 들어가더라도 먼저 나오지 않을 수 있는 특성을 갖는다. 작업량이 적으면 일찍 나옴.
main() 은 반드시 들어간 순서대로 나온다. - RxSwift
* RxSwift 의 주요 기능 5가지 : Observable, operator, scheduler, subject, (single..별로 안 중요함)
step 1
- Observable : RxSwift에서 가장 핵심적인 개념
RxSwift의 Observable 객체를 생성하고, 그 객체에서 이벤트가 생성된다. 이 모든 과정은 비동기적으로 이루어진다.
- Subscribe : RxSwift의 Observer는 Observable 객체를 Subscribe(구독)하고, 객체에서 생성된 값에 Observer는 반응한다.
-> 그러니까 Subscribe라는거는 Observer가 Observable 객체를 관찰하고 반응하겠다는 의미로 해석된다.
-> Observable 객체는 구독되기 전에는 아무런 이벤트를 발생시키지 않고 그저 정의되어있을 뿐이다.
-> Observer는 코드에 직접적으로 나타나지는 않지만 subscribe과 연결되어 반응하고 관찰하는.. 중요한 주체이다.
- Observable의 3가지 행동규칙
1) next : Observable은 next를 통해 행동(이벤트)를 발행한다.
2) error : 에러가 발생하면 subscribe가 종료(dispose)된다.
3) complete : next를 통해 모든 값이 발행되면 complete 가 호출되면 subscribe가 종료된다.
-> 중간에 error가 발생하면 complete는 호출되지 않는다!
=> 행동 규칙은 switch문을 통해, 혹은 개별 인자를 통해 구현될 수 있다. (2가지 방법이 있는 것)
이제 코드로 예제를 보자. 일단 create를 통해 Observable 객체를 생성해준다.
func rxswiftLoadImage(from imageUrl: String) -> Observable<UIImage?> { return Observable.create { seal in asyncLoadImage(from: imageUrl) { image in seal.onNext(image) seal.onCompleted() } return Disposables.create() } }
반환되는 Observerbale 객체를 subscribe()메소드로 구독한다.
(observeOn은 스케쥴러 지정 메소드인데, 다음 교시에 학습할 예정)
그러면 이제 Observable이 발행한 이벤트를 클로져의 result에 받아서 코드 블로 내부의 행동 규칙을 실행한다.
그리고 이렇게 메소드들이 연결되어 흘러가는 것을 "stream" 이라고 정의한다.
- Disposable : Subscribe 종료
모든 이벤트를 발행한 Observable의 stream은 Subscribe을 종료해야 한다. 3가지 방법이 있다.
1) disposable 사용 : Disposable 객체를 변수로 받아서 dispose() 메소드로 취소하기
var dispose: Disposable? @IBAction func onLoadImage(_ sender: Any) { imageView.image = nil dispose = rxswiftLoadImage(from: LARGER_IMAGE_URL) .observeOn(MainScheduler.instance) .subscribe({ result in switch result { case let .next(image): self.imageView.image = image case let .error(err): print(err.localizedDescription) case .completed: break } }) } @IBAction func onCancel(_ sender: Any) { dispose?.dispose() }
2. disposeBag 사용 : DisposeBag() 객체를 이용해 여러 개의 Subscribe에 대한 구독 취소 가능
-> dispose() 메소드를 지원하지 않으므로, 새로운 DisposeBag() 객체를 할당해줌으로써 cancel 기능 구현
3. disposed() : insert하지 않도 메소드를 stream에 연결해 구현, 코드는 위에 Observable 예제로 대체함var dispose: Disposable? var disposeBag: DisposeBag = DisposeBag() @IBAction func onLoadImage(_ sender: Any) { imageView.image = nil dispose = rxswiftLoadImage(from: LARGER_IMAGE_URL) .observeOn(MainScheduler.instance) .subscribe({ result in switch result { case let .next(image): self.imageView.image = image case let .error(err): print(err.localizedDescription) case .completed: break } }) disposeBag.insert(dispose) } @IBAction func onCancel(_ sender: Any) { disposeBag = DisposeBag() }
-> 일반적으로 쓰이는 코드 형식인 것 같다.
step 2 Operators
: Observable에는 많은 연산자가 존재한다. 그리고 공식 사이트에서 친절하게 설명해주고 있다.
그런데 연산자를 설명할 때 아래와 같은 도식화된 그림을 사용하므로 읽는 법을 익혀두면 공식사이트에서 연산자 설명 읽을 수 있다.
그림 읽는법
- 화살표 : stream을 의미
- 박스 : operator를 의미
- 도형들 : Observable 객체에서 생성된 데이터들
- x표시 : 에러를 의미
- | 표시 : completed를 의미
연산자(메소드) 몇 가지를 살펴보자.
1. 생성 operator : Observable 객체를 만들어 냄
-> 이벤트를 발생시키기 위해서는 Create(Observable 객체 return) -> Subscribe() -> onNext()로 이벤트를 발생시키는 과정이 필수적인데, 이를 간단하게 create 해주는 메소드들이 존재한다.
1) just
: "Hello World"로 문자열이 그대로 출력된다.
Observable을 따로 생성하거나 completed 행동 규칙을 따로 설정해줄 필요 없이 간결한 코드로 출력이 가능하다.
@IBAction func exJust1() {
Observable.just("Hello World")
.subscribe(onNext: { str in
print(str)
})
.disposed(by: disposeBag)
}
: 파라미터로 배열이 들어가면 배열이 '그대로' 출력된다.
@IBAction func exJust2() {
Observable.just(["Hello", "World"])
.subscribe(onNext: { arr in
print(arr)
})
.disposed(by: disposeBag)
}
2) from
: just와 다르게 array의 요소들이 한 줄에 하나씩 출력된다.
@IBAction func exFrom1() {
Observable.from(["RxSwift", "In", "4", "Hours"])
.subscribe(onNext: { str in
print(str)
})
.disposed(by: disposeBag)
}
2. map : 변환 operator
: stream을 따라 위에서 데이터를 받아 변경(mapping)한 후 아래로 내려보낸다.
@IBAction func exMap1() {
Observable.just("Hello")
.map { str in "\(str) RxSwift" }
.subscribe(onNext: { str in
print(str)
})
.disposed(by: disposeBag)
}
: array의 요소를 받아 문자열의 count인 int로 맵핑해준다. 각 줄에 4, 3이 출력된다.
@IBAction func exMap2() {
Observable.from(["with", "곰튀김"])
.map { $0.count }
.subscribe(onNext: { str in
print(str)
})
.disposed(by: disposeBag)
}
- FlatMap
: 이벤트를 다른 observable로 바꿔주는 연산자
3. filter : 필터링 operator
: from의 array 요소를 하나씩 조건에 맞는지 검사한 후 맞는 것만 내려보낸다.
@IBAction func exFilter() {
Observable.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
.filter { $0 % 2 == 0 }
.subscribe(onNext: { n in
print(n)
})
.disposed(by: disposeBag)
}
- 아래 예제에서는 operator들을 복합적으로 이용해 data들이 stream을 타고 내려가는 것을 확인할 수 있다.
@IBAction func exMap3() {
Observable.just("800x600")
.map { $0.replacingOccurrences(of: "x", with: "/") } // "800/600"
.map { "https://picsum.photos/\($0)/?random" }
.map { URL(string: $0) }
.filter { $0 != nil }
.map { $0! }
.map { try Data(contentsOf: $0) }
.map { UIImage(data: $0) }
.subscribe(onNext: { image in
self.imageView.image = image
})
.disposed(by: disposeBag)
}
'RxSwift' 카테고리의 다른 글
[RxSwift] 알엑스로 마리모 다이어리 v1.1 리팩토링하기 - 1 (0) | 2021.12.20 |
---|---|
[RxSwift] 곰튀김님의 RxSwift 4시간 끝내기 정리 - 4교시 (0) | 2021.11.08 |
[RxSwift] 곰튀김님의 RxSwift 4시간 끝내기 정리 - 3교시 (0) | 2021.11.01 |
[RxSwift] 곰튀김님의 RxSwift 4시간 끝내기 정리 - 2교시 (0) | 2021.10.26 |
댓글