학습 정리
RxSwift
옵저버 패턴
- 1:N 관계로 이루어진, 관찰 패턴
- 객체를 구독하면 옵저버가 상태를 알려줌
함수형 프로그래밍
- 1급 객체
- 변수나 상수에 저장을 할 수 있어야 함
- 함수에서
return
할 수 있어야 함 - 파라미터로 전달할 수 있어야 함
- 순수 함수
/* 환율 계산해주는 프로그램 이 코드는 순수 함수라고 할 수 없다. → 전역 상태에 의존 = 참조 투명성이 없다 */
var rate = 1120
func krw(usd: Int) -> Int {
return usd * rate
}
krw(usd: 2) // 2240
krw(usd: 3) // 3360
rate = 1130
krw(usd: 2) // 2260
krw(usd: 3) // 3390
ReactiveX
- 관찰 가능한 스트림을 이용한 비동기 프로그래밍 API
- Reactive eXtension
RxSwift
- Observable
- Rx의 기본이 되는 스트림을 가지고 있는 기본 시퀀스
- 하나 또는 연속된 항목을 배출
- 구독 후 next, error, completed로 처리
- next 스트림
- 연속된 값들을 배출하고 옵저버는 next 스트림을 관찰 및 구독해서 원하는 행동을 함
- error 스트림
- 값을 배출하다가 에러가 생기면 error를 배출한 다음 해당 Observable은 스트림을 멈춤
- complete
- Observable이 모든 값을 다 배출하면 이 상태(?)가 됨
- error가 발생하면 complete은 발생하지 않음!
- next 스트림
- Operator
- Observable 안에서 데이터 처리를 위한 모든 연산들
- map, filter, merge 등 수십가지의 연산이 있음
- 메소드 체이닝으로 많이 진행함
- Single
- Observable 같이 N번이 아닌, 단번의 데이터를 처리하기 위한 시퀀스
- 구독 후 success, error 두 가지로 처리
- 한번만 오고, 한번에 끝남
- Subject
- 여러개의 Observer가 데이터를 관찰해야 할 때 사용하는 객체
- Observer 와 Observable 사이의 브릿지 역할
- 여러 개의 Observer가 데이터를 관찰해야하는 경우 자주 쓰임
굳이?
- 왜 비용을 들여가면서?
- 왜 잘 돌아가는 코드를 바꿔가면서?
- 왜 꼭 더 난이도 있는 RxSwift를?
이유
- 간결해지는 코드
- 조금 더 길어지더라도, 간단하면서도 깔끔해지는 코드
- 간편한 비동기 관리
RxSwift 알아보기 1 ~ 3 기록
정수형 배열의 엘리먼트를 체크하는 함수 (RxSwift)
// items 라는 정수 타입의 배열을 파라미터로 받음 -> 정수 제네릭 타입의 Observable 객체를 리턴
func checkArrayObservable(items: [Int]) -> Observable<Int> {
return Observable<Int>.create { observer -> Disposable in
for item in items {
if item == 0 {
observer.onError(NSError(domain: "Error: value is zero.", code: 0, userInfo: nil)) // 0이면 에러를 흘려준다
break
}
observer.onNext(item) // 0이 아니면 각 엘리먼트를 next로 흘려준다
sleep(1)
}
observer.onCompleted() // 모든 순회가 끝나면 completed 되었다는 걸 알림
return Disposables.create()
}
}
Subscribe, Dispose
Subscribe
Observable
의 stream을 관찰하고 구독해서 받는 역할
→ Disposable
이라는 객체를 반환한다!
예제 코드
// interval: n초마다 정수 타입의 스트림이 방출됨
Observable<Int>.interval(RxTimeInterval.seconds(1), scheduler: MainScheduler.instance)
.take(10) // parameter 만큼의 스트림 허용
.subscribe(onNext: { value in
print(value)
}, onError: { error in
print(error)
}, onCompleted: {
print("onCompleted")
}, onDisposed: {
print("onDisposed")
})
// 예외 상황 발생
// 카운팅이 끝나기 전에 뷰 컨트롤러를 해제해 버린다면?
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
UIApplication.shared.keyWindow?.rootViewController = nil
} // 앱이 죽었음에도 불구하고 끝까지 카운트가 진행됨
이것을 해결해보자.
Disposable
disposable: 처분할 수 있는, 사용 후 버릴 수 있는
public protocol Disposable {
/// Dispose resource.
func dispose()
}
// interval: n초마다 정수 타입의 스트림이 방출됨
let disposable = Observable<Int>.interval(RxTimeInterval.seconds(1), scheduler: MainScheduler.instance)
.take(10) // parameter 만큼의 스트림 허용
.subscribe(onNext: { value in
print(value)
}, onError: { error in
print(error)
}, onCompleted: {
print("onCompleted")
}, onDisposed: {
print("onDisposed")
})
// 예외 상황 발생
// 카운팅이 끝나기 전에 뷰 컨트롤러를 해제해 버린다면?
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
disposable.dispose()
} // 반환된 disposable 객체를 가지고 있다가 뷰 컨트롤러가 deinit 될 때 dispose 실행
만약 구독받는 Observable이 여러개라면?
→ DisposeBag을 사용하자
extension Disposable {
/// Adds `self` to `bag`
///
/// - parameter bag: `DisposeBag` to add `self` to.
public func disposed(by bag: DisposeBag) {
bag.insert(self)
}
}
파라미터로 들어오는 DisposeBag 객체에 자신을 insert 한다.
모든 Disposable
객체에 disposed
를 해주면 해당 파라미터인 disposeBag
에 등록되고 disposeBag 객체가 해제되면서 등록된 모든 disposable이 다 같이 dispose 되어버린다.
/// var로 선언한 이유: disposeBag이 해제되면 모든 disposable이 dispose되는 원리를 개발도중 사용할 수 있음
/// subscribe 중이던 disposable을 초기화하고 싶으면 새로운 DisposeBag 객체를 넣어주면 끝!
var disposeBag = DisposeBag()
// interval: n초마다 정수 타입의 스트림이 방출됨
Observable<Int>.interval(RxTimeInterval.seconds(1), scheduler: MainScheduler.instance)
.take(10) // parameter 만큼의 스트림 허용
.subscribe(onNext: { value in
print(value)
}, onError: { error in
print(error)
}, onCompleted: {
print("onCompleted")
}, onDisposed: {
print("onDisposed")
})
.disposed(by: disposeBag) // **← 여기 부분!**
RxSwift In 4Hours
동기/비동기 처리
@IBAction func onLoadAsync(_ sender: Any) {
// TODO: async
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
let image = self.loadImage(from: self.IMAGE_URL)
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
private func loadImage(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
}
just
Observable.just("Hello World")
.subscribe(onNext: { str in
print(str)
})
.disposed(by: disposeBag)
just
의 인자로 넣어준 "Hello World"
가 str
로 넘어간다.
Observable.create()
를 대신 해주는 메소드- 배열을 넣으면 배열을 처리함(아무거나 넣어도 된단 소리)
from
Observable.from(["RxSwift", "In", "4", "Hours"])
.subscribe(onNext: { str in
print(str)
})
.disposed(by: disposeBag)
from
에 배열이 들어가면 하나씩 실행한다.
map
Observable.just("Hello")
.map { str in "\(str) RxSwift" }
.subscribe(onNext: { str in
print(str)
})
.disposed(by: disposeBag)
실행순서
just(Hello)
map { str in "\(str) RxSwift" }
onNext: { str in print(str) }
- 결과: Hello RxSwift
Observable.just(["Hello", "World"])
.map { str in "\(str) RxSwift" }
.subscribe(onNext: { str in
print(str)
})
.disposed(by: disposeBag)
실행순서
just(["Hello", "World"])
map { str in "\(str) RxSwift" }
onNext: { str in print(str) }
- 결과: ["Hello", "World"] RxSwift
Observable.from(["Hello", "World"])
.map { str in "\(str) RxSwift" }
.subscribe(onNext: { str in
print(str)
})
.disposed(by: disposeBag)
실행순서
just(["Hello", "World"])
→ 첫번째 원소map { str in "\(str) RxSwift" }
onNext: { str in print(str) }
- 결과: Hello RxSwift
just(["Hello", "World"])
→ 두번째 원소map { str in "\(str) RxSwift" }
onNext: { str in print(str) }
- 결과: World RxSwift
filter
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)
true
일 때만 데이터가 밑으로 내려감
이미지를 다운받는 과정
위에서 배운 걸로 해석해보자.
Observable.just("800x600")
.map { $0.replacingOccurrences(of: "x", with: "/") } // → "800/600"
.map { "https://picsum.photos/\($0)/?random" } // → "https://picsum.photos/800/600/?random"
.map { URL(string: $0) } // → URL 객체로 변환
.filter { $0 != nil } // → URL 객체가 nil이면 내려가지 않음
.map { $0! } // → 내려오면 강제 언랩핑 (위에서 체크했으니까 강제 언래핑해도 됨)
.map { try Data(contentsOf: $0) } // → URL에 있는 데이터를 다운
.map { UIImage(data: $0) } // → UIImage?
.subscribe(onNext: { image in
self.imageView.image = image
})
.disposed(by: disposeBag)
observeOn
Observable.just("800x600")
.observeOn(ConcurrentDispatchQueueScheduler.init(qos: .default)) // 백그라운드에서 돌릴 때
.map { $0.replacingOccurrences(of: "x", with: "/") } // → "800/600"
.map { "https://picsum.photos/\($0)/?random" } // → "https://picsum.photos/800/600/?random"
.map { URL(string: $0) } // → URL 객체로 변환
.filter { $0 != nil } // → URL 객체가 nil이면 내려가지 않음
.map { $0! } // → 내려오면 강제 언랩핑 (위에서 체크했으니까 강제 언래핑해도 됨)
.map { try Data(contentsOf: $0) } // → URL에 있는 데이터를 다운
.map { UIImage(data: $0) } // → UIImage?
.observeOn(MainScheduler.instance) // 메인 스레드에서 돌리는거
.subscribe(onNext: { image in
self.imageView.image = image
})
.disposed(by: disposeBag)
observeOn
을 해준 다음줄 부터 영향이 미친다.
subscribeOn
subscribe
가 실행되는 시점부터 동작한다. 아무 위치에나 넣어도 상관없음!
side-effect
side-effect를 허용하는 건 두 개가 있다.
- do
- subscribe