1. 왜 비동기 프로그래밍을 하는가?
생활 예로, 마트에서 물건을 살 때 계산 줄을 서는 것을 생각하면 쉽다.
먼저 온 순서대로 한사람 한사람씩 계산되는 과정이 '동기적 프로그래밍'이다.
여기서, 어떤 사람이 물건을 엄청 많이 샀다던가, 계산 중에 다른 물건을 집어온다거나 할 때, 뒤에 서있는 고객들은 그 문제의 사람이 계산을 마칠 때까지 기다리는 수밖에 없다.
이와 같이, 동기적으로 프로그래밍을 한다면 한 작업이 끝날 때까지 그 뒤의 어떤 작업도 진행되지 않기에
화면 로딩, 통신 연결 등의 비효율을 높이고, 사용자 입장에서의 사용성 또한 급격히 떨어진다.
코드가 한 페이지에서 동기적으로만 일을 하게 된다면, 아마 페이지 최상단에 있는 것부터 하나씩 또 하나씩 로딩이 될 것이며, 설상가상 시작하면 중간에 물릴 수도 없으니, 화면을 로딩하는 중간에 다른 명령을 수행할 수도 없을 것이다.
만약 동영상 재생 한 페이지에서 하나씩하나씩 따로 로딩해서 동영상이 돌아가는데 꽤 오랜 시간이 걸리고, 그 시간 동안 다른 건 아무것도 클릭하지 못한다면 매우 불편할 것이다.
거기에 대한 해답이 바로 '비동기 프로그래밍'이다. 비동기적으로 코드를 실행하면 더 유동적으로, 더 효율적으로 많은 일을 할 수 있다!
동기적 일처리 방식 : 순차적으로 일을 스스로 끝내 나가는 방식 (한줄 한줄 쳐낸다)
비동기적 일처리 방식 : 해야 할 일을 위임하고 기다리는 방식 (동시다발적으로 업무를 수행하고 완료되는대로 loading되는 방식)
1-1. Swift에서의 비동기 처리 방식
DispatchQueue, completion handler, RxSwift/Combine, Swift Concurrency
2. RxSwift의 등장 계기
DispatchQueue, completion handler로 비동기 처리 프로그래밍을 하다보면, callback 지옥에 빠지게 되면서 코드 가독성이 매우 안좋아진다.
이걸 가독성 좋게하면서 return 문으로 비동기 코드의 결과값을 넘겨줄 수 없을까? 하다가
데이터 흐름과 변경사항을 관찰함(반응형 프로그래밍 특징)으로써 비동기 코드의 복잡성을 개선해주기 위해 나온 서드파티이다.
[Before]
DispatchQueue.global().async {
let url = URL(string: url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
completion(json)
}
}
// 데이터 통신이 끝나 넘겨받은 후
downloadJson(MEMBER_LIST_URL) { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
[After]
return 나중에생기는데이터() { f in
DispatchQueue.global().async {
let url = URL(string: url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f(json)
}
}
}
let json: 나중에생기는데이터<String?> = downloadJson(MEMBER_LIST_URL)
json.나중에오면 { json in
self.editView.text = json
self.setVisibleWithAnimation(activityIndicator, false)
}
3. RxSwift 란
iOS의 비동기 프로그래밍에서, 나중에 들어오는 return값(Observable)을 나중에 데이터가 return해서 들어오면 처리(Subscribe) 하는 것을 간결하고 효율적으로 도와주는 프레임워크이다.
- 나중에생기는데이터<타입> = Observable<타입>
- 나중에생기면 = Subscribe
return Observable.create() -> 나중에생기는데이터() { f in
DispatchQueue.global().async {
let url = URL(string: url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f.onNext(json)
}
}
return Disposables.create()
}
let json: Observable<String?> = downloadJson(MEMBER_LIST_URL)
json.subscribe = 나중에 오면 { json in
self.editView.text = json
self.setVisibleWithAnimation(activityIndicator, false)
}
4. RxSwift 사용 방법
1) 비동기로 생기는 데이터를 Observable로 감싸서 리턴하여 데이터 전달
func downloadJson(_ url:String) -> Observable<String?> {
//1. 비동기로 생기는 데이터를 Observable로 감싸서 리턴하는 방법
return Observable.create() { emitter in
let url = URL(string: url)!
let task = URLSession.shared.dataTask(with: url) { (data, _, err) in
guard err == nil else {
emitter.onError(err!) //에러 호출
return
}
if let dat = data, let json = String(data: dat, encoding: .utf8){
emitter.onNext(json) //데이터 전달
}
emitter.onCompleted() //종료 -> Observable의 끝
}
task.resume()
return Disposables.create() {
task.cancel() //observable을 취소할 때 하는 동작들 정의
}
}
}
2) Observable로 오는 데이터를 받아서 처리
@IBAction func onLoad() {
editView.text = ""
setVisibleWithAnimation(activityIndicator, true)
let observable = downloadJson(MEMBER_LIST_URL)
let disposable = observable.subscribe { event in // subscribe에 의해 함수 동작 및 데이터 전달받기
switch event {
case let .next(json):
DispatchQueue.main.async {
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
case .completed:
break
case .error:
break
}
}
disposable.dispose() //Observable이 시행중이든 아니든 즉시 작업 취소
}
* observable의 생명주기
- create
- subscribe
- onNext (데이터 전달)
- onCompleted / onError (실행 종료)
- disposed (동작이 끝난 Observable은 재사용할 수 없으므로, 쓰레기통에 버려준다는 느낌으로 '버리기')
* observable이 종료되는 시점
.completed, .error
5. 연산자 (Operator)
위에서 다뤘던 긴 코드를 간결하게 만들어주는 친구
_ = downloadJson(MEMBER_LIST_URL)
.subscribe { event in
switch event {
case let .next(json):
DispatchQueue.main.async {
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
case .completed: //완료되면 멈춤
break
case .error: //에러가 발생하면 멈춤
break
}
}
위 코드를, 아래와같이 간결하게 작성할 수 있음
//sugar API -> 위처럼 case를 다 나누지 않아도 onNext, onError만 가져와서 사용
_ = downloadJson(MEMBER_LIST_URL)
.subscribe(onNext: { json in
DispatchQueue.main.async {
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
})
다 외울수는 없으니, 그때그때 찾아보면 좋음
연산자 모아놓은 문서 : https://reactivex.io/documentation/operators.html
1) 자주 쓰는 연산자 모음
- Observable.just("데이터") : 데이터 하나 전달할 때 사용하는 연산자
- .from(["hello", "world"]) : 여러 데이터를 하나씩 차례대로 전달할 때 사용하는 연산자
- .subscribe(onNext: { ... }, onCompleted: { ... }, onError: { } ...): onNext, onCompleted, onError, dispose 중 필요한 것만 파라미터로 사용 한다.
- .observeOn() : 다음 줄부터 실행할 스레드를 지정
- downstream(아래로) 영향을 준다
- DispatchQueue.main.async { ... }를 사용하지 않고 한 줄로 해결
- 매개변수로는 Scheduler타입을 넣는다.
- 예를 들어 MainScheduler.instance, ConcurrentDispatchQueueScheduler(qos: .defalut) ..
- .subscribeOn() : 맨 처음 시작할 스레드를 지정
- 어디에서 호출하든 맨 처음 스레드를 지정함
- upstream(위로) 영향을 준다
- .map { ... },.filter { ... } ...
- .buffer : 여러 데이터가 있을 때 지정된 갯수만큼 묶어서 내려보내줌
- .scan : 직전 데이터와 새 데이터를 가지고 연산을 수행
- .take(횟수) : 지정 횟수만큼만 수행
- .debug() : Observable내에서 어떤 데이터가 전달되는지를 보여줌
- merge : 데이터 타입이 같은 여러개의 Observable을 하나의 observable로 만듦
- zip : 두 Observable의 데이터를 합쳐 쌍으로 만들어 보냄 ex. 1 2 3 4 5.. observable과 a b c d e observable -> 1a 2b 3c... observable
- combinelatest : zip과 비슷하나, 쌍을 만들 데이터가 없을 경우 이전 데이터를 가지고 쌍을 만들어서 내려줌
2) 마블 다이어그램
- 동그라미 = 데이터
- 네모 = 연산자
- 색깔 : 쓰레드가 달라짐을 의미함
- 가로 화살표 = Observable
- 세로줄 = complete
ex)
- array 데이터가 from operator를 거치면, Observable이 나오고, 데이터가 순서대로 전달이 되고나서 complete된다.
6. Subject
Observable처럼 subscribe해 값을 받아올 수 있고, 외부에서 값을 컨트롤할 수도 있는 객체
(MVVM 같이 ViewModel의 Observable을 ViewController에서 컨트롤 하는 상황 등에서 유용하게 쓰임)
https://reactivex.io/documentation/subject.html
1) 왜 쓰는가?
- create 시점 뿐 아니라, 이미 만들어진 subject에 대해서도 값 변경 가능
- Observable과의 차이점 : 생성된 이후에도 외부에서 값을 바꿀 수 있다. (데이터 주입 가능)
- PublishSubject와 BehaviorSubject를 가장 많이 사용한다.
2) Subject 종류
- PublishSubject
- 자신을 subscribe한 객체에게 데이터를 내려줌
- 이후 subscribe한 객체에게는 그 이후의 데이터를 내려줌
- BehaviorSubject(기본값)
- subscribe하면 기본값을 일단 내려주고 시작함
- 이후 subscribe한 객체에게는 가장 최근의 데이터 + 그 이후의 데이터를 내려줌
- AsyncSubject
- subscribe한 객체들이 있어도 데이터를 내려주지 않다가
- 자신이 complete되는 시점에 맨 마지막 데이터만 subscribe한 객체들에게 내려주고 끝남
- ReplaySubject
- 새롭게 subscribe한 객체에게 그동안 발생한 데이터 중 bufferSize 만큼의 최근 값들을 내려줌
💡 Stream 이란?
Observable의 input이 있고, subscribe하면서 output이 나오는 모든 일련의 흐름들을 스트림이라고 한다.
7. 기존 프로젝트에 RxSwift + MVVM 적용하기
1) ViewController의 ViewModel에 UI와 관련한 데이터, 비즈니스 로직 등을 분리한다.
- 그러나 아래와 같이 변경된 데이터를 ui에 반영하려면, ui에 반영하는 코드를 매번 작성해주어야한다.
@IBAction func onOrder(_ sender: UIButton) {
// 1. 이렇게 해서는 ui에 반영 안되니까 ui에 반영해주는 코드를 또 추가해주어야함 (updateUI)
// 이부분을 viewModel의 데이터가 변경될때마다 바인딩하고싶음!
viewModel.totalPrice += 100
updateUI()
}
func updateUI() {
itemCountLabel.text = "\(viewModel.itemsCount)"
totalPrice.text = viewModel.totalPrice.currencyKR()
}
2) ui에 표현할 데이터들을 Subject로 만들어주고, subscribe시 UI에 반영될 수 있도록 한다.
import Foundation
import RxSwift
class MenuListViewModel {
// var totalPrice: Int = 10_000
var totalPrice = PublishSubject<Int>()
}
viewModel.totalPrice.onNext(100)
override func viewDidLoad() {
viewModel.totalPrice
.scan(0, accumulator: +)
.map { $0.currencyKR() }
.subscribe(onNext: {
self.totalPrice.text = $0
})
.disposed(by: disposeBag)
*RxCocoa를 사용하여 위 바인딩 코드를 바꾸면, 매번 데이터가 바뀔때마다(onNext) UI를 바꾸는 작업을 하지 않아도 된다.
viewModel.itemsCount
.map { "\($0)" }
.bind(to: itemCountLabel.rx.text)
.disposed(by: disposeBag)
reference
https://limjs-dev.tistory.com/136
'🍎 iOS > 반응형 프로그래밍' 카테고리의 다른 글
[Combine] Combine vs RxSwift 비교 위주 Combine 개념 정리 (0) | 2025.08.04 |
---|---|
[RxSwift, RxCocoa] 주요 Operator 특징 및 차이점 정리 (계속 업데이트 예정) (0) | 2023.03.16 |
[RxSwift] Disposable, Dispose, DisposeBag (0) | 2023.03.16 |
[RxSwift] RxSwift + MVVM + Moya 적용 (feat. tableview binding) (0) | 2023.01.19 |