Promise, Rx ๋“ฑ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•์€ ๋งŽ๋‹ค. ์ด๋ฅผ ๋ฐฐ์›Œ๋ณด๊ธฐ ์ด์ „์—, ์™œ ๊ทธ๋Ÿฌํ•œ ๊ฐœ๋…์ด ๋‚˜์™”๋Š”์ง€, ์–ด๋– ํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ฐœ์„ ํ•ด์™”๋Š”์ง€๋ฅผ ์ฝ”๋“œ๋ฅผ ๊ณ ์ณ๋ณด๋ฉด์„œ ์ดํ•ดํ•ด๋ณด๋Š” ๊ฒƒ์ด ์ด ํฌ์ŠคํŒ…์˜ ๋ชฉํ‘œ์ด๋‹ค. ์ตœ๋Œ€ํ•œ ๊ฐœ์กฐ์‹์œผ๋กœ ์ ์œผ๋ ค ๋…ธ๋ ฅํ–ˆ๋‹ค. ์ฒซ๋ฒˆ์งธ ๊ธ€์—์„œ๋Š” ์ˆœ์ฐจ์ ์œผ๋กœ ๋„คํŠธ์›Œํฌ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์ฝ”๋“œ๋ฅผ ์ฝ๊ธฐ ์‰ฝ๋„๋ก ๊ฐœ์„ ํ•ด๋ณด๋ฉด์„œ, ์™œ ์ด๋Ÿฌํ•œ ๊ฐœ๋…์ด ๋‚˜์˜ค๊ฒŒ ๋˜์—ˆ๋Š”์ง€๋ฅผ ์ดํ•ดํ•ด๋ณด์ž.

ํ”„๋กœ์ ํŠธ ์„ค๋ช…

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2021-09-22 แ„‹แ…ฉแ„Œแ…ฅแ†ซ 10 48 33
  • Load ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์œผ๋กœ json์„ ๋ฐ›์•„์˜ด
  • ์ธ๋””์ผ€์ดํ„ฐ ๋ฐ”๊ฐ€ ์˜ค๋ฅธ์ชฝ์— ์ƒ๊ธฐ๊ณ , ๋กœ๋“œ๊ฐ€ ๋๋‚˜๋ฉด ์•„๋ž˜์˜ textView์— json์„ ๋ณด์—ฌ์ง€๊ฒŒ ํ•จ
import RxSwift
import SwiftyJSON
import UIKit
 
let MEMBER_LIST_URL = "https://my.api.mockaroo.com/members_with_avatar.json?key=44ce18f0"
 
class ViewController: UIViewController {
    @IBOutlet var timerLabel: UILabel!
    @IBOutlet var editView: UITextView!
 
    override func viewDidLoad() {
        super.viewDidLoad()
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
            self?.timerLabel.text = "\(Date().timeIntervalSince1970)"
        }
    }
 
    private func setVisibleWithAnimation(_ v: UIView?, _ s: Bool) {
        guard let v = v else { return }
        UIView.animate(withDuration: 0.3, animations: { [weak v] in
            v?.isHidden = !s
        }, completion: { [weak self] _ in
            self?.view.layoutIfNeeded()
        })
    }
 
    // MARK: SYNC
 
    @IBOutlet var activityIndicator: UIActivityIndicatorView!
 
    @IBAction func onLoad() {
        editView.text = ""
        setVisibleWithAnimation(activityIndicator, true)
 
        let url = URL(string: MEMBER_LIST_URL)!
        let data = try! Data(contentsOf: url)
        let json = String(data: data, encoding: .utf8)
        self.editView.text = json
        
        self.setVisibleWithAnimation(self.activityIndicator, false)
    }
}
 
  • ๋จผ์ € ํ”„๋กœ์ ํŠธ๋Š” ๊ฐ„๋‹จํ•˜๋‹ค.
  • ๋ทฐ๊ฐ€ ๋กœ๋“œ๋˜๋ฉด, ํƒ€์ด๋จธ๋ฅผ ์„ค์ •ํ•ด์„œ ํ™”๋ฉด์— ์‹œ๊ฐ„์ด ํ‘œ์‹œ๋˜๊ฒŒ ํ•œ๋‹ค.
  • ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ์‹œ์ž‘ํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜จ ๋’ค์— ํ™”๋ฉด์— ๋ณด์—ฌ์ค€๋‹ค.
  • ๋งˆ์ง€๋ง‰์œผ๋กœ ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ๊บผ์ค€๋‹ค.

์ฒซ๋ฒˆ์งธ ๊ฐœ์„  ์‚ฌํ•ญ

  • ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”๋ฐ, String(data: encoding:) ์˜ ๊ฒฝ์šฐ ๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ๋•Œ๋ฌธ์—, UI Update๋ฅผ ํ•  ์ˆ˜ ์—†์–ด, ๋ชจ๋“  ํ™”๋ฉด์ด ๋ฉˆ์ถ˜๋’ค, ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์€ ๋’ค์— ์—…๋ฐ์ดํŠธ๊ฐ€ ๋œ๋‹ค.
  • ์ด๋Š” Main Thread์—์„œ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ์ด๋‹ค. ๋”ฐ๋ผ์„œ ๋น„๋™๊ธฐ๋กœ ํ•ด๋‹น ์ž‘์—…์„ ์ง„ํ–‰ํ•ด์•ผ ํ•œ๋‹ค.
 
@IBAction func onLoad() {
    self.editView.text = ""
    self.setVisibleWithAnimation(self.activityIndicator, true)
 
    DispatchQueue.global().async { [weak self] in
        let url = URL(string: MEMBER_LIST_URL)!
        let data = try! Data(contentsOf: url)
        let json = String(data: data, encoding: .utf8)
        
        DispatchQueue.main.async { [weak self] in
            self?.editView.text = json
            self?.setVisibleWithAnimation(self?.activityIndicator, false)
        }
        
    }
}
 
  • ํ•ด๋‹น ์ฝ”๋“œ๋Š” ์ด์™€ ๊ฐ™์ด ๋ณ€๊ฒฝํ•˜์—ฌ ํ•ด๊ฒฐ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
  • weak self๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ˆœํ™˜์ฐธ์กฐ๋ฅผ ๋ฐฉ์–ดํ•ด์ฃผ์—ˆ๋‹ค.
  • UI update์˜ ๊ฒฝ์šฐ main thread์—์„œ ๋™์ž‘ํ•˜๊ฒŒ ํ•˜์—ฌ ์›ํ•˜๋Š” ๋™์ž‘์„ ํ•˜๊ฒŒ ํ•˜์˜€๋‹ค.

๋‘๋ฒˆ์งธ ๊ฐœ์„  ์‚ฌํ•ญ

  • ์ฝ”๋“œ๊ฐ€ ์ฝ๊ธฐ ์ข‹์ง€ ์•Š๋‹ค. ๊ธฐ๋Šฅ ๋ณ„๋กœ ๋‚˜๋ˆ„๋Š” ๊ฒƒ์ด ์ฝ๊ธฐ ์ข‹๋‹ค.
  • ํ•ด๋‹น ํ•จ์ˆ˜์˜ ๋™์ž‘์€, ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๊ณ , ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๊ณ . ๋‘๋‹จ๊ณ„๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ๋ฌธ์ œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๋™์ž‘ ์ž์ฒด๊ฐ€ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์—, ํ•ด๋‹น ๋™์ž‘์ด ๋๋‚œ ํ›„์— UI๋ฅผ ์—…๋ฐ์ดํŠธ ํ•ด์•ผํ•œ๋‹ค๋Š” ์ œ์•ฝ์ด ์กด์žฌํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.
  • ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์—, ๋ฐ์ดํ„ฐ๋ฅผ ๋น„๋™๊ธฐ๋กœ ๋ฐ›์•„์˜ค๋˜, ํ•ด๋‹น ์ž‘์—…์ด ๋๋‚œ ํ›„์— ๋™์ž‘ํ•˜๋Š” Completion handler๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•จ์ˆ˜๋ฅผ ๋ถ„๋ฆฌํ•œ๋‹ค.
  • ์ด ๋ฐฉ๋ฒ•์ด ๋ณดํ†ต์˜ swift์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋ณธ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์ด๋‹ค.
private func downloadJson(url: String, _ completion: @escaping (String?) -> Void) {
    DispatchQueue.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)
        }
    }
}
 
@IBAction func onLoad() {
    self.editView.text = ""
    self.setVisibleWithAnimation(self.activityIndicator, true)
    
    downloadJson(url: MEMBER_LIST_URL) { [weak self] json in
        self?.editView.text = json
        self?.setVisibleWithAnimation(self?.activityIndicator, false)
    }
}
  • main thread์—์„œ ์ž‘๋™ํ•˜๋„๋ก completion handler๋ฅผ ๋„ฃ์–ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ๋‹ค.
  • ์—ฌ๊ธฐ์„œ @escaping ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
  • ํ•ด๋‹น ํ‚ค์›Œ๋“œ๋Š”, ํ•จ์ˆ˜ ๋‚ด์—์„œ ํด๋กœ์ € ์‚ฌ์šฉ์ด ๋๋‚˜์ง€ ์•Š๊ณ , ํ•จ์ˆ˜์˜ ๋ฆฌํ„ด์ด ๋๋‚œ ํ›„์— ํด๋กœ์ € ์‚ฌ์šฉ์„ ํ•˜๋Š” ๊ฒฝ์šฐ ๋ช…์‹œ์ ์œผ๋กœ ์ ์–ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
  • ํ•ด๋‹น ํด๋กœ์ €๊ฐ€ ์ฝœ์Šคํƒ์ด ์‚ฌ๋ผ์ง„ ๋’ค์— ์ง€์›Œ์ง€์ง€ ์•Š๊ณ (์›๋ž˜ ์ฝœ์Šคํƒ์— ์Œ“์ธ ํ•จ์ˆ˜์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋“ฑ์€ ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด ์ข…๋ฃŒ๋˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ• ๋‹น ํ•ด์ œ๋œ๋‹ค.) ์ถ”์ ์„ ๊ณ„์†ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.
  • ํ•˜์ง€๋งŒ ๋งŒ์•ฝ ํด๋กœ์ €๊ฐ€ optional์ธ ๊ฒฝ์šฐ์—๋Š” ํ•ด๋‹น ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.
    • ์˜ต์…”๋„์ธ ๊ฒฝ์šฐ @escaping์ด ๊ธฐ๋ณธ ๋™์ž‘์ด๋ผ๊ณ  ํ•œ๋‹ค.
    • ์ถ”๊ฐ€
      • ์ผ๋‹จ ๊ธฐ๋ณธ์ ์œผ๋กœ excaping์ด ๊ธฐ๋ณธ์ด๋‹ค.
      • ๊ทธ๋Ÿฐ๋ฐ ํ•จ์ˆ˜์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋“ค์–ด์˜ฌ ๋•Œ๋งŒ ๊ธฐ๋ณธ์œผ๋กœ non escaping์ด ๋œ๋‹ค.
        • ๊ทธ ์ด์œ ๋Š” ์•„๋งˆ ์ฝœ์Šคํƒ์˜ ๋ฌธ์ œ์ผ ๊ฒƒ์ด๋‹ค. ๋‹น์—ฐํžˆ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ์ฝœ ์Šคํƒ์ด ๋๋‚œํ›„์— ๋‹ค ์ง€์›Œ์ ธ์•ผ ํ•˜๋‹ˆ๊นŒ
      • ๊ทธ๋ž˜์„œ ํŠน๋ณ„ํ•œ ๊ฒฝ์šฐ์— ๋Œ€ํ•ด ํ•ด๋‹น ํ‚ค์›Œ๋“œ๋ฅผ ์ ์–ด์ฃผ์–ด์•ผ ํ•˜๋Š” ๊ฒƒ
      • ๊ทธ๋Ÿฐ๋ฐ, ์ด๋ ‡๊ฒŒ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํด๋กœ์ €๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ (์—ด๊ฑฐํ˜•, ํŠœํ”Œ, ๊ตฌ์กฐ์ฒด์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ)์—๋Š” ๊ธฐ๋ณธ ๋™์ž‘์œผ๋กœ escaping์ด ์ ์šฉ๋œ๋‹ค.
      • ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์˜ต์…”๋„ ํด๋กœ์ €๋ฅผ ๋„ฃ๋Š” ๊ฒฝ์šฐ, ์• ์ดˆ์— ์˜ต์…”๋„์˜ ์ •์ฒด๊ฐ€ enum์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ๋ณธ ๋™์ž‘์ด escaping์ธ ๊ฒƒ.
      • ๊ทธ๋ž˜์„œ ์•ˆ์จ์ค˜๋„ ๋ฌด๋ฐฉํ•˜๋‹ค.
      • ์ถœ์ฒ˜

์„ธ๋ฒˆ์งธ ๊ฐœ์„  ์‚ฌํ•ญ

  • ๊ทธ๋Ÿฐ๋ฐ, ์ด๊ฒŒ ๋ฐ”๋กœ ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐฉ๋ฒ•์ด๋‹ค.
  • ํ•˜์ง€๋งŒ ์ฝœ๋ฐฑํ•จ์ˆ˜์˜ ๋‹จ์ ์€, ๊ณ„์†ํ•ด์„œ ์ฝ”๋“œ์˜ depth๊ฐ€ ์ฆ๊ฐ€ํ•œ๋‹ค๋Š” ์ ์ด๋‹ค.
  • ๋™๊ธฐ์‹ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ๋ฒ•์ฒ˜๋Ÿผ, ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ณ , ๋ฐ›์€ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด์„œ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์ค€๋‹ค๋ฉด ์–ผ๋งˆ๋‚˜ ์ข‹์„๊นŒ
let json = downloadJson(url)
 
self.editView.text = json
  • ์ด๋Ÿฐ์‹์œผ๋กœ ๋ง์ด๋‹ค.
  • ๊ฒฐ๊ตญ ํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ์„ ์ •๋ฆฌํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
  • ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌํ„ดํ•ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์„๊นŒ?
    • ๋ฆฌํ„ดํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋™๊ธฐ์‹ ์ฒ˜๋ฆฌ๋ฐฉ๋ฒ•์ฒ˜๋Ÿผ ์ฝ”๋“œ๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ด๋Ÿฌํ•œ ํ•„์š”์„ฑ์—์„œ ํƒ„์ƒํ•œ ๊ฒƒ์ด Reactive programming์ด๋‹ค.
    • ์•„๋ž˜์—์„œ ๋ณด๋ฉด ์•Œ๊ฒ ์ง€๋งŒ, ๋จผ์ € ํ๋ฆ„์„ ์ •ํ•ด๋†“๊ณ (ํƒ€์ž…์—์„œ ์ด๋Ÿฌํ•œ ํ๋ฆ„์„ ๋งŒ๋“ค์–ด๋ฒ„๋ฆฐ๋‹ค.) ๋‚˜์ค‘์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ์—ฐ๊ด€๋œ ์ž‘์—…์ด ์‹คํ–‰๋˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.
class ๋‚˜์ค‘์—์ƒ๊ธฐ๋Š”๋ฐ์ดํ„ฐ<T> {
    // ์–ด๋– ํ•œ ํƒ€์ž…์„ ๋ฐ›์•„์„œ Void๋ฅผ ๋ฆฌํ„ดํ•˜๋Š” ํด๋กœ์ €๋ฅผ ์ธ์ˆ˜๋กœ ๊ฐ–๋Š” ํด๋กœ์ €
    // ์•ˆ์ชฝ์— ๋“ค์–ด๊ฐ€๋Š” ํด๋กœ์ €๊ฐ€ ํ›„์— ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค ๋ฐ›์œผ๋ฉด ์ˆ˜ํ–‰ํ•  completion handler์˜ ์—ญํ• ์„ ํ•œ๋‹ค.
    private let task: (@escaping (T) -> Void) -> Void
    
    init(task: @escaping (@escaping (T) -> Void) -> Void) {
        self.task = task
    }
    
    func ๋‚˜์ค‘์—์˜ค๋ฉด(_ f: @escaping (T) -> Void) {
        task(f)
    }   
}
  • ๊ทธ๋ž˜์„œ ์ด๋Ÿฌํ•œ ํƒ€์ž…์„ ๋งŒ๋“ค์—ˆ๋‹ค.
  • ๊ฐœ๋…์ ์œผ๋กœ๋Š” ๋‚˜์ค‘์— ์ƒ๊ธฐ๋Š” ๋ฐ์ดํ„ฐ๋ผ๊ณ  ํ•ด๋‘์—ˆ๋‹ค.
  • ํ•ด๋‹น ํƒ€์ž…์„ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•  ๋•Œ๋Š”, ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ์ด์ „์— ์ˆ˜ํ–‰ํ•  ๊ฒƒ๋“ค์— ๋Œ€ํ•ด ์ฒ˜๋ฆฌํ•  ํด๋กœ์ €๋ฅผ ๋ฐ›๋Š”๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ๋กœ ์‹คํ–‰ํ•˜๋Š” ์‹œ์ (๋‚˜์ค‘์—์˜ค๋ฉด)์— ์‹คํ–‰์ด ๋๋‚œ ๋’ค์— ์ฒ˜๋ฆฌํ•  ์ž‘์—…(completion)์„ ๋„˜๊ฒจ์„œ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.
private func downloadJson(url: String) -> ๋‚˜์ค‘์—์ƒ๊ธฐ๋Š”๋ฐ์ดํ„ฐ<String?> { 
    return ๋‚˜์ค‘์—์ƒ๊ธฐ๋Š”๋ฐ์ดํ„ฐ() { f in
        DispatchQueue.global().async {
            let url = URL(string: url)!
            let data = try! Data(contentsOf: url)
            let json = String(data: data, encoding: .utf8)
            
            DispatchQueue.main.async {
                f(json)
            }
        }
    }
    
}
 
@IBAction func onLoad() {
    self.editView.text = ""
    self.setVisibleWithAnimation(self.activityIndicator, true)
    
    let json: ๋‚˜์ค‘์—์ƒ๊ธฐ๋Š”๋ฐ์ดํ„ฐ<String?> = downloadJson(url: MEMBER_LIST_URL)
    
    json.๋‚˜์ค‘์—์˜ค๋ฉด { [weak self] json in
        self?.editView.text = json
        self?.setVisibleWithAnimation(self?.activityIndicator, false)
    }
}
  • ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ฆ‰, ์šฐ๋ฆฌ๊ฐ€ ์ฒ˜์Œ์— ์›ํ–ˆ๋˜ ๊ฒƒ ์ฒ˜๋Ÿผ, ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฐ’์ž์ฒด๋ฅผ ๋ฆฌํ„ดํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ํ•˜์ง€๋งŒ, ์œ„์˜ ๊ตฌํ˜„์‚ฌํ•ญ์„ ๋ณด๋ฉด ์•Œ๊ฒ ์ง€๋งŒ, ๊ฐœ๋…์ ์œผ๋กœ ๋ฆฌํ„ดํ•œ ๊ฒƒ์ผ ๋ฟ, ์•„์ง ๋กœ์ง์ด ์‹คํ–‰๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์„ ์ˆ˜ ์žˆ๋‹ค.
  • ์–ด๋Š ์‹œ์ ์— ์‹คํ–‰์ด ๋˜๋Š๋ƒ? ๋‚˜์ค‘์—์˜ค๋ฉด ์ด๋ผ๋Š” ํ‚ค์›Œ๋“œ๋ฅผ ํ†ตํ•ด์„œ, ํ•ด๋‹น ์ž‘์—…์ด ๋งˆ์นœ ์ดํ›„์— ํ•  ๋™์ž‘์„ ๋ช…์‹œํ•  ๋•Œ, ๋น„๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋œ๋‹ค.
  • ์ด์ œ ๋‚˜์ค‘์— ๋ณด๋ฉด ์•Œ๊ฒ ์ง€๋งŒ, ์—ฌ๊ธฐ์„œ ๋‚˜์ค‘์— ์ƒ๊ธฐ๋Š” ๋ฐ์ดํ„ฐ ๋ผ๋Š” ํƒ€์ž… ์ž์ฒด์˜ ์ด๋ฆ„์„ ์–ด๋–ป๊ฒŒ ๋ช…๋ช…ํ•˜๋Š๋ƒ์— ๋”ฐ๋ผ ๋‹ค์–‘ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
    • Promise
    • Combine
    • RxSwift
    • Bolts
  • RxSwift์—์„œ๋Š” ๋‚˜์ค‘์—์ƒ๊ธฐ๋Š”๋ฐ์ดํ„ฐ = Observable, ๋‚˜์ค‘์—์˜ค๋ฉด = Subscribe ๋กœ ๋ช…๋ช…ํ•œ๋‹ค.

Reactive Programming

๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์€ ๋ฐ์ดํ„ฐ ํ๋ฆ„(data flows)๊ณผ ๋ณ€ํ™” ์ „ํŒŒ์— ์ค‘์ ์„ ๋‘” ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจ๋Ÿฌ๋‹ค์ž„(programming paradigm)์ด๋‹ค. ์ด๊ฒƒ์€ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด๋กœ ์ •์  ๋˜๋Š” ๋™์ ์ธ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์‰ฝ๊ฒŒ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์•ผํ•˜๋ฉฐ, ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ํ†ตํ•ด ํ•˜๋ถ€ ์‹คํ–‰ ๋ชจ๋ธ์ด ์ž๋™์œผ๋กœ ๋ณ€ํ™”๋ฅผ ์ „ํŒŒํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

  • ํ•ต์‹ฌ
    • ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ๋กœ๋ถ€ํ„ฐ ์ „ํŒŒ
    • ๋ฐ›๋Š” ์ˆ˜์‹ ์ž ์กด์žฌ
    • Rx = Observable + Observer + Schedulers
  • Rx๋Š” ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์‰ฝ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•œ ์œ ํ‹ธ๋ฆฌํ‹ฐ์ด๋‹ค.

![imAssets/134277748-9c37d69f-e9bb-47center-small}General Marble Diagram

๋„ค๋ฒˆ์งธ ๊ฐœ์„  ์‚ฌํ•ญ

  • ์ด์ œ๋Š” ์‹ค์ œ RxSwift๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋™์ž‘์„ ๋ณ€๊ฒฝํ•ด๋ณด์ž.
private func downloadJson(url: String) -> Observable<String?> {
    
    return Observable.create() { f in
        DispatchQueue.global().async {
            let url = URL(string: url)!
            let data = try! Data(contentsOf: url)
            let json = String(data: data, encoding: .utf8)
            
            DispatchQueue.main.async {
                f.onNext(json)
            }
        }
        
        return Disposables.create()
    }
    
}
 
@IBAction func onLoad() {
    self.editView.text = ""
    self.setVisibleWithAnimation(self.activityIndicator, true)
    
    let disposable = downloadJson(url: MEMBER_LIST_URL)
        .subscribe { [weak self] event in
            switch event {
            case .next(let json):
                self?.editView.text = json
                self?.setVisibleWithAnimation(self?.activityIndicator, false)
            case .error(let error):
                print(error)
            case .completed:
                break
            }
        }
    
    // disposable.dispose()
    
}
  • ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
  • Observable, subscribe๋กœ ํ•จ์ˆ˜์ด๋ฆ„์„ ๋ณ€๊ฒฝํ•˜์˜€๋‹ค.
  • subscribeํ•  ๋•Œ ๋ฐ›๋Š” ์ธ์ž๊ฐ€ json์ด ์•„๋‹ˆ๊ณ  event๋ฅผ ๋ฐ›๋Š”๋‹ค.
    • ํ•ด๋‹น event๋Š” type์ด ์žˆ๋Š”๋ฐ, onNext, error, completed๊ฐ€ ์žˆ๋‹ค.
  • Observable ๊ฐ์ฒด๋ฅผ ์„ ์–ธํ•  ๋•Œ, ๋™์ž‘์„ ์ •์˜ํ•œ ํ›„์— Disposables๋กœ ๋ฆฌํ„ดํ•œ๋‹ค.
    • ์•„๋ž˜์—์„œ subscribe๋™์ž‘์„ ํ•œ ๋’ค์— ์ด Disposable ๊ฐ์ฒด๋ฅผ ๋ฆฌํ„ด๋ฐ›๋Š”๋‹ค.
    • ํ•ด๋‹น ๊ฐ์ฒด๋Š” dispose๋ผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๋ฐ, ์ด๋Š” ์œ„์˜ ์ •์˜ํ•œ subscribe๋™์ž‘์ด ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ๋๋‚˜์ง€ ์•Š์•˜์–ด๋„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๊ทธ๋ž˜์„œ ์œ„์˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰์‹œํ‚ค๋ฉด, let disposable ๋ผ์ธ์„ ์‹คํ–‰์‹œํ‚ค๊ณ  ๋ฐ”๋กœ disposable.dispose()๊ฐ€ ์‹คํ–‰๋˜์–ด ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ์ทจ์†Œ์‹œ์ผœ๋ฒ„๋ ค ์•„๋ฌด๋Ÿฐ ๋™์ž‘๋„ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
    • disposable ๊ฐ™์€ ๊ฒฝ์šฐ viewWillDisappear์— ์ถ”๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋ฉด ๋ทฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ์ทจ์†Œ์‹œํ‚ค๋Š” ํšจ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.
      • VC์•ˆ์— ๋ณ€์ˆ˜๋กœ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์•ผ ํ•  ๊ฒƒ
      • ๋งŒ์•ฝ์— ๋ฐ›์•„์•ผ ํ•˜๋Š” ๊ฒƒ๋“ค์ด ๋งŽ๋‹ค๋ฉด ๋ฐฐ์—ด๋กœ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๊ฐ€ ๋ฐฐ์—ด ์•ˆ์— ์žˆ๋Š” disposable์— ๋Œ€ํ•ด ๋ชจ๋‘ dispose๋ฅผ ํ•˜๋ฉด ๋œ๋‹ค.

๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•

๋‘๊ฐ€์ง€๋ฅผ ๋ฐฐ์šธ ๊ฒƒ์ด๋‹ค.

  1. ๋น„๋™๊ธฐ๋กœ ์ƒ๊ธฐ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์‹ธ์„œ ๋ฆฌํ„ดํ•˜๋Š” ๋ฐฉ๋ฒ•
private func downloadJson(url: String) -> Observable<String?> {
        
    Observable.create { emitter in
        emitter.onNext("Hello")
        emitter.onNext("world")
        emitter.onCompleted()
        
        return Disposables.create()
    }
}
  • ํ•˜๋‚˜์”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐœ์†กํ•˜๊ฒŒ ๋œ๋‹ค. ๋๋‚˜๋ฉด ๋๋‚œ๋‹ค๋Š” ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.
  • ๋‹ค์Œ์€ ์ œ๋Œ€๋กœ ๋œ ์‚ฌ์šฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž.
private func downloadJson(url: String) -> Observable<String?> {
    
    return Observable.create { emitter in
        let url = URL(string: url)!
        
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard error == nil else {
                emitter.onError(error!)
                return
            }
            
            if let data = data, let json = String(data: data, encoding: .utf8) {
                emitter.onNext(json)
            }
            
            emitter.onCompleted()
        }
        
        task.resume()
        
        return Disposables.create() {
            task.cancel()
        }
    }
}
  • ์„ธ์…˜์„ ๋งŒ๋“ค๊ณ  ์‹œ์ž‘ํ•œ๋‹ค.
  • ๋งŒ์•ฝ์— ๊ตฌ๋…์„ ์ทจ์†Œ ํ•œ๋‹ค๋ฉด(Disaposable) ์„ธ์…˜๋„ ์ข…๋ฃŒํ•ด์•ผ ํ•œ๋‹ค.
    • ํ•ด๋‹น ์ž‘์—…์„ Disposable์— ๋ฌถ์–ด ๋‘”๋‹ค.
  • ์„ธ์…˜ ๋‚ด๋ถ€์—์„œ๋Š” ์—๋Ÿฌ๊ฐ€ ๋‚  ๊ฒฝ์šฐ, error๋ฅผ ๋ฐฉ์ถœํ•œ๋‹ค.
  • ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์€ ๊ฒฝ์šฐ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋กœ ๋ฐฉ์ถœํ•œ๋‹ค.
  • ๊ทธ๋Ÿฐ๋ฐ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ด์ „์— ๋งŒ๋“ค์—ˆ๋˜ ์•„๋ž˜ ์ฝ”๋“œ์—์„œ ์—๋Ÿฌ๊ฐ€ ์ƒ๊ธด๋‹ค.
@IBAction func onLoad() {
  self.editView.text = ""
  self.setVisibleWithAnimation(self.activityIndicator, true)
  
  _ = downloadJson(url: MEMBER_LIST_URL)
      .subscribe { [weak self] event in
          switch event {
          case .next(let json):
              self?.editView.text = json
              self?.setVisibleWithAnimation(self?.activityIndicator, false)
          case .error(let error):
              print(error)
          case .completed:
              break
          }
      }
  
}
    
  • ์ด์ „์—๋Š” ๋‚˜์ค‘์—์˜ค๋ฉด ํ•จ์ˆ˜ ์•ˆ์—์„œ main ์Šค๋ ˆ๋“œ์—์„œ ๋™์ž‘์‹œํ‚ค๋„๋ก ํ–ˆ์ง€๋งŒ, ์ง€๊ธˆ Rx์˜ ๊ฒฝ์šฐ subscribe ๋™์ž‘์„ ํ•  ๋•Œ, URLSession์„ ์‹คํ–‰์‹œํ‚จ ๊ทธ ์Šค๋ ˆ๋“œ์—์„œ ๋™์ž‘์„ ์‹คํ–‰์‹œํ‚ค๋„๋ก ์„ค์ •๋˜์–ด ์žˆ๋‹ค.
  • ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์ง€๊ธˆ ๊ฐ™์€ ๊ฒฝ์šฐ UIupdate๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์Šค๋ ˆ๋“œ๋ฅผ ๋ณ€๊ฒฝํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
@IBAction func onLoad() {
    self.editView.text = ""
    self.setVisibleWithAnimation(self.activityIndicator, true)
    
    _ = downloadJson(url: MEMBER_LIST_URL)
        .subscribe { [weak self] event in
            switch event {
            case .next(let json):
                DispatchQueue.main.async {
                    self?.editView.text = json
                    self?.setVisibleWithAnimation(self?.activityIndicator, false)
                }
            case .error(let error):
                print(error)
            case .completed:
                break
            }
        }
    
}
  • ์ด๋ ‡๊ฒŒ!

Observable์˜ ์ƒ๋ช…์ฃผ๊ธฐ

  1. Create
  2. Subscribe
  3. onNext
  4. onCompleted, onError
  5. Disposed

์—ฌ๊ธฐ์„œ ์•Œ์•„์•ผ ํ•˜๋Š” ์ ์€, ์•„๊นŒ๋„ ๋งํ–ˆ์ง€๋งŒ, Create ๋˜์—ˆ๋‹ค๊ณ  ๋™์ž‘ํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋‹ค. Subscribe๊ฐ€ ๋˜์—ˆ์„ ๋•Œ ๋™์ž‘ํ•œ๋‹ค. ์ฆ‰, ๊ตฌ๋…์„ ์‹คํ–‰ํ•  ๋•Œ ๋ฐ์ดํ„ฐ๋“ค์ด ์ƒ์„ฑ๋˜์„œ ์ „๋‹ฌ๋˜๋Š” ๊ฒƒ. debug() ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ๋™์ž‘์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

@IBAction func onLoad() {
    self.editView.text = ""
    self.setVisibleWithAnimation(self.activityIndicator, true)
    
    _ = downloadJson(url: MEMBER_LIST_URL)
        .debug()
        .subscribe { [weak self] event in
            switch event {
            case .next(let json):
                DispatchQueue.main.async {
                    self?.editView.text = json
                    self?.setVisibleWithAnimation(self?.activityIndicator, false)
                }
            case .error(let error):
                print(error)
            case .completed:
                break
            }
        }
    
}
2021-09-22 13:11:23.037: ViewController.swift:109 (onLoad()) -> subscribed
2021-09-22 13:11:24.252: ViewController.swift:109 (onLoad()) -> Event next(Optional("[{\"id\":1,\"name\":\"Gladys Brugden\",\"avatar\":\"https://robohash.org/a ....

2021-09-22 13:11:24.279: ViewController.swift:109 (onLoad()) -> Event completed
2021-09-22 13:11:24.279: ViewController.swift:109 (onLoad()) -> isDisposed

์ˆœํ™˜์ฐธ์กฐ ์ด์Šˆ

  • ๊ฒฐ๊ตญ ํด๋กœ์ €๋ฅผ ํ™œ์šฉํ•ด์„œ ์ด๋Ÿฌํ•œ ๋กœ์ง์„ ๋งŒ๋“ค์–ด๋ƒˆ๋‹ค.
  • ๊ทธ๋Ÿฌ๋ฉด ๋”ฐ๋ผ์˜ค๋Š” ๋ฌธ์ œ๊ฐ€ ๋ญ๋ƒ๋ฉด, ์ˆœํ™˜์ฐธ์กฐ๋‹ค.
  • ์ผ๋‹จ ์ˆœํ™˜์ฐธ์กฐ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ๋ฐ–์— ์—†๋Š”๋ฐ, ์–ด๋Š ์‹œ์ ์— ๊ทธ๋Ÿผ ํด๋กœ์ €๋•Œ๋ฌธ์— ๋ฐœ์ƒํ•˜๋Š” reference count๊ฐ€ ์ค„์–ด๋“œ๋Š๋ƒ.
  • ์ฆ‰, ์–ธ์ œ ํด๋กœ์ €๊ฐ€ ์‚ฌ๋ผ์ง€๋Š๋ƒ.
  • on complete๊ฐ€ ๋˜๋ฉด ์‚ฌ๋ผ์ง„๋‹ค.
  • ํ•˜์ง€๋งŒ ํ˜น์‹œ ๋ชจ๋ฅด๋‹ˆ [weak self]๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ ๋Š”๊ฒŒ ๋‚˜์•„๋ณด์ธ๋‹ค.

Reference