cleanUrl: /programming/do-not-blame-mvc-2/

일단 한 번 만들어봅시다.

아주 잠시만 시간을 내서 아래 앱을 만들어보시겠어요? 아주 간단한 앱입니다. “Cat”세그먼트를 클릭하면 고양이 이모지 셀들이 보이고, “Smiley”세그먼트를 클릭하면 스마일리 이모지 셀들이 보이는 앱입니다. 바쁘시면 그냥 눈을 감고 상상코딩을 해보세요.

1lagN0GAFJVk18tkEsIQ8ig.gif

다 만드셨나요? 그렇다면 아래 코드를 봐주세요. 혹시 이런 식으로 만들지는 않으셨는지요?

class ModelController: NSObject {
    var kittisList = ["🐱", "😹", "😼", "😸", "😽", "😾"]
    var smileyList = ["😐", "😂", "😏", "😊", "😊", "😠", "😱"]
}

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    // MARK: - View
    @IBOutlet var tableView: UITableView!
    @IBOutlet var segmentControl: UISegmentedControl!

    // MARK: - Model
    let modelController = ModelController()

    // MARK: - LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
    }

    override func viewWillAppear(_ animated: Bool) {
        tableView.reloadData()
    }

    // MARK: - Actions
    @IBAction func segementSelected(_ sender: UISegmentedControl) {
        tableView.reloadData()
    }

    // MARK: - UITableViewDataSource

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if segmentControl.selectedSegmentIndex == 0 {
            return modelController.kittisList.count
        }
        else {
            return modelController.smileyList.count
        }
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if segmentControl.selectedSegmentIndex == 0 {
            let cell = tableView.dequeueReusableCell(withIdentifier: "catCell", for: indexPath)
            cell.textLabel?.text = modelController.kittisList[indexPath.row]
            return cell
        }
        else {
            let cell = tableView.dequeueReusableCell(withIdentifier: "smileyCell", for: indexPath)
            cell.textLabel?.text = modelController.smileyList[indexPath.row]
            return cell
        }
    }
}

여러분이 만든, 혹은 만들려던 형태와 비슷한가요? 그렇다면 이 글이 도움이 될 지도 모르겠습니다.

위 코드를 보면, 별 것 아닌 앱인데도, 벌써 ViewController가 50줄을 넘으려 하네요. 로직 몇 개만 더하면 100줄을 넘는 건 일도 아닐 듯 합니다.

그게 당연하다고 생각하시나요? 이 정도는 짧은 거라고 생각하시나요? 아닙니다. 절대 그렇지 않습니다. 이게 짧다고 느끼는 건, 마치 오래 사용해서 누렇게 된 체육복을 입으면서 “체육복은 원래 노랗지”라고 생각하게 되는 것과 비슷한 상황입니다. 체육복은 빨면 하얗게 될 수 있습니다.

위 코드를 보면 절반 가량을 UITableViewDataSource관련 함수들이 잡아먹고 있습니다. 우리는 종종 이는 불가피한 일이라고 생각하기도 합니다. 심지어 UITableView를 만들자마자 무의식적으로 tableView.dataSource = self 와 같은 코드를 쓰기도 하죠.

이런 습관이 생긴 데에는 애플의 책임도 적지 않습니다. 많은 예제코드와 템플릿 코드에서 애플이 직접 그런 코드를 쓰니까요. 예를들어 Xcode에서 새로운 UITableViewController를 만들면, 즉시 그 자신을 UITableViewDataSource로, 그리고 UITableViewDelegate로 설정해놓은 코드가 만들어집니다.

하지만 정작 UITableView에 관한 애플의 문서에서는 다음과 같이 말하고 있습니다.

Table views are a collaboration between many different objects, including:

제가 영어를 엄청 잘 하진 않습니다만, 제게는 UITableViewDataSource가 반드시 UIViewController일 필요는 없다는 내용을 행간에 담고 있는 듯 보입니다.

다시 한 번 만들어 봅시다

UITableViewDataSource를 별도의 객체로 만들어 위 코드를 리팩토링 해봅시다.

class ViewController: UIViewController {

    // MARK: - View
    @IBOutlet var tableView: UITableView!
    @IBOutlet var segmentControl: UISegmentedControl!

    // MARK: - DatSources
    var dataSources:[UITableViewDataSource] = []
    let catDataSource = CatDataSource()
    let smileyDataSOurce = SmileyDataSource()

    // MARK: - Model
    let modelController = ModelController()

    // MARK: - LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()

        catDataSource.dataList = modelController.kittisList
        smileyDataSOurce.dataList = modelController.smileyList
        dataSources = [catDataSource, smileyDataSOurce]

        tableView.dataSource = dataSources[0]
        tableView.tableFooterView = UIView(frame: CGRect.zero)
    }

    override func viewWillAppear(_ animated: Bool) {
        tableView.reloadData()
    }

    // MARK: - Actions
    @IBAction func segementSelected(_ sender: UISegmentedControl) {
        tableView.dataSource = dataSources[sender.selectedSegmentIndex]
        tableView.reloadData()
    }
}