๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŽ iOS/Architecture

[Design Pattern] ReactorKit์ด๋ž€?

by Fomagran ๐Ÿ’ป 2021. 6. 17.
728x90
๋ฐ˜์‘ํ˜•

 



์•ˆ๋…•ํ•˜์„ธ์š” Foma ๐Ÿ‘Ÿ ์ž…๋‹ˆ๋‹ค!

์˜ค๋Š˜์€ RxSwift์™€ MVVM ๋””์ž์ธ ํŒจํ„ด์„ ์‚ฌ์šฉํ• ๋•Œ ์•„์ฃผ ์œ ์šฉํ•˜๊ฒŒ ์“ฐ์ด๋Š” ReactorKit์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค!

๋ฐ”๋กœ ์‹œ์ž‘ํ• ๊ฒŒ์š”~


ReactorKit์ด๋ž€?


ReactorKit ๊ณต์‹ ๊นƒํ—ˆ๋ธŒ์—์„œ๋Š” ์ด๋ ‡๊ฒŒ ์†Œ๊ฐœํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

ReactorKit์€ ๋ฐ˜์‘ํ˜• ๋ฐ ๋‹จ๋ฐฉํ–ฅ Swift ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ์œ„ํ•œ ํ”„๋ ˆ์ž„ ์›Œํฌ์ž…๋‹ˆ๋‹ค.



๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค๋ช…๋“œ๋ฆฌ๋ฉด ReactorKit์€ ์•„๋ž˜์™€ ๊ฐ™์ด Reactor์™€ View๋ฅผ ์—ฐ๊ฒฐํ•ด์ฃผ๋Š”๋ฐ

์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ์‹์„ Action๊ณผ State๋กœ ๋‚˜๋ˆˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ViewModel ์—ญํ• ์„ Reactor๊ฐ€ ํ•˜๋Š”๋ฐ ์ด ViewModel์—์„œ ์ผ์–ด๋‚˜๋Š” ๋ฐ˜์‘ํ˜• ์ด๋ฒคํŠธ๋“ค์„ ์•ก์…˜๊ณผ ์ƒํƒœ๋กœ ๋‚˜๋ˆˆ ๊ฒƒ์ด์ฃ !


ReactorKit์„ ์“ฐ๋ฉด ๋ญ๊ฐ€ ์ข‹์€๋ฐ?


1. ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฝ๋‹ค.

๋ทฐ์—์„œ ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์„ ๋ถ„๋ฆฌํ•ด๋‚ด ๋ฆฌ์•กํ„ฐ๋Š” ๋ทฐ์— ๋Œ€ํ•œ ์ข…์†์„ฑ์ด ์—†์–ด์ง‘๋‹ˆ๋‹ค.

๊ณ ๋กœ ๋ฆฌ์•กํ„ฐ์™€ ๋ทฐ๋ฅผ ๋ฐ”์ธ๋”ฉํ•˜๋Š” ํ•˜๋Š” ์ฝ”๋“œ๋งŒ ํ…Œ์ŠคํŠธํ•˜๋ฉด ๋˜๊ณ  ํ…Œ์ŠคํŠธํ•˜๊ธฐ๊ฐ€ ํ›จ์”ฌ ์ˆ˜์›”ํ•ด์ง‘๋‹ˆ๋‹ค.

2. ๋ถ€๋ถ„์ ์œผ๋กœ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฆฌ์•กํ„ฐ ํ‚ท์€ "์ „์ฒด์ ์ธ ๋””์ž์ธ ํŒจํ„ด์„ ์ด ๋ฆฌ์•กํ„ฐํ‚ท์œผ๋กœ ํ•ด!!!" ๊ฐ€ ์•„๋‹ˆ๋ผ

"๋„ˆ ์‚ฌ์šฉํ•˜๊ณ ์‹ถ์€ ๊ณณ์—๋งŒ ๋ถ€๋ถ„์ ์œผ๋กœ ์‚ฌ์šฉํ•ด๋„ ๋ผ" ๋ผ๊ณ  ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณ ๋กœ ๋ฆฌ์•กํ„ฐํ‚ท์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ์ž‘์„ฑํ•  ํ•„์š”๋Š” ์—†๋‹ค๋Š” ๊ฒƒ์ด์ฃ .

3. ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•ด์ง„๋‹ค.

๋ฆฌ์•กํ„ฐํ‚ท์€ ๋‹จ์ˆœํ•œ ์ผ์„ ์œ„ํ•ด ๋ณต์žกํ•œ ์ฝ”๋“œ๋ฅผ ํ”ผํ•˜๋Š” ๊ฒƒ์ด ์ž์‹ ๋“ค์˜ ํ•ต์‹ฌ์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์‹ค์ œ๋กœ ๋‹ค๋ฅธ ์•„ํ‚คํ…์ณ์— ๋น„ํ•ด ํ›จ์”ฌ ์ ์€ ์ฝ”๋“œ๊ฐ€ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.


Example


๋ฆฌ์•กํ„ฐํ‚ท์— ๋‚˜์™€์žˆ๋Š” ๊ณต์‹ ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด์„œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์ดํ•ดํ•ด๋„๋ก ํ•ฉ์‹œ๋‹ค!

๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌ์กฐ๋ฅผ ์„ค๋ช…๋“œ๋ฆฌ๋ฉด ํ™”๋ฉด์„ ๋„์šฐ๋Š” ๋ทฐ(๋ทฐ์ปจํŠธ๋กค๋Ÿฌ)์™€ ๊ทธ ํ™”๋ฉด์— ๋Œ€ํ•œ ๋น„์ง€๋‹ˆ์Šค๋ฅผ ๋กœ์ง์„ ์ž‘์„ฑํ• 

๋ฆฌ์•กํ„ฐ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.


Pod


ํŒŸ ํŒŒ์ผ์— ์•„๋ž˜์™€ ๊ฐ™์ด ReactorKit๊ณผ RxCocoa๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๊ณ  pod install์„ ํ•ด์ค๋‹ˆ๋‹ค.

 

pod 'ReactorKit' 

pod 'RxCocoa'

Storyboard


๋ทฐ๋Š” ํ”Œ๋Ÿฌ์Šค ๋ฒ„ํŠผ๊ณผ ๋งˆ์ด๋„ˆ์Šค ๋ฒ„ํŠผ ์ˆซ์ž๋ฅผ ๋„์šธ ๋ ˆ์ด๋ธ” ๋กœ๋”ฉ์„ ๋ณด์—ฌ์ค„ ์ธ๋””์ผ€์ดํ„ฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

 


CounterViewController


๋จผ์ € ํ•„์š”ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์„ import ํ•ด์ค๋‹ˆ๋‹ค.

 

import UIKit 
import ReactorKit 
import RxCocoa


์Šคํ† ๋ฆฌ๋ณด๋“œ์™€ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์—ฐ๊ฒฐํ•ด์ค๋‹ˆ๋‹ค.

 

 @IBOutlet weak var loadingIndicator: UIActivityIndicatorView! 
 @IBOutlet weak var plusButton: UIButton! 
 @IBOutlet weak var numberLabel: UILabel! 
 @IBOutlet weak var minusButton: UIButton!

CounterReactor


์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋ฆฌ์•กํ„ฐํ‚ท์˜ ํ•ต์‹ฌ์ด ๋ฆฌ์•กํ„ฐ๋ฅผ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค.

๊ฐ€์žฅ ๋จผ์ € ReactorKit์„ import ํ•ด์ฃผ๊ณ  Reactor๋ฅผ ์ฑ„ํƒํ•ด์ค๋‹ˆ๋‹ค.

 

import ReactorKit 

class CounterViewReactor:Reactor { ...



๊ฐ€์žฅ ๋จผ์ € ์•ก์…˜๋ถ€ํ„ฐ enum์œผ๋กœ ์„ ์–ธํ•ด์ค๋‹ˆ๋‹ค.

Action์€ ๋ทฐ์—์„œ ์“ฐ์ด๋Š” ์•ก์…˜๋“ค์„ ๋‚˜์—ดํ•ด์ฃผ๋ฉด ๋ผ์š”.

์šฐ๋ฆฌ๋Š” +๋ฒ„ํŠผ๊ณผ -๋ฒ„ํŠผ์ด ์žˆ์œผ๋‹ˆ +๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋Š” ์•ก์…˜ -๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋Š” ์•ก์…˜์„ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๊ฒƒ์ด์ฃ .

 

 //์œ ์ €์—๊ฒŒ ๋ฐ›์€ ์•ก์…˜ 
 enum Action { 
 	case plus 
 	case minus 
  }



์•ก์…˜์„ ๋ฐ›์•„์„œ ์–ด๋– ํ•œ ๋ณ€ํ™”๊ฐ€ ์ผ์–ด๋‚˜๋Š”์ง€๋„ enum์œผ๋กœ ์„ ์–ธํ•ด์ค๋‹ˆ๋‹ค.

์•ก์…˜์— ๋”ฐ๋ผ์„œ ๊ฐ’์„ ์ฆ๊ฐ€ ์‹œํ‚ค๋Š” ๊ฒƒ๊ณผ ๊ฐ’์„ ๊ฐ์†Œ ์‹œํ‚ค๋Š” ๊ฒƒ ๊ทธ๋ฆฌ๊ณ  ๋กœ๋”ฉ์„ ์–ด๋–ป๊ฒŒ ๋‚˜ํƒ€๋‚ผ๊ฑด์ง€์— ๋Œ€ํ•œ

๋ณ€ํ™”๋“ค์„ ๋‚˜์—ดํ•ด์ฃผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

 //์•ก์…˜์„ ๋ฐ›์€ ์–ด๋– ํ•œ ๋ณ€ํ™” 
 enum Mutation { 
 case increaseValue 
 case decreaseValue 
 case setLoading(Bool) 
}



์ด์ œ ๋ณ€ํ™”๋“ค์— ๋Œ€ํ•œ ๊ฐ’์„ ์ €์žฅํ•  ์ƒํƒœ๊ฐ€ ์žˆ์–ด์•ผ๊ฒ ์ฃ ?

๋”ํ•˜๊ฑฐ๋‚˜ ๋บ์„ ๋•Œ์˜ ์ƒํƒœ๋ฅผ ์ €์žฅํ•  number์™€ ๊ทธ ์‚ฌ์ด์— ๋กœ๋”ฉ์„ ๋ณด์—ฌ์ค„ ๋กœ๋”ฉ์˜ ์ƒํƒœ๋ฅผ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

 //์–ด๋– ํ•œ ๋ณ€ํ™”๋ฅผ ๋ฐ›์€ ์ƒํƒœ 
 struct State { 
 var number:Int 
 var isLoading:Bool
 }


์ด์ œ๋ถ€ํ„ฐ๋Š” ๋ฉ”์†Œ๋“œ๋“ค์„ ๋งŒ๋“ค์–ด์ค„๊ฑด๋ฐ์š”.

์•ก์…˜์„ ๋ฐ›์•˜์„ ๋•Œ ๋ณ€ํ™”๋ฅผ ์‹คํ–‰ํ•  mutate ์ž…๋‹ˆ๋‹ค. (Action -> Mutation)

์•„๋ž˜๋ฅผ ๋ณด๋ฉด ๊ฐ ์•ก์…˜์— ๋”ฐ๋ผ ์ ํ•ฉํ•œ ๋ฎคํ…Œ์ด์…˜ ์ด๋ฒคํŠธ๋“ค์ด ๋ฐœ์ƒ๋ฉ๋‹ˆ๋‹ค.

 

 //์•ก์…˜์— ๋งž๊ฒŒ ๋ณ€ํ™”ํ•ด 
 func mutate(action:Action) -> Observable<Mutation> {
 	switch action { 
    	case .plus: 
        	return Observable.concat([
            	Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.increaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)) 
                ]) 
        case .minus: 
        	return Observable.concat([
            	Observable.just(Mutation.setLoading(true)),
            	Observable.just(Mutation.decreaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
            	Observable.just(Mutation.setLoading(false)) 
            	]) 
        	} 
 	}


์œ„ mutate์—์„œ ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ๋“ค์„ ๋ฐ›์•„์„œ ๊ฐ’์˜ ์ƒํƒœ๋ฅผ ๋ณ€ํ™”์‹œ์ผœ์ค„ reduce ์ž…๋‹ˆ๋‹ค. (Mutation -> State)

๋ณ€ํ™”๋œ ์ด๋ฒคํŠธ๋“ค์— ๋”ฐ๋ผ์„œ ๊ฐ’์„ ์ ํ•ฉํ•˜๊ฒŒ ๋ฐ”๊ฟ”์ค๋‹ˆ๋‹ค.

 

 //๋ณ€ํ™”์— ๋งž๊ฒŒ ๊ฐ’์ด ์„ค์ •ํ•ด 
 func reduce(state: State, mutation: Mutation) -> State {
 	var state = state 
    switch mutation { 
    	case .increaseValue:
        	state.number += 1 
        case .decreaseValue: 
        	state.number -= 1 
        case let .setLoading(isLoading):
        	state.isLoading = isLoading 
         } 
         	return state
     }

CounterViewController


์ด์ œ ๋ทฐ์™€ ๋ฆฌ์•กํ„ฐ๋ฅผ ์—ฐ๊ฒฐํ•ด์ฃผ์–ด์•ผ๊ฒ ์ฃ ?

๊ฐ€์žฅ ๋จผ์ € ๋””์Šคํฌ์ฆˆ๋ฐฑ๊ณผ ์œ„์—์„œ ๋งŒ๋“ค์–ด์ค€ CounterViewReactor๋ฅผ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

 let disposeBag:DisposeBag = DisposeBag()
 let counterViewReactor:CounterViewReactor = CounterViewReactor()


์ด์ œ ๋ฆฌ์•กํ„ฐ๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” bind ๋ฉ”์†Œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

func bind(reator:CounterViewReactor) {...}


bind ๋ฉ”์†Œ๋“œ ์•ˆ์— ๊ฐ ๋ฒ„ํŠผ๊ณผ ๋ฆฌ์•กํ„ฐ์˜ ์•ก์…˜์„ ๋ฐ”์ธ๋”ฉํ•ด์ค๋‹ˆ๋‹ค.

 

 //+๋ฒ„ํŠผ๊ณผ reactor์˜ action์„ ๋ฐ”์ธ๋”ฉ 
 plusButton
    .rx 
    .tap 
    .map{CounterViewReactor.Action.plus} 
    .bind(to: reator.action) 
    .disposed(by: disposeBag)
    
//-๋ฒ„ํŠผ๊ณผ reactor์˜ action์„ ๋ฐ”์ธ๋”ฉ 
 minusButton
    .rx 
    .tap 
    .map{CounterViewReactor.Action.minus} 
    .bind(to: reator.action) 
    .disposed(by: disposeBag)


bind ๋ฉ”์†Œ๋“œ ์•ˆ์— ๋ฆฌ์•กํ„ฐ์˜ ์ƒํƒœ๊ฐ’๊ณผ ๋ทฐ์˜ ์ˆซ์ž๋ฅผ ๋‚˜ํƒ€๋‚ผ ๋ ˆ์ด๋ธ”๊ณผ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ๋ฐ”์ธ๋”ฉํ•ด์ค๋‹ˆ๋‹ค.

 

 //reacotor์˜ value์™€ numberLabel์„ ๋ฐ”์ธ๋”ฉ 
 reator.state 
    .map {"\($0.number)"} 
    .distinctUntilChanged() 
    .bind(to: numberLabel.rx.text) 
    .disposed(by: disposeBag) 
    
 //reactor์˜ isLoading๊ณผ loadingIndicator๋ฅผ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ• ์ง€ ๋ง์ง€ ๋ฐ”์ธ๋”ฉ 
  reator.state 
    .map{$0.isLoading} 
    .distinctUntilChanged() 
    .bind(to: loadingIndicator.rx.isAnimating) 
    .disposed(by: disposeBag)
 
 //reactor์˜ isLoading๊ณผ loadingIndicator๋ฅผ ์ˆจ๊ธธ์ง€ ๋ง์ง€ ๋ฐ”์ธ๋”ฉ 
 reator.state 
    .map{!$0.isLoading} 
    .distinctUntilChanged() 
    .bind(to: loadingIndicator.rx.isHidden) 
    .disposed(by: disposeBag)




bind(reator:) ๋ฉ”์†Œ๋“œ์— ๋งจ ์ฒ˜์Œ ๋งŒ๋“ค์–ด์ค€ counterViewReactor๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„ฃ๊ณ  ์‹คํ–‰์‹œ์ผœ์ค๋‹ˆ๋‹ค.

 

 override func viewDidLoad() {
 	super.viewDidLoad() 
    	bind(reator: counterViewReactor)
  }

 


์‹คํ–‰ํ™”๋ฉด


์•„๋ž˜์™€ ๊ฐ™์ด ๋ฒ„ํŠผ์ด ๋ˆŒ๋ฆฌ๋Š” ์•ก์…˜์— ๋”ฐ๋ผ์„œ ๊ฐ’์ด ๋ณ€ํ™”ํ•˜๊ฒŒ ๋˜๊ณ 

๋ณ€ํ™”ํ•œ ๊ฐ’์€ ๋ ˆ์ด๋ธ”๊ณผ ๋กœ๋”ฉ์ธ๋””์ผ€์ดํ„ฐ์— ์ ์šฉ๋˜๋Š” ๋ชจ์Šต์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 


Source Code


์ „์ฒด ์†Œ์Šค ์ฝ”๋“œ๋Š” ์•„๋ž˜ ๊นƒํ—™์œผ๋กœ ๊ฐ€์„œ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”!

 

 

fomagran/ReactorKit-Example

๋ฆฌ์•กํ„ฐํ‚ท ์˜ˆ์ œ ์—ฐ์Šตํ•˜๊ธฐ. Contribute to fomagran/ReactorKit-Example development by creating an account on GitHub.

github.com

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€