2022-01-04
한줄 요약 : reactive! 반응형! 비동기적으로 일어나는 일들에 대한 코드를 깔끔하고 쉽게 사용하기 위해서 RxSwift를 사용한다.
0. 멀티 스레드
기본적으로, iOS는 멀티 쓰레드 (multi-thread) 환경이다. 쓰레드(일하는 녀석)가 여러개 있어서, 일을 따로 진행해준다.
특별히 쓰레드를 지정하지 않으면 main 스레드에서 돈다. 데이터를 받아오거나 하는 작업이 필요하다면 DispatchQueue.main.async {}
를 사용해서 다른 스레드에 작업을 넘긴다.
중요한 건 UI 작업을 할 때는 무조건 main 스레드에서 해야한다.
1. 개념잡기
RxSwift 4시간에 끝내기 github 을 클론받아서 진행했다.
1-2교시는 시간이 흐름에 따라 위의 타이머가 가면서, 버튼을 누르면 json을 로드해오는 동안 로딩 중 표시(indicator)가 나오고, json text를 화면에 보여주는 간단한 예제를 만들면서 진행했다.
ViewController 코드 : 딱히 다른 부분은 건드리지 않아서 이 코드에서 바뀌어나가는 부분만 필기를 해볼 예정이다.
1. Why RxSwift?
먼저, ViewController 코드의 문제점은
- 로드 버튼을 누르면 서버에서 json을 받아 오는 동안 (동기라서) 시간이 멈춘다.
- 원래 버튼 오른쪽에 indicator가 나와야하는데, 로딩 중에도 그게 나오지 않는다.
1. Swift 코드로 해결
- Swift에서는 비동기를 위해 DispatchQueue 를 사용해서 스케줄링을 한다.
@IBOutlet var activityIndicator: UIActivityIndicatorView!
@IBAction func onLoad() {
editView.text = ""
setVisibleWithAnimation(self.activityIndicator, true)
// 비동기로 실행할 것은 global 큐로 넘긴다.
DispatchQueue.global().async {
let url = URL(string: MEMBER_LIST_URL)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
// 그런데 UI 작업은 main 큐에서 해야하므로, 다시 뺀다.
DispatchQueue.main.async {
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
}
- 위의 코드에서 json을 불러오는 부분을 함수로 뺀다면?
func downloadJson(_ url: String) -> String? {
let url = URL(string: url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
return json
}
@IBOutlet var activityIndicator: UIActivityIndicatorView!
@IBAction func onLoad() {
editView.text = ""
setVisibleWithAnimation(self.activityIndicator, true)
DispatchQueue.global().async {
let json = dowonloadJson(MEMBER_LIST_URL)
DispatchQueue.main.async {
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
}
}
이렇게 되는데, 함수로 빼도 코드가 깔끔하지는 않다. DispatchQueue 부분까지 함수로 넣어서 깔끔하게 만들고 싶다. 하지만, DispatchQueue 부분을 함수 안에 넣게 되면, 함수는 스레드가 실행되는 동안 Return 되어버려서 필요한 (여기서는 json) 값을 return 해 줄 수가 없다.
-> 그래서 Swift에서는 closure를 사용해서 값을 전달하게 된다.
- closure 사용.
// global queue 안에서 return을 할 수 가 없다.
// 함수 실행은 함수 실행대로 되고, DispatchQueue는 global 스레드에서 돌고 끝나기 때문.
// escaping closure를 사용해서 결과값 전달.
func dowonloadJson(_ url: String, _ completion: @escaping ((String?) -> Void))? {
DispatcheQueue.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)
// 본체 함수가 return 되고 나서 closure가 실행 되어야 하니까 escaping 키워드를 붙인다.
// 그리고, optional 함수는 escaping이 default라서 키워드를 붙이지 않아도 된다.
}
}
}
@IBOutlet var activityIndicator: UIActivityIndicatorView!
@IBAction func onLoad() {
editView.text = ""
setVisibleWithAnimation(self.activityIndicator, true)
downloadJson(MEMBER_LIST_URL) { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
깔끔해졌다.
하지만, 콜백 지옥처럼 다운로드를 한 다음에 동기적으로 해야하는 작업이 많다면? DispatchQueue 안에 DispatcheQueue 를 계속해서 중첩해야한다...
2. RxSwift 아이디어
-> 함수 안에 비동기적으로 돌아가는 일이 있어도, closure 대신 return 값으로 전달할 수 없을까?
// 위의 아이디어대로 짠 코드.
class 나중에생기는데이터<T> {
private let task: (@escaping (T) -> Void) -> Void
init(task: @escaping (@escaping (T) -> Void) -> Void) {
self.task = task
}
func 나중에오면(_ f: @escaping (T) -> Void) {
task(f)
}
}
func downloadJson(_ url : String) -> 나중에생기는데이터<String?> {
// task를 생성. task는 인자로 @escaping (T) -> Void 를 받음.
// f 함수가 뭐든, json 정보를 인자로 받아서 f 함수가 실행될 것임.
// 나중에오면 이라는 함수가 실행될 때 여기서 전달한 클로저가 실행된다.
return 나중에생기는데이터() { f in
let url = URL(string: url)!
let data = try! Data(contentsof: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f(json)
}
}
}
@IBOutlet var activityIndicator: UIActivityIndicatorView!
@IBAction func onLoad() {
editView.text = ""
setVisibleWithAnimation(self.activityIndicator, true)
let json: 나중에생기는데이터<String?> = downloadJson(MEMBER_LIST_URL)
// task 를 실행하는데, task의 인자인 f라는 함수를 넘겨주고 있다.
// f에 들어가는 인자는 json으로 받아서 사용.
json.나중에오면 { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
}
그래서 비동기로 생기는 결과값을 completion으로 전달하지 않고 return 값으로 전달하도록 유틸리티로 만든 것이
- Promise - then
- Bolts - then
- RxSwift - subscribe
-> RxSwift는 딱 이 용도만 있어서, 아래 두가지를 알면 RxSwift를 정복할 수 있다.
- 비동기로 생기는 데이터를 Observable로 감싸서 return
- Observable로 오는 데이터를 받아서 처리하는 방법
3. RxSwift - Observable, subscribe, Disposable
// 위에서 봤던 나중에 생기는 데이터 -> Observable
// 나중에 오면 -> subscribe
// 에 completed, error 등을 더 처리할 수 있게 만든 것이 RxSwift다.
func downloadJson(_ url: String) -> Observable<String?> {
// 1. 비동기로 생기는 데이터를 Observable로 감싸서 return
return Observable.create() { f in
DispatchQueue.global().main {
let url = URL(string: MEMBER_LIST_URL)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f.onNext(json) // onNext 로 data 전달. -> next 로 받아서 처리
f.onCompleted()
}
}
// Observable 이 끝날때는 Disposables 를 return 해야한다.
return Disposables.create()
}
}
@IBOutlet var activityIndicator: UIActivityIndicatorView!
@IBAction func onLoad() {
// 2. Observable로 오는 데이터를 받아서 처리하는 방법
let disposable = download(MEMBRE_LIST_URL)
.subscribe { [weak self] event in
// closure 가 생성되면서 self 를 가리켜서 refernce count 가 증가 -> 순환 참조 발생
// 해결 1. weak self 붙여서 해결
// 해결 2. closure가 없어지면 reference count 감소.
// scomplete나 error case 일 때 closure 사라짐. -> Observable 생성할 때 onCompleted() 넣어주면 된다.
switch event {
case .next(let json):
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
// disposable.dispose()
// 함수를 실행하면 최종적으로 disposable 이 반환되기 때문에, 그걸 받아서 실행하던 작업이 끝나지 않았을 때도 취소할 수 있다.
// 취소 버튼을 눌렀을 때 이 코드를 넣는 식으로 짜면 됨.
}
2. Observable 문법
func downloadJson(_ url: String) -> Observable<String?> {
return Observable.create() { emitter in
emitter.onNext("Hello") // data 전달
emitter.onNext("World") // 여러개 전달 가능
emitter.onCompleted() // 전달 완료.
return Disposables.create()
}
}
제대로 만들어보자.
- Observable 생성
// Observable의 생명 주기
// 1. Create
// 동작
// 2. Subscribe : Observable은 create만 한다고 실행되지 않음. Subscribe가 있을 때 동작함.
// 3. onNext
// 끝
// 4. onCompleted / onError
// 5. Disposed
// 재사용 불가능. dispose 를 동작시킨 후에는 다시 사용할 수 없다.
func downloadJson(_ url: String) -> Observable<String?> {
// 비동기로 생기는 데이터를 observable로 감싸서 return
return Observable.create() { emitter in
let url = URL(string: MEMBER_LIST_URL)!
// 이 작업은 URLSession이 돌고 있는 스레드에서 발생하게 된다.
let task = URLSession.shaerd.dataTask(with: url) { (data, _, err) in
// error 발생 -> onError
guard err == nil else {
emitter.onError(err!)
return
}
// data 부르기
if let data = data, let json = String(data: data, encoding: .utf8) {
emitter.onNext(json)
}
// data가 불러와지면 불러와진 대로, 아니면 아닌대로 onCompleted
emitter.onCompleted()
}
task.resume()
// dispose가 불렸을 때 task를 cancel
return Disposables.create() {
task.cancel()
}
}
}
- Observable 처리 - subscribe
@IBAction func onLoad() {
editView.text = "" setVisibleWithAnimation(self.activityIndicator, true)
// 2. Observable로 오는 데이터를 받아서 처리하는 방법
// 변수로 처리할 필요가 없다면, chaining으로 처리하자.
let observable = downloadJson(MEMBER_LIST_URL)
// closure를 이용해서 데이터를 처리할 수 있다.
// observable 이 종료되면 클로저가 사라지기 때문에 순환 참조도 사라짐.
let disposable = observable.subscribe { event in
switch event {
case .next(let json):
// subscribe에서 도는건 URLSession이 도는 스레드니까 UI Logic 을 main thread 로 옮김.
DispatchQueue.main.async {
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
case .error(let error):
break
case .completed:
break
}
}
disposable.dispose()
}
3. Sugar API
RxSwift는 비동기로 전달되는 데이터를 Observable로 감싸서 return 값으로 활용할 수 있게 해주는 역할 밖에 없다. 근데 데이터를 처리하기가 너무 길고 귀찮기 때문에 Sugar API를 사용한다.
1. just, from
데이터를 딱 하나만 전달할 때 사용한다.
여러개를 전달하고 싶다면,
- just(배열)을 한꺼번에 우당탕 보내서 return 된 type을 변경해서 사용하거나,
- from(배열) 로 배열에 있는 요소를 하나씩 꺼내서 보낼 수 있다.
func downloadJson(_ url: String) -> Observable<String?> {
return Observable.just("Hello World")
// return Observable.create() { emmiter in
// emitter.onNext("Hello World") // data 전달 : onNext
// emitter.onCompleted() // 전달이 끝났다.
//
// return Disposables.create() // Observable.create() 의 마지막에는 Disposable을 return
// }
}
2. observeOn
// 1. subscribe(onNext: {}, onCompleted: {}, onError: {}) 중 하나만 받아서 처리할 수도 있다.
let disposable = observable
.subscribe(onNext: { json in
DispatchQueue.main.async {
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
})
// 2. observeOn으로 스레드 Queue 처리를 할 수 있다.
let disposable = observable
.observeOn(MainScheduler.instance) // 이렇게 data를 중간에 바꾸는 sugar를 operator 라고 부른다.
.subscribe(onNext: { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
})
3. map, filter, subscribeOn ...
https://reactivex.io/documentation/operators.html
// operator : filter, map, observeOn ..... 매우 많음
// 필요한걸 찾고 문서에서 설명을 읽어보고 사용하면 된다.
let disposable = observable
.map { json in json?.count ?? 0 } // operator
.filter { cnt in cnt > 0 } // operator
.map { "\($0)" } // string 변환 // operator
.observeOn(MainScheduler.instance) // data를 중간에 바꾸는 sugar : opreator
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .default))
// subscribeOn : 위치에 상관없이 시작하는 큐를 설정해준다. 처음에 두는 것이 읽기 좋음.
// default queueOS를 갖는 Dispatch Queue thread에서 처음부터 실행.
.subscribe(onNext: { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
})
observeOn, subscribeOn 은 스케쥴러를 인자로 받는데,
subscribeOn는 upStream에 영향, observeOn은 downstream에 영향.
4. reactiveX 문서 읽는법
여러 operator를 찾아서 사용해야하기 때문에 무슨 역할을 하는지 정확하게 알아보고 사용하는 것이 좋다. 강의에서 여러개 살펴보면서 읽는 법을 알려주셨다.
동그라미 - data
네모 - opreator
세모 - 해당 색의 thread로 바꾼다.
화살표 - observable
화살표 선 위의 작대기 - completed
last 는 앞의 observable이 끝날 때 맨 뒷 요소가 전달된다.
+)
buffer 는 데이터가 여러개 왔을 때, 묶어서 보낸다.
scan 은 reduce 같은 느낌.
출처
유튜브 곰튀김님 RxSwift 4시간에 끝내기
https://youtu.be/iHKBNYMWd5I
RxSwift 4시간에 끝내기 github
https://github.com/iamchiwon/RxSwift_In_4_Hours
느낀점
실제로 개발할 때 백엔드 연결 부분에서 엄청 헤맸다.
사실 아직도 헤매고 있다...
받아오는건 되는데 DispatchQueue 도 알긴 알겠는데 이 빨간줄은 뭐지..? 이런 기분이었다.
그래서 비동기를 할 때 정말 많이 쓴다는 RxSwift 라이브러리를 공부하기로 했다. 편하니까 쓰겠지? 싶어서 공부를 시작했는데, 와..... 짱이다... 근데 적용을 하려면 시간이 좀 걸릴 것 같다. 일단 강의 내용을 복기하면서 포스팅을 마치고 나면 예시 프로젝트에서 구현하지 않은 과정을 직접 구현해보고 예시 코드와 비교해보려고 한다.
포스팅을 쓰면서 고민을 많이 했다. 띡 올려놓은 코드만 보고도 이해가 가는게 좋겠지만, 개발에 있어서 초보자 입장에서는 늘 코드를 짤 때 이런 사고의 흐름이 있어서 어떤 순서대로 짰겠다~ 를 아는게 너무 많은 도움이 됐다. 그래서 강의의 흐름을 살려서 정리하려고 하니... 목차로 나눌 수 있는 틀이 안잡혀서 어려웠다.
그리고 원래는 Swift Language Guide 부터 다시 뿌시고 하려고 했는데, 미루다 보니 시간이 없기도 했고, 대학생처럼 공부하지 말라는 말을 많이 들어서, 무슨 공부를 할 때 목차를 따라가면서 순차적으로 하려는 생각을 이참에 버리려고 한다. 들으면서 모르는 걸 그때그때 찾아서 정리하는게 효율적인 것 같다.
JavaScript 에서 인자로 콜백함수를 넘기고 함수 안의 값을 사용하기 위해서 closure를 사용했는데 프론트 스쿨에서 공부한게 도움이 됐다. 그리고 js에서는함수(() => {})
<- 항상 이렇게 써야하는게 귀찮았는데, Swift 에서는 {} 로 쓸 수 있고, 함수 마지막 인자인 경우에는 괄호도 생략 가능한게 진짜 편했다.