ํฐ์คํ ๋ฆฌ ๋ทฐ
[RxSwift] #6) RxSwift + MVVMํจํด [RxCocoa๋ฅผ ํ์ฉํ UITableView ๊ตฌ์ฑ]
ir.__.si 2023. 2. 1. 03:46๋ณธ ํฌ์คํ ์ ๊ณฐํ๊น๋์ ๊ฐ์์์์ ๊ธฐ๋ฐ์ผ๋ก, ๊ฐ์ธ์ ์ผ๋ก ๊ณต๋ถํ ๋ด์ฉ์ ์ ๋ฆฌํ ๊ธ์ ๋๋ค.
๋์ฑ ์์ธํ ๋ด์ฉ์, ๊ฐ์ ์์์ ์ง์ ์์ฒญํ์๋๊ฒ์ ์ถ์ฒ๋๋ฆฝ๋๋ค!
์ด์ ์ ํฌ์คํ ํ RxSwift + MVVMํจํด [Subject๋ฅผ ํ์ฉํด๋ณด์]์ ์ด์ด์ง๋ ํฌ์คํ ์ ๋๋ค.
์ ๋ฒ ํฌ์คํ ๋ง๋ฏธ์ ๋์๋ RxCocoa์ ๋ํด ๊ฐ๋จํ๊ฒ ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
RxCocoa๋ฅผ ์์ฝํ์๋ฉด,
[RxSwift์ ์์๋ค์ UIKit์ ์ ์ฉํ ์ ์๋๋ก extension ์์ผ์ ์ ๋ชฉ์ํจ ๊ฒ]
์ด๋ผ๊ณ ์ค๋ช ํ ์ ์์ต๋๋ค.
๊ธ๋ก๋ง ๋ณด๋ฉด ์ฝ๊ฒ ์ดํด๊ฐ ๋์ง ์์ผ๋, ์์๋ฅผ ํตํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
override func viewDidLoad() {
super.viewDidLoad()
viewModel.itemsCount
.subscribe(onNext: {self.itemCountLabel.text = "\($0)"})
.disposed(by: disposeBag)
.
.
.
}
ํ์ฌ๋ ์ด๋ ๊ฒ ์ฝ๋๊ฐ ๊ตฌ์ฑ์ด ๋์ด์์ต๋๋ค.
viewModel์ itemsCount๋ฅผ ๊ตฌ๋ ํ๋ฉด์, PublishSubject<Int>๊ฐ์ itemCountLabel.text์ ๋๊ฒจ์ฃผ๊ณ ์์ฃ ??
์ด ์ฝ๋๋ฅผ ํ๋ฒ ์ด๋ ๊ฒ ๋ฐ๊ฟ๋ณผ๊น์?
override func viewDidLoad() {
super.viewDidLoad()
viewModel.itemsCount
.map{"\($0)"}
.bind(to: itemCountLabel.rx.text)
.disposed(by: disposeBag)
.
.
.
}
์ด๋ค์ ์ด ๋ฌ๋ผ์ก๋์ง ์์๊ฒ ๋์??
๋ฐ๋ก .subscribe()๋ฅผ ํตํด text์ ๊ฐ์ ์ ๋ฌ ํด ์ฃผ์๋ ๋ถ๋ถ์ด
-> .bind()๋ผ๋ operator๋ฅผ ํตํด์ itemCountLabel์ ์ง์ ๊ฐ์ ์ง์ด๋ฃ๊ณ ์์ต๋๋ค!
(.map()์ด ์๋ก ์๊ธด ์ด์ ๋, text์ ๊ฐ์ ์ง์ binding์์ผ์ผ ํ๊ธฐ ๋๋ฌธ์ ์ด์ ์ ๋ฏธ๋ฆฌ Stringํ์ ์ผ๋ก ๋ณํํ๋ ์์ ์ ์ถ๊ฐ ํ ๊ฒ์ ๋๋ค.)
์ฆ, .bind()๋ผ๋ operator๋ฅผ ์ฌ์ฉํ๋ฉด, UIKit์ ์ปดํฌ๋ํธ์ ์ง์ ๊ฐ์ ์ ๋ฌํ ์ ์๋๋ฐ ์ด๋ฅผ ์ํด์๋
.rx๋ผ๋ ํค์๋๋ฅผ ์ฌ์ฉํด์ผํ๊ณ ์ด๊ฒ์ RxCocoa์์ ์ ๊ณตํด์ค๋๋ค!
์ฌ์ค .rx๋ฅผ ์จ์ผํ๋ค๊ธฐ ๋ณด๋ค๋ binder<T> ํ์ ์ผ๋ก ๋ณํํด์ฃผ๋ extension์ด ํ์ ํ ๊ฒ์ด์ง๋ง,
RxCocoa๋ผ์ด๋ธ๋ฌ๋ฆฌ์์๋ .rx ๋ผ๋ ์ด๋ฆ์ผ๋ก ๋๋ถ๋ถ ์ง์ํด์ฃผ๊ณ ์์ต๋๋ค.
(ํ์ง๋ง, ๋๋ถ๋ถ ์ด๋ผ๊ณ ์ ์ ๊ฒ์ ๋น์ฐํ ๊ฐ๋ฐ์๊ฐ ํ์ํ ์ ๋์ ๋ฐ๋ผ์๋ ์๋ก extension์ ๋ง๋ค์ด์ค์ผ ํ๋ค๋ ์๋ฏธ์ ๋๋ค!)
๋ํ, .bind()๋ฅผ ์ฌ์ฉํ ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ ์ ์์ง๋ง ์ํ์ฐธ์กฐ๋ฅผ ์์์ ์ฒ๋ฆฌ ํด ์ค๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
๊ทธ๋ผ ์ต์ข ์ ์ผ๋ก, Observable์ UIKit์ ๋ฐ์ธ๋ฉ ํด์ฃผ๋ ์ฝ๋๋ ์๋์ ๊ฐ์ด ๊ตฌ์ฑ๋ฉ๋๋ค!
override func viewDidLoad() {
super.viewDidLoad()
viewModel.itemsCount
.map{"\($0)"}
.bind(to: itemCountLabel.rx.text)
.disposed(by: disposeBag)
viewModel.totalPrice
.map{$0.currencyKR()}
.bind(to: totalPrice.rx.text)
.disposed(by: disposeBag)
}
๊ทธ๋ฐ๋ฐ ๋ง์ฝ, data์ฒ๋ฆฌ๊ฐ main thread๊ฐ ์๋ global thread์์ ์ฒ๋ฆฌ๊ฐ ๋๋ค๋ฉด, ์์ ์ฝ๋๊ฐ ์ ์์ ์ผ๋ก ์ฒ๋ฆฌ๊ฐ ๋ ๊น์???
์ฌ์ค iOS์ ๊ธฐ๋ณธ์ ์ธ ๋์ thread๋ main thread๋ก ์ง์ ์ด ๋์ด ์์ต๋๋ค.
ํ์ง๋ง, ๋ฐ๋์ ๋ชจ๋ ์ฒ๋ฆฌ๊ฐ main์์ ์ฒ๋ฆฌ๊ฐ ๋๋ค๋ ๋ณด์ฅ์ ํ ์ ์๊ณ , ์ฌ์ฉ์๊ฐ ์์๋ก ๋ณ๊ฒฝํด ๋ฒ๋ฆด ์ ์์ต๋๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์, ์ฐ๋ฆฌ๋ ์ ์ฝ๋์ ์ด์ ์ ์์๋ณด์๋ observeOn()์ด๋ผ๋ operator๋ฅผ ์ฌ์ฉํ์ฌ
์๋์ ๊ฐ์ด ๊ตฌ์ฑํ๋ฉด, ํด๋น ์ฝ๋์ ๋์์ ๊ฐ์ ๋ก main์ฐ๋ ๋๋ก ๋ถ๊ธฐํ ์ ์์ต๋๋ค!!
override func viewDidLoad() {
super.viewDidLoad()
viewModel.itemsCount
.map{"\($0)"}
.observeOn(MainScheduler.instance)
.bind(to: itemCountLabel.rx.text)
.disposed(by: disposeBag)
viewModel.totalPrice
.map{$0.currencyKR()}
.observeOn(MainScheduler.instance)
.bind(to: totalPrice.rx.text)
.disposed(by: disposeBag)
}
์, ๊ทธ๋ ๋ค๋ฉด ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ฐ๋ฆฌ๊ฐ ์ด๋ฒ์ ํ๊ณ ์ํ๋ menusObservable์ UITableView์ ๋ฐ์ธ๋ฉ ํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์์์์ ๋์ผํ๊ฒ .rx๋ฅผ ์ฌ์ฉํ ํ ๋ฐ, ์ฐ์ ์ฝ๋๋ฅผ ๋ณด๊ณ ์ค๋ช ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
let cellID = "MenuItemTableViewCell"
override func viewDidLoad() {
super.viewDidLoad()
viewModel.menusObservable
.bind(to: tableView.rx.items(cellIdentifier: cellID,
cellType: MenuItemTableViewCell.self)){index, item, cell in
cell.title.text = item.name
cell.price.text = "\(item.price)"
cell.count.text = "\(item.count)"
}
.disposed(by: disposeBag)
}
์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด, ์ญ์๋ .bind๋ฅผ ์ฌ์ฉํ๊ณ tableview.rx๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ๊น์ง๋ ์ด์ ๊ณผ ๋์ผํ์ฃ ? ๋ค์๋ ํฌ๊ฒ ์ด๋ ต์ง ์์ต๋๋ค.
์ดํ์๋ .items(cellIdentifier: , cellType: )๋ฅผ ํธ์ถํ๋๋ฐ, ์ด ํจ์๋ @escaping closer๋ฅผ ๋ฐํํ๊ฒ ๋ฉ๋๋ค.
(cellIdentifier์๋ cell์ ID๋ฅผ ์ ๋ ฅ ํด ์ฃผ๊ณ , cellType์๋ ๋ง ๊ทธ๋๋ก ํด๋น cell์ ํ์ ์ ์์ฑํด์ฃผ๋ฉด ๋ฉ๋๋ค.)
์ด๋ฅผ ํตํด์ @escaping closer์๋ index, item, cell์ด ์ธ์๋ก ๋ค์ด๊ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ ์ธ์๋ค์
1) index : IndexPath.row์ ๊ฐ์ ์ญํ
2) item : tableview์ ๋ฃ์ด์ฃผ๋ ์ค์ ๋ฐ์ดํฐ(์ ํํ๊ฒ๋ Sequence์ Element!).
(์์ ์์์์๋ [Menu]์ index๋ณ Element๊ฐ ๋ค์ด๊ฐ๊ฒ ๋ฉ๋๋ค!)
3) cell : tableview์์ index๋ณ๋ก ๊ทธ๋ ค์ค cell์ ์ ๋ณด
๋ฅผ ์๋ฏธํฉ๋๋ค!
์ ๊ทธ๋ผ ์ด๋๋ก ์คํ์ ์ํค๋ฉด ์ ๋์์ ํ ๊น์???
.
.
.
์ ๋ต์ "๊ทธ๋ ์ง ์๋ค!" ์ ๋๋ค.
๊ทธ ์ด์ ์ ๋ํด์๋ ์กฐ๊ธ ์๊ฐ์ ํด ๋ด์ผํฉ๋๋ค.
์ฐ๋ฆฌ๋ ํ์ฌ PublishSubjectํ์ ์ผ๋ก ์์ฑ์ด ๋ menusObservable์ ์ฌ์ฉํ๊ณ ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ Subject์ ํฐ ํน์ง ์ค ํ๋๊ฐ, ์์ฑ๊ณผ ๋์์ Observable์ ๋ฐฉ์ถํ๊ธฐ ์์ํ๋ค ๋ผ๋ ์ ์ ๋๋ค.
์๋์ ๋ง๋ธ๊ทธ๋ฆผ์ ํ๋ฒ ๋ณด๊ฒ ์ต๋๋ค.
์ด ์ฌ์ง์์ ๋งจ์์ [Red] [Green] ๋ฐ์ดํฐ๋ ์ด๋ฏธ Observable๋ก ๋ฐฉ์ถ์ด ๋๊ณ ์๋ ๋ชจ์ต์ ๋ณผ ์ ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ, ๊ฐ์ฅ ์๋์์ .subscribe()๋ฅผ ํ ์์ ์์๋ ์ด์ data์๋ํ ์ ๋ณด๋ฅผ ์ ์ ์๊ธฐ ๋๋ฌธ์ ์ด PublishSubject๋ฅผ
.subscribe()ํ๋ค๊ณ ํด๋ ์๋ฌด๋ฐ ๊ฐ์ ๊ฐ์ง๊ณ ์์ง ์๊ฒ ๋๋ ๊ฒ์ ๋๋ค.
(์ฐธ๊ณ ๋ก, ์๋์์ ๋๋ฒ์งธ stream๊ณผ ๊ฐ์ฅ ์๋์ stream์ ์์ ๋ณ๋์ stream ์ด๋ผ๋ ์ ์ ์์ผ์๋ฉด ์๋ฉ๋๋ค!!!)
๊ทธ๋ฐ๋ฐ ์ฐ๋ฆฌ๊ฐ ์์ฑํ ์๋์ ์ฝ๋๋ฅผ ๋ณด๋ฉด,
class MenuViewController: UIViewController {
let viewModel = MenuListViewModel()
let disposeBag = DisposeBag()
let cellID = "MenuItemTableViewCell"
override func viewDidLoad() {
super.viewDidLoad()
viewModel.menusObservable
.bind(to: tableView.rx.items(cellIdentifier: cellID,
cellType: MenuItemTableViewCell.self)){index, item, cell in
cell.title.text = item.name
cell.price.text = "\(item.price)"
cell.count.text = "\(item.count)"
}
.disposed(by: disposeBag)
}
.
.
.
}
class MenuListViewModel{
var menusObservable = PublishSubject<[Menu]>()
init(){
var menus : [Menu] = [
Menu(name: "ํ๊น1", price: 100, count: 0),
Menu(name: "ํ๊น1", price: 100, count: 0),
Menu(name: "ํ๊น1", price: 100, count: 0),
Menu(name: "ํ๊น1", price: 100, count: 0)
]
menusObservable.onNext(menus)
}
}
viewModel์ด๋ผ๋ ๋ณ์๊ฐ ์์ฑ์ด ๋๋ ๋์์ init()ํจ์๋ฅผ ํตํด ์ด๊ธฐํ๋ฅผ ์งํํ๊ฒ ๋ฉ๋๋ค.
๊ทธ๋ฐ๋ฐ ์ด ์ด๊ธฐํ ๊ณผ์ ์์ menusObservable์์ menus๋ฅผ ๋ณด๋ด๋๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
์ฆ,
์ฐ๋ฆฌ๊ฐ menusObservable์ ๋ฐ์ธ๋ฉํ์ฌ tableView์์ ํด๋น item์ ๊ด์ฐฐ ํ๋ ์์ ๋ณด๋ค
๋ฐ์ดํฐ๊ฐ ๋ค์ด๊ฐ ์์ ์ด ์์๊ธฐ ๋๋ฌธ์, PublishSubject์ ํน์ฑ ์ .subscribe()์ด์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฐฉ๋ฒ์ด ์๋ ๊ฒ
์ ๋๋ค.
๊ทธ๋ ๋ค๋ฉด ๋ฌธ์ ๋ ์์์ผ๋ ํด๊ฒฐ๋ฐฉ๋ฒ๋ ์๊ฒ ์ฃ ?
๋ฐ๋ก ์ง์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์๋ง ์๋ค๋ฉด ์ฝ๋๊ฐ ์ ์์ ์ผ๋ก ๋์ํ ๊ฒ ์ ๋๋ค!
๊ทธ๋ฆฌ๊ณ ์ด ๊ธฐ๋ฅ์ ํ๋ Subject๋ ์ฐ๋ฆฌ๋ ์ด๋ฏธ BehaviorSubject๋ผ๋ ๊ฒ์ ์๊ณ ์์ต๋๋ค.
(ํน์, Subject์ ์ข ๋ฅ์ ๋ํด ์ ๋ชจ๋ฅด์๋ ๋ถ์ ์ด ๊ฒ์๋ฌผ์ ์ฐธ๊ณ ํด์ฃผ์ธ์!)
๋ฐ๋ผ์, ์๋ ์ฝ๋์ ๊ฐ์ด MenuListViewModel ํด๋์ค์์ menusObservable์ ์ ์ธ ํ ๋, BehaviorSubject๋ก ์์ฑํด์ค๋ค๋ฉด
.subscribe()ํ๊ธฐ ์ง์ ๊ฐ์ธ, menus๋ฅผ ๋ถ๋ฌ์ฌ ์ ์๊ฒ ๋ฉ๋๋ค!
class MenuListViewModel{
var menusObservable = BehaviorSubject<[Menu]>(value: [])
lazy var itemsCount = menusObservable
.map{$0.map{$0.count}.reduce(0,+)}
lazy var totalPrice = menusObservable
.map{$0.map{$0.price * $0.count}.reduce(0,+)}
init(){
var menus : [Menu] = [
Menu(name: "ํ๊น1", price: 100, count: 0),
Menu(name: "ํ๊น1", price: 100, count: 0),
Menu(name: "ํ๊น1", price: 100, count: 0),
Menu(name: "ํ๊น1", price: 100, count: 0)
]
menusObservable.onNext(menus)
}
}
๊ทธ๋ ๋ค๋ฉด ์ ๋ง Binding์ด ์ ๋ถ ์ ๋์ด์๋์ง,
ORDER๋ฒํผ์ ๋๋ฅด๋ฉด ๊ฐ์ด ๋ณํ๊ฒํ์ฌ ๋์ํด๋ณด๊ฒ ์ต๋๋ค.
@IBAction func onOrder(_ sender: UIButton) {
// TODO: no selection
// showAlert("Order Fail", "No Orders")
// performSegue(withIdentifier: "OrderViewController", sender: nil)
viewModel.menusObservable
.onNext([Menu(name: "์ฉ์ธ", price: 1000, count: Int.random(in: 0...4)),
Menu(name: "์ฉ์ธ", price: 1000, count: Int.random(in: 0...4)),
Menu(name: "์ฉ์ธ", price: 1000, count: Int.random(in: 0...4))
])
}
์ด๋ ๊ฒ ORDER๋ฒํผ์ ๋๋ฅด๋ฉด menusObservable์ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ๋ ๋์์, ์ฝ๋๋ก ์์ฑํ๊ณ ์คํํ๋ฉด
์ค์ ์ํ๋๋๋ก ์ ๋์ํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค!
๊ทธ๋ผ ์ด์ clear๋ฅผ ๋๋ฅด๋ฉด, ์ฃผ๋ฌธ ๊ธ์ก๊ณผ ์ฃผ๋ฌธ ๊ฐฏ์๋ฅผ ์ด๊ธฐํ ํ๋ ์ฝ๋ ์์ฑ์ ํด ๋ณด๊ฒ ์ต๋๋ค!
@IBAction func onClear() {
viewModel.clearAllItemSelections()
}
์ฐ์ ์์ ์ฝ๋์ฒ๋ผ, viewModel์ ํตํด clearAllItemSelections()๋ฅผ ํธ์ถํฉ์๋ค.
๊ทธ๋ฆฌ๊ณ ์ค์ viewModelํ์ผ์ ๊ฐ์ ํด๋น ํจ์๋ฅผ ๋ง๋ค์ด ๋ด ์๋ค.
func clearAllItemSelections(){
menusObservable
.map {
$0.map{ m in
Menu(name: m.name, price: m.price, count: 0)
}
}
.subscribe {
self.menusObservable.onNext($0)
}
}
์์๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ๋ฉด, ํด๋น ํจ์๊ฐ ํธ์ถ ๋ ๋ ๋ง๋ค menusObservable์ ๋ฐ์ดํฐ๋ฅผ count๊ฐ 0์ธ ๊ฐ์ผ๋ก ๋ณํํ๊ณ
๋ค์ .subscribeํ์ฌ ์๋ก์ด stream์ ๋ง๋ค์ด์ค๋๋ค.
๊ทธ๋ฐ๋ฐ, ์ฐ๋ฆฌ๊ฐ viewDidLoad์ .subscribeํ์ฌ ๋ง๋ stream์ App LifeCycle์ ์ํด ํด๋น View์์ ๋ฑ 1๋ฒ๋ง ํธ์ถ๋๊ณ , View๊ฐ ์ฌ๋ผ์ง ๋ ์๋์ผ๋ก dispose๋๊ฒ ๋ฉ๋๋ค.
๊ทธ๋ ์ง๋ง ์ฐ๋ฆฌ์ ์ฝ๋๋ clear๋ฒํผ์ ๋๋ฅผ ๋ ๋ง๋ค clearAllItemSelections()๋ฅผ ํธ์ถํ๊ธฐ ๋๋ฌธ์,
๋ฒํผ์ ๋๋ฅผ ๋ ๋ง๋ค .subscribe()๋ฅผ ํตํด ๊ณ์ ์๋ก์ด stream์ ๋ง๋ค๊ฒ ๋ฉ๋๋ค.
func clearAllItemSelections(){
menusObservable
.map {
$0.map{ m in
Menu(name: m.name, price: m.price, count: 0)
}
}
.take(1)
.subscribe {
self.menusObservable.onNext($0)
}
}
๋ฐ๋ผ์, ์์ ์ฝ๋์ ๊ฐ์ด .take()์ด๋ผ๋operator๋ฅผ ์ฌ์ฉํ์ฌ 1๋ฒ ์คํํ๋ฉด์๋์ผ๋ก ํด๋น stream์ด dispose๋๊ฒ ๋ง๋ค์ด์
๋ฌด์๋ฏธํ stream์ด ๊ณ์ ์ด์์๋ ๊ฒ์ ๋ง์์ค๋๋ค.
+
์ฌ์ค, ํด๋น View์์์ ๋ง์ง๋ง ๊ธฐ๋ฅ์ธ ์ฃผ๋ฌธ ๊ฐ์ ์ถ๊ฐ/๊ฐ์ ๊ธฐ๋ฅ์ด ๋จ์์ต๋๋ค.
ํ์ง๋ง ์ง๊ธ๊น์ง ๊ณต๋ถํ ๊ธฐ๋ฅ๋ค์ ์ ์ฌ์ฉํ๋ฉด, ์ด ์ฃผ๋ฌธ ๊ฐ์๋ฅผ ์กฐ์ ํ๋ ๊ธฐ๋ฅ๋ ์ถฉ๋ถํ ๋ง๋ค์ด ๋ณผ ์ ์์ต๋๋ค.
๋ฐ๋ผ์, ์ด ๋ถ๋ถ์ ์๋ฃ๋ฅผ ์ ๋ณด๊ณ ์ง์ ๋ง๋ค์ด ๋ณด์๋ ๊ฒ์ ์ถ์ฒ ๋๋ฆฝ๋๋ค!
'๐ > RxSwift' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[RxSwift] #7) Driver๋ ๋ฌด์์ธ๊ฐ?? [Driver์ Relay] (0) | 2023.02.06 |
---|---|
[RxSwift] #5) RxSwift + MVVMํจํด [Subject๋ฅผ ํ์ฉํด๋ณด์] (0) | 2023.01.30 |
[RxSwift] #4) Subject๋ ๋ฌด์์ผ๊น? (1) | 2023.01.29 |
[RxSwift] #3) RxSwift + MVVMํจํด [๊ธฐ๋ณธ ํ๋ก์ ํธ ๊ตฌ์ฑ] (0) | 2023.01.29 |
[RxSwift] #2) Operator๋?? + ReactiveX ๋ฌธ์๋ณด๋ ๋ฒ (2) | 2023.01.28 |