흔한 정의...
리액티브 프로그래밍
은 비동기
와 이벤트
기반의 데이터 스트리밍을 Observable Sequence
로 변환하여 개발하는 프로그래밍 방식이다.
저게 무슨 말일까? 전혀 이해가 안갑니다.
또한, Reactive Programming
를 처음 마주하게 되었을 때 자주 보는 단어들은 Observable
, Observer
, Schduler
, Operator
, 모든 것을 stream으로 본다
... 등등 이다. 이건 또 뭘까? 하..
차근차근 알아봐요.
첫 시작은 Observable
에 대한 개념부터 잡아보죠.
생각의 출발점을 먼저 기존의 프로그래밍 방식
에서 시작해봐요.
유저의 디바이스에 있는 사진들을 가지고와서 사진들을 CollectionView에 보여주는 기능
이 있는 사진 앱을 만든다고 생각해보세요.
첫 번째 해야 할일은 사진 리스트를 가져오는 함수
를 만드는 것입니다.
class PhotoListViewController: UIViewController {
var collectionView: UICollectionView = ...
...
// 사진 리스트를 가져오는 함수
func fetchPhotos() -> PHFetchResult<PHAsset>? {
let collection = PHAssetCollection.fetchAssetCollections(
with: .smartAlbum,
subtype: .smartAlbumUserLibrary,
options: nil
).firstObject
if let collection = collection {
let fetchResult = PHAsset.fetchAssets(in: collection, options: nil)
return fetchResult
} else {
return nil
}
}
}
그리고 fetchPhotos()
함수를 호출하여 사진들을 CollectionView에 보여주는 기능
을 만들어야 합니다.
class PhotoListViewController: UIViewController {
...
var fetchResult: PHFetchResult<PHAsset>?
var cachingImageManager = PHCachingImageManager()
override func viewDidLoad() {
super.viewDidLoad()
...
fetchResult = fetchPhotos()
collectionView.reloadData()
}
}
extension PhotoListViewController: UICollectionViewDataSource {
func collectionView(
_ collectionView: UICollectionView,
numberOfItemsInSection section: Int
) -> Int {
if let fetchResult = fetchResult {
return fetchResult.count
} else {
return 0
}
}
func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: PhotoCell.cellIdentifier,
for: indexPath
) as! PhotoCell
if let fetchResult = fetchResult {
let asset = fetchResult[indexPath.item]
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = false
// 해당 asset의 이미지를 읽어와서 cell의 imageView에 image를 넣습니다.
cachingImageManager.requestImage(
for: asset,
targetSize: CGSize(width: 720, height: 1280),
contentMode: .aspectFill,
options: options,
resultHandler: { image, info in
if let image = image {
DispatchQueue.main.async {
cell.imageView.image = image
}
}
})
}
return cell
}
}
여기 까지 기존에 흔히 우리가 사용하는 방식으로 만들었습니다. 아래는 지금 까지의 흐름을 보여줍니다.
갑자기, 기획자가 '사진들의 총 개수를 네비게이션 바 왼쪽에 보여주는 기능
을 만들어주세요.'라는 요구사항이 온다면 다음과 같이 fetchPhotos()
함수에 의해 반환된 fetchResult 값에 또 다시 의존되어 구현되어집니다.
class PhotoListViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
...
fetchResult = fetchPhotos()
if let fetchResult = fetchResult {
photosCountLabel.text = "\(fetchResult.count)"
}
collectionView.reloadData()
}
}
사진들을 CollectionView에 보여주는 기능
과 사진의 총 개수를 보여주는 기능
은 fetchPhotos()
함수에 의해 반환된 fetchResult 값에 의존하여 구현되어진다는 것을 알 수 있습니다.
이 처럼 여러분이 지금까지 프로그래밍을 할 때, 하나의 흐름(fetchPhotos 함수
)에 의존하여 여러 갈래의 흐름(사진들을 collectionView에 보여주는 기능
, 사진의 총 개수를 보여주는 기능
)들을 구현하는 방식으로 개발해왔습니다.
지금까지는 요구사항이 많지 않아 손쉽게 구현할 수 있었습니다. 하지만 요구사항
이 많아지게 되면 프로그래머들은 요구사항을 구현하기 위해 특정 흐름(로직)
을 찾아 특정 흐름에 의존하여 여러 갈래의 흐름(요구사항)
들을 개발해야 하지만 힘들다는 점입니다. 왜 힘들까요? 엄청나게 복잡한 소스에서 특정 흐름
찾기란 대단히 어렵습니다. 위에 있는 그림을 통해서는 흐름이 어떤식으로 다음 흐름으로 이어지는지 알 수 있지만, 소스코드 상에서는 어디가 어디까지가 흐름인지 알기가 대단히 힘듭니다.
따라서 Reactive Programming
이란 기존 프로그래밍 방식에서 어려웠던 흐름
에서 흐름
으로 자연스럽게 흐름 중심 사고
로 개발하는 프로그래밍 방식입니다.
그렇타면 Reactive Programming에서는 어떻게 흐름
과 흐름
을 연결했을까요?
Reactive에서는 값을 내보내는 흐름
을 Observable
이라 표현하고 값을 내보내는 흐름에 구독하는(의존하는) 흐름
을 Observer
라고 표현합니다. 이 둘을 가지고 Observable을 Observer가 관찰하여 흐름을 이어가는 식으로 개발되어집니다.
이 개념을 지금까지 우리가 만들어놓은 것에 대입해보면 fetchPhotos()
는 Observable
가 되며, 사진들을 collectionView에 보여주는 기능
, 사진의 총 개수를 보여주는 기능
들은 Observer
가 됩어집니다. 아래와 같이 표현할 수 있어요.
자 이제 기존 소스를 RxSwift를 사용해서 바꿔 보겠습니다. 우선 fetchPhotos()
함수를 Observable로 바꾸도록 하겠습니다.
먼저 Observable로 바꾸기 전에 Event
라는 것을 알아야 합니다.
Reactive 세계에서는 Observable(생산자)
가 방출하는 값을 Event
라고 하며, Event의 종류는 3가지로 분류됩니다.
next(정상값), error(에러), completed(흐름이 정상적으로 종료)
즉, fetchPhotos()
함수는 리턴할 수 있는 값은 사진 리스트 결과 객체
와 nil
입니다. 사진 리스트 결과 객체
이 값은 정상적인 값을 의미하는 .next
event로 분류되며, nil
와 같은 값은 비정상적인 값을 의미하는 error
event 로 분류되어집니다. 그리고 함수가 리턴하는 시점은 함수의 종료를 의미하는 completed
event로 분류됩니다.
이제 정말로 바꿔보죠.
class PhotoListViewController: UIViewController {
var collectionView: UICollectionView = ...
...
enum PhotoListError: Error {
case fetchError
}
func fetchPhotos() -> Observable<PHFetchResult<PHAsset>> {
// 1
return Observable.create { observer in
let collection = PHAssetCollection.fetchAssetCollections(
with: .smartAlbum,
subtype: .smartAlbumUserLibrary,
options: nil
).firstObject
// 2
if let collection = collection {
let fetchResult = PHAsset.fetchAssets(in: collection, options: nil)
observer.on(.next(fetchResult))
} else {
observer.on(.error(PhotoListError.fetchError))
}
observer.on(.completed)
return Disposables.create()
}
}
}
하나씩 분석해 보면
- Observable을 생성하고 리턴하고 있습니다.
- observer에게
on()
함수를 사용하여 event를 보내고 있습니다.
이렇게 바뀐 Observable을 사용해보겠습니다.
class PhotoListViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
...
let fetchPhotosObservable = fetchPhotos()
// 사진들을 collectionView는 흐름
fetchPhotosObservable
.subscribe(onNext: { [weak self] fetchResult in
guard let `self` = self else { return }
self.fetchResult = fetchResult
self.collectionView.reloadData()
}, onError: { error in
// ...
}, onCompleted: {
// ...
})
// 사진의 총 개수를 보여주는 기능
fetchPhotosObservable
.subscribe(onNext: { [weak self] fetchResult in
guard let `self` = self else { return }
self.photosCountLabel.text = "\(fetchResult.count)"
})
}
}
위 코드를 보면 fetchPhotos()
함수에 의해 생성된 Observable(사진 리스트를 가져오는 흐름
)을 통해 구독하여 나머지 흐름( 사진들을 collectionView에 보여주는 기능
, 사진의 총 개수를 보여주는 기능
)을 구현하고 있습니다.
지금은 저렇게 Reactive 프로그래밍 방식과 기존 프로그래밍 방식으로 하는것과 코드나 줄차이가 없는데 뭐가 좋은거지 라고 생각하실 수 있습니다. 만약에 엄청나게 많은 코드가 있고, 갑자기 요구사항을 마구마구 온다면, Reactive 프로그래밍 방식으로 짜져있는 코드는 흐름
이 잘보이기 때문에 단지 특정흐름
에 구독을 통해 다른 흐름
을 손쉽게 만들 수 있습니다. 하지만 기존 프로그래밍 방식은 어떨까요? 어디가 어디까지 흐름인지 구분이 쉽게 되지 않아 계속해서 코드를 분석하는 시간이 길어지겠죠.
이 글의 목적은 Reactive 프로그래밍을 공부할 때, 첫 출발점은 Observable에 기반하여 나머지 Reactive 프로그래밍 개념들이 나왔기 때문에 그만큼 Observable은 Reactive 프로그래밍에서 제일 중요한 개념입니다. 이러한 개념을 잡는데 쉽제 않았기에 도움이 되는 마음에서 글을 써보았습니다. 부족하지만 읽어 주셔서 감사합니다. ~