Published on

๐ŸŽ Swift - Rx ์—†์ด Reactive Binding ํ•ด๋ณด๊ธฐ

Authors
  • avatar
    Name
    ์ด์ฐฝ์ค€
    Twitter

Rx ์—†์ด View โžก๏ธ ViewModel Bindingํ•˜๊ธฐ

Rx๋ผ๋Š” ๋†ˆ ๊ณต๋ถ€ํ•  ๋• ์–ด๋ ค์› ๋Š”๋ฐ.. ํ•œ ๋ฒˆ ์จ๋ณด๋‹ˆ๊นŒ ์—†์ด ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ํ•  ๋•Œ ๊ฐˆ์ฆ์ด ๊ณ„์†ํ•ด์„œ ์ผ์–ด๋‚˜๋Š” ํ˜„์ƒ์„ ๊ฒช๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์˜์กดํ•˜๋Š” ๊ฒƒ์€ ์ข‹์ง€ ์•Š์œผ๋‹ˆ๊นŒ Rx ์—†์ด ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ณต๋ถ€ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ์˜ ๊ฒฝ์šฐ ๊ฒ€์ƒ‰ํ•ด๋ณธ ๊ฑฐ์˜ ๋ชจ๋“  ๊ฒŒ์‹œ๋ฌผ์—์„œ ๋™์ผํ•œ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋‚ด๋ถ€์ ์ธ ๋™์ž‘ ๊ณผ์ •๋„ ๋ชจ๋ฅธ์ฑ„๋กœ ๊ฐ€์ ธ๋‹ค์“ฐ๊ธด ์‹ซ์–ด์„œ ๊ณต๋ถ€ ๊ฒธ ์ž‘์„ฑํ•œ ๊ฒŒ์‹œ๋ฌผ์ž…๋‹ˆ๋‹ค!

์ฒดํฌ ๋ฆฌ์ŠคํŠธ ์•ฑ์„ ๋งŒ๋“ ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ์‹œ๋‹ค. ํ•  ์ผ์„ ์™„๋ฃŒํ•  ๋•Œ๋งˆ๋‹ค ๊ทธ ๋‚  ์ผ์˜ ์™„๋ฃŒ์œจ Label์„ ๊ณ„์†ํ•ด์„œ ๋ฐ”๊ฟ”์ฃผ๊ณ  ์‹ถ์–ด์š”. ๋”ฑ Rx๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŽธ๋ฆฌํ•œ ์ƒํ™ฉ์ด์ง€๋งŒ ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์„ ์ฐพ์•„๋ด…์‹œ๋‹ค.

๊ฐ’์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์ˆ˜ํ–‰ํ•˜๋Š” ๋™์ž‘.. ์ •ํ™•ํžˆ ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ํ•˜๋Š” ์นœ๊ตฌ๊ฐ€ ์žˆ์—ˆ๋˜ ๊ฒƒ ๊ฐ™์•„์š”.

๋ฐ”๋กœ didSet์ž…๋‹ˆ๋‹ค.

didSet

var donePercentage: Int = 0 {
  didSet {
    print(oldValue)
  }
}

donePercentage์˜ ๊ฐ’์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค didSet์— ์žˆ๋Š” print ํ•จ์ˆ˜๊ฐ€ ์ž๋™์œผ๋กœ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ์ด didSet์„ ํ‚ค์›Œ๋“œ๋กœ ์žก๊ณ  ์‹œ์ž‘ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

Observable

Rx์—์„œ ์šฐ๋ฆฌ๋Š”

viewModel.todos
  .subscribe(onNext: { todo in
    print(todo.count)
  })
  .disposed(by: self.disposeBag)

์ด๋Ÿฐ ์‹์œผ๋กœ Observableํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๊ณ  subscribeํ•˜์—ฌ ํ•„์š”ํ•œ ๋™์ž‘๋“ค์„ ์ฒ˜๋ฆฌํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

๋จผ์ € ํ•„์š”ํ•œ Observable ํƒ€์ž…์„ ์ฐจ๊ทผ์ฐจ๊ทผ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค.

value: T

class Observable<T> {
  var value: T

  init(_ value: T) {
    self.value = value
  }
}

Observable์€ ๋ชจ๋“  ํƒ€์ž…์— ๋Œ€ํ•ด์„œ ๊ด€์ฐฐ์ด ๊ฐ€๋Šฅํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— Generic ํƒ€์ž…์„ T๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด ์•Œ๋งน์ด์ธ value ๊ฐ’์„ ๋„ฃ์–ด์ฃผ์—ˆ์ฃ .

์ด value ๊ฐ’์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์–ด๋–ค ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ด์ฃผ์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— didSet์„ ์‚ฌ์šฉํ•ด์ค์‹œ๋‹ค.

var value: T {
  didSet {
    // ์‹คํ–‰ํ•  ํ•จ์ˆ˜
  }
}

listener (ํด๋กœ์ €)

๋‹ค์‹œ Rx์˜ ๊ฒฝ์šฐ๋ฅผ ๋ด๋ณผ๊นŒ์š”?

์ € didSet ์•ˆ์— ๋“ค์–ด๊ฐ€๊ฒŒ ๋  ํ•จ์ˆ˜๋ฅผ ์ฐพ์•„๋ด…์‹œ๋‹ค.

.subscribe(onNext: { todo in
  print(todo)
})

subscribe๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ํด๋กœ์ €์˜ ํ˜•ํƒœ๋กœ ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ ๊ฐ™๋„ค์š”.

๊ทธ๋ ‡๋‹ค๋ฉด ์•ˆ์— ๋“ค์–ด๊ฐˆ ํ•จ์ˆ˜๋ฅผ ๋‹ด์„ ํด๋กœ์ € ๋ณ€์ˆ˜๋ฅผ ์ •์˜ํ•ด์ค์‹œ๋‹ค.

var listener: ((T) -> Void)?

return ๋ฐ›๋Š” ๊ฐ’์€ ์—†์œผ๋‹ˆ Void๋กœ ์ฃผ๊ณ  value ๊ฐ’์„ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•ด์ฃผ์–ด์•ผ ํ•˜๋‹ˆ input ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ T(์ œ๋„ˆ๋ฆญ ํƒ€์ž…)์„ ๋ฐ›์•„์ค์‹œ๋‹ค.

์ด์ œ ์ด listener ํด๋กœ์ €๋ฅผ Observable ์ฝ”๋“œ์— ์ ์šฉ์‹œ์ผœ์ค์‹œ๋‹ค.

class Observable<T> {
  var value: T {
    didSet {
      self.listener?(value)
    }
  }

  var listener: ((T) -> Void)?

  init(_ value: T) {
    self.value = value
  }
}

subscribe

View์ชฝ์—์„œ subscribe ์•ˆ์—์„œ ํด๋กœ์ €๋กœ ๋ฐ›๋Š” ํ•จ์ˆ˜๋ฅผ listener์— ๋‹ด๊ณ  ์žˆ๋‹ค๊ฐ€ didSet์ด ํ˜ธ์ถœ๋˜๋ฉด ์‹คํ–‰๋˜์•ผํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ @escaping ํด๋กœ์ €๋ฅผ ๋ฐ›์•„์ฃผ๊ณ  ํด๋ž˜์Šค ๋‚ด๋ถ€์˜ listener์— ๋‹ด์•„์ค์‹œ๋‹ค.

func subscribe(listener: @escaping (T) -> Void) {
  listener(value)
  self.listener = listener
}

์ด ๋•Œ listener(value)์™€ ๊ฐ™์ด ํ•œ ๋ฒˆ ์‹คํ–‰ํ•ด์คฌ๊ธฐ ๋•Œ๋ฌธ์—, subscribe์™€ ๋™์‹œ์— ๋ฌด์กฐ๊ฑด ํ•œ ๋ฒˆ์€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

binding๊ณผ ๋™์‹œ์— ์‹คํ–‰์„ ํ•˜์ง€ ์•Š๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ์—๋Š” ๋นผ์ฃผ์–ด๋„ ์•„๋ฌด ๋ฌธ์ œ ์—†์Šต๋‹ˆ๋‹ค.

์ข…ํ•ฉ

์ด์ œ ์ฝ”๋“œ๋“ค์„ ํ•ฉ์ณ๋ด…์‹œ๋‹ค!

class Observable<T> {
  var value: T {
    didSet {
      self.listener?(value)
    }
  }

  var listener: ((T) -> Void)?

  init(_ value: T) {
    self.value = value
  }

  func subscribe(listener: @escaping (T) -> Void) {
    listener(value)
    self.listener = listener
  }
}

์‚ฌ์šฉ

class HomeViewModel {
  var todoData: Observable<[ToDo]> = Observable([])
}

class HomeViewController: UIViewController {
  private var viewModel = HomeViewModel()

  lazy var progressLabel = UILabel().then { ... }

  override func viewDidLoad() {
    self.viewDidLoad()
    self.viewModel.todoData.subscribe { todo in
      DispatchQueue.main.async {
        self.progressLabel.text = todo.count
      }
    }

    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
      self.viewModel.todoData.value = [
        ToDo(title: "Test", state: .completed)
      ]
    }
  }
}