iOS/์•„ํ‚คํ…์ฒ˜

[์•„ํ‚คํ…์ฒ˜] ReactorKit์— ๋Œ€ํ•ด์„œ(feat.Flux)

๊ฒฝํ‘ธ 2023. 9. 23. 15:30
๋ฐ˜์‘ํ˜•

์•ˆ๋…•ํ•˜์„ธ์š”.

์˜ค๋Š˜์€ ReactorKit์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

 

ReactorKit?

ReactorKit์˜ ๊นƒํ—ˆ๋ธŒ์— ๋“ค์–ด๊ฐ€ ๋ณด๋ฉด ์†Œ๊ฐœ๊ธ€์˜ ์ฒซ ๋ฌธ์žฅ์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

ReactorKit is a framework for a reactive and unidirectional Swift application architecture.
- ReactorKit์€ ๋ฐ˜์‘ํ˜• ๋ฐ ๋‹จ๋ฐฉํ–ฅ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์œ„ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•์ด๋ผ๋Š” ๋‹จ์–ด๋งŒ ๋ด๋„ Reactive Programming์„ ์œ„ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋ผ๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ˜๋ฉด์— ๋‹จ๋ฐฉํ–ฅ์ด๋ผ๋Š” ๋ง์€ ์กฐ๊ธˆ ์–ด์ƒ‰ํ•˜๊ธฐ๋„ ํ•˜๊ณ  ์‰ฝ๊ฒŒ ๋ญ”๊ฐ€๋ฅผ ๋– ์˜ฌ๋ฆฌ๊ธฐ ์–ด๋ ค์› ๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

ํ•ด๋‹น ๊นƒํ—ˆ๋ธŒ๋ฅผ ์กฐ๊ธˆ ๋” ๋“ค์—ฌ๋‹ค๋ณด๋ฉด

ReactorKit์€ Flux(๋‹จ๋ฐฉํ–ฅ ํ๋ฆ„์˜ ์•„ํ‚คํ…์ฒ˜) + Reactivex(๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ)์„ ์ฝ˜์…‰ํŠธ๋กœ ํ•œ๋‹ค๋Š” ๋ถ€๋ถ„์ด ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ๋ฐฉํ–ฅ ํ๋ฆ„์ด ๋ญ”์ง€ ์ดํ•ดํ•  ํ•„์š”๊ฐ€ ์žˆ์œผ๋‹ˆ Flux๋ฅผ ์กฐ๊ธˆ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

Flux ?

์œ„์˜ ์ด๋ฏธ์ง€์—์„œ ํ™”์‚ดํ‘œ ๋ฐฉํ–ฅ์ด ํ•œ ๋ฐฉํ–ฅ์œผ๋กœ๋งŒ ๊ฐ€๋ฆฌํ‚ค๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„ค๋ช…ํ•˜์ž๋ฉด

์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์„ ๊ธฐ๋ฐ˜์œผ๋กœ Action์„ ๋งŒ๋“ค๊ณ (createAction) ์ด Action์„ Dispatcher์—๊ฒŒ ์ „๋‹ฌํ•˜์—ฌ Store(Model)์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•œ ๋’ค์— View์— ๋ฐ˜์˜ํ•˜๊ฒŒ ๋˜๋Š” ๋‹จ๋ฐฉํ–ฅ์˜ ํ๋ฆ„์ œ์–ด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์•„ํ‚คํ…์ฒ˜์ž…๋‹ˆ๋‹ค.

๋‹จ๋ฐฉํ–ฅ์˜ ํ๋ฆ„์ œ์–ด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  ์–ด๋– ํ•œ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘๋˜๋Š”์ง€ ์ •๋„๋งŒ ์•Œ๋ฉด ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. 

๋” ๊ถ๊ธˆํ•˜์‹ค ์ˆ˜ ์žˆ์œผ์‹ค ๊ฒƒ ๊ฐ™์•„ ๊ณต์‹๋ฌธ์„œ ๋งํฌ ๋‚จ๊ฒจ๋‘๊ฒ ์Šต๋‹ˆ๋‹ค.

 

Flux | ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•„ํ‚คํ…์ณ

์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•„ํ‚คํ…์ณ (ํ•œ๊ตญ์–ด ๋ฒˆ์—ญ)

haruair.github.io

๊ฒฐ๊ณผ์ ์œผ๋กœ ์ด๋Ÿฌํ•œ ๋‹จ๋ฐฉํ–ฅ์˜ ํ๋ฆ„์ œ์–ด์™€ ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ผ๊ณ  ์ƒ๊ฐํ•˜์‹œ๋ฉด ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

ReactorKit์—์„œ ๋‹จ๋ฐฉํ–ฅ ํ๋ฆ„์€ ์•„๋ž˜์˜ ์ด๋ฏธ์ง€์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

View๋ฅผ ํ†ตํ•ด ๋“ค์–ด์˜จ ์ •๋ณด๋ฅผ ํ† ๋Œ€๋กœ Action์„ ๋ฐฉ์ถœํ•˜๊ณ  ์ด๋Š” ๋‹ค์‹œ Reactor์˜ mutate๋ฅผ ํ†ตํ•ด ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์œ„ํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  reduce๋ฅผ ํ†ตํ•ด ์ƒํƒœ(State)๊ฐ€ ๋ณ€๊ฒฝ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์นด์šดํ„ฐ ์•ฑ์ด ์žˆ๋‹ค๊ณ  ํ•ด๋ด…์‹œ๋‹ค.

View๋ฅผ ํ†ตํ•ด ๋ฒ„ํŠผ์˜ ํ„ฐ์น˜ ์ด๋ฒคํŠธ๊ฐ€ ๋“ค์–ด์™”๊ณ  ๋ฏธ๋ฆฌ ์ •์˜ํ•ด ๋‘” Action์—์„œ add๋ฅผ ๋ฐฉ์ถœํ•˜๊ฒŒ ๋˜๋ฉด Reactor์—์„œ ํ•ด๋‹น Action์— ๋Œ€ํ•œ mutate, reduce๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•ด State๋ฅผ ๋ณ€๊ฒฝํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ๋œ State๋Š” View์— ๋‹ค์‹œ ๋ฐ˜์˜๋˜๊ฒŒ ๋˜๊ณ ์š”.

๊ธ€๋กœ๋งŒ ๋ณด๋ฉด ์–ด๋ ต๋”๋ผ๊ณ ์š”.

๊ทธ๋ž˜์„œ ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋ฅผ ์ค€๋น„ํ•ด ๋ดค์Šต๋‹ˆ๋‹ค.

 

์˜ˆ์ œ

๊ฐ„๋‹จํ•˜๊ฒŒ TodoList๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ํ•ด๋‹น TodoList์—์„œ ํด๋ฆญ ์‹œ check ํ‘œ์‹œ๊ฐ€ ๋˜๋Š” ์•ฑ์„ ์˜ˆ์ œ๋กœ ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์ „์ฒด ์ฝ”๋“œ๋Š” ๊นƒํ—ˆ๋ธŒ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”.

 

GitHub - 9oHigh/usket.ReactorKit

Contribute to 9oHigh/usket.ReactorKit development by creating an account on GitHub.

github.com

 

1. ๋ชจ๋ธ

struct TodoItem {
    let id: Int
    let title: String
    var isDone: Bool
}

- ํ…Œ์ด๋ธ” ๋ทฐ์— ํ‘œ์‹œํ•  ์•„์ดํ…œ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

 

2. Reactor

import RxSwift
import ReactorKit

class TodoListReactor: Reactor {
    
    // View๋ฅผ ํ†ตํ•ด ๋“ค์–ด์˜ค๋Š” Action
    enum Action {
        case load
        case toggleItem(Int)
    }
    
    // Action์„ ํ†ตํ•ด ์‹ค์งˆ์ ์œผ๋กœ ํ•  ๋™์ž‘๋“ค
    enum Mutation {
        case setLoading(Bool)
        case setTodoList([TodoItem])
        case setError(Error)
        case toggleItem(Int)
    }
    
    // Mutation(์‹ค์งˆ์ ์œผ๋กœ ํ•  ๋™์ž‘)์— ๋”ฐ๋ผ ์ƒํƒœ๋ฅผ ๋ณ€ํ™”
    struct State {
        var todoList: [TodoItem] = []
        var isLoading: Bool = false
        var error: Error?
    }
    
    // ๋ฐ˜๋“œ์‹œ ์ดˆ๊ธฐ State๊ฐ€ ์žˆ์–ด์•ผํ•จ
    // ์ƒ์„ฑ์ž์—์„œ ๋ฐ›์•„๋„ ์ƒ๊ด€์—†์Œ
    let initialState = State()
    private let todoService: TodoService
    
    init(todoService: TodoService) {
        self.todoService = todoService
    }
    
    // ์•ก์…˜์ด ๋“ค์–ด์˜ค๋ฉด ํ˜ธ์ถœ
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .load:
            return todoService.loadTodoList()
                .map { .setTodoList($0) }
                .startWith(.setLoading(true))
                .catch { .just(.setError($0)) }
        case .toggleItem(let id):
            return .just(.toggleItem(id))
        }
    }
    
    // mutate๊ฐ€ ์‹คํ–‰๋œ ์ดํ›„ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝ
    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .setLoading(let isLoading):
            newState.isLoading = isLoading
        case .setTodoList(let todoList):
            newState.todoList = todoList
            newState.isLoading = false
        case .setError(let error):
            newState.error = error
            newState.isLoading = false
        case .toggleItem(let id):
            if let index = newState.todoList.firstIndex(where: { $0.id == id }) {
                newState.todoList[index].isDone.toggle()
            }
        }
        return newState
    }
}

- Reactor ํ”„๋กœํ† ์ฝœ์— ์ •์˜๋˜์–ด ์žˆ๋Š” Action, Mutation, State๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ ๋กœ ์ €์˜ ๊ฒฝ์šฐ

Action -> Reacotr(mutate) -> Reactor(reduce) -> State์˜ ํ๋ฆ„์œผ๋กœ ์ดํ•ดํ•˜๊ณ  ํ•ด๋‹น ์˜ˆ์ œ๋ฅผ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ ์ž˜๋ชป๋œ ๊ฒƒ์ด๋ผ๋ฉด ๋Œ“๊ธ€ ๋ถ€ํƒ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค ๐Ÿฅฒ

 

3. TodoService

protocol TodoServiceType {
    func loadTodoList() -> Observable<[TodoItem]>
}

class TodoService: TodoServiceType {
    
    func loadTodoList() -> Observable<[TodoItem]> {
        let todoItems = [
            TodoItem(id: 1, title: "์•„์นจ ๋จน๊ธฐ", isDone: false),
            TodoItem(id: 2, title: "์ ์‹ฌ ๋จน๊ธฐ", isDone: false),
            TodoItem(id: 3, title: "์ €๋… ๋จน๊ธฐ", isDone: false)
        ]
        return Observable.just(todoItems).delay(.seconds(1), scheduler: MainScheduler.instance)
    }
}

์‹ค์ œ๋กœ๋Š” ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•˜๋Š” ๋“ฑ์˜ ์ž‘์—…๋„ ์žˆ๊ฒ ์ง€๋งŒ ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

delay๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒƒ์€ State์˜ isLoading์ด true์ธ ๊ฒฝ์šฐ, ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๋Š” ๋กœ์ง์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

 

4. TodoViewController

viewDidAppear์—์„œ reactor?.action.onNext(.load)๋ฅผ ํ†ตํ•ด action์ด load๋ฅผ ๋ฐฉ์ถœํ•˜๊ฒŒ ๋˜๋ฉด

reactor์˜ mutate๊ฐ€ ํ˜ธ์ถœ๋˜๊ณ  ๋‹ค์‹œ reduce๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด์„œ ํ™”๋ฉด์— ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋ณด์ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

import UIKit
import ReactorKit
import RxSwift
import RxCocoa

class TodoViewController: UIViewController, View, UIScrollViewDelegate {
    
    private let tableView = UITableView()
    private let activityIndicator = UIActivityIndicatorView(style: .large)
    var disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // load ์•ก์…˜ ๋ฐฉ์ถœ
        self.reactor?.action.onNext(.load)
    }
    
    func bind(reactor: TodoListReactor) {
        // state๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค todoList๋ฅผ ๋ฐ˜์˜
        reactor.state
            .map { $0.todoList }
            .bind(to: tableView.rx.items (cellIdentifier: "TodoCell", cellType: TodoCell.self)) { row, item, cell in
                cell.configure(item: item)
            }
            .disposed(by: disposeBag)
        
        // state๊ฐ€ ๋ณ€๊ฒฝ๋  ๋–„๋งˆ๋‹ค state์˜ isLoading์„ ํ™•์ธํ•˜๊ณ  indicator์— ๋ฐ˜์˜
        reactor.state.map { $0.isLoading }
            .distinctUntilChanged()
            .subscribe(onNext: { [weak self] isLoading in
                if isLoading {
                    self?.activityIndicator.startAnimating()
                } else {
                    self?.activityIndicator.stopAnimating()
                }
            })
            .disposed(by: disposeBag)
            
        // ์…€ ํด๋ฆญ์‹œ toggleItem ์•ก์…˜์„ reactor ์•ก์…˜์— ๋ฐ”์ธ๋”ฉ
        tableView.rx.modelSelected(TodoItem.self)
            .map { .toggleItem($0.id) }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        reactor.state.map { $0.error }
            .compactMap { $0 }
            .subscribe({ [weak self] error in
                let alertViewController = UIAlertController(title: "๊ฒฝ๊ณ ", message: "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", preferredStyle: UIAlertController.Style.alert)
                
                let alertAction = UIAlertAction(title: "์˜คํ‚ค", style: UIAlertAction.Style.default) { action in
                    alertViewController.dismiss(animated: true)
                }

                alertViewController.addAction(alertAction)
                self?.present(alertViewController, animated: true)
            })
            .disposed(by: disposeBag)
        
        tableView.rx.setDelegate(self)
            .disposed(by: disposeBag)
    }
    
    ...
}

 

 


 

๋น„๋ก ๊ฐ„๋‹จํ•œ ์•ฑ์ด์ง€๋งŒ

์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋‹ˆ ์กฐ๊ธˆ ๋” ๋ณต์žกํ•œ ์•ฑ์ด๋ผ๋„ ํฌ๊ฒŒ ์–ด๋ ต์ง€ ์•Š์„ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ๋Š๋‚Œ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค.

ReactorKit์œผ๋กœ ์กฐ๊ทธ๋งˆํ•œ ํ”„๋กœ์ ํŠธ๋ฅผ ํ•œ ๋ฒˆ ํ•ด๋ด์•ผ๊ฒ ๋„ค์š”.

 

์•„๋ฌด์ชผ๋ก ๋„์›€์ด ๋˜์‹œ๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ ์ด๋งŒ ๐Ÿ‘‹๐Ÿป ๐Ÿ‘‹๐Ÿป ๐Ÿ‘‹๐Ÿป

๋ฐ˜์‘ํ˜•