Skip to content →

ViewController는 죄가 없다 – 2

일단 한 번 만들어봅시다.

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

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

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:

  • Your data source object. This object adopts the UITableViewDataSource protocol and provides the data for the table.
  • Your delegate object. This object adopts the UITableViewDelegate protocol and manages user interactions with the table’s contents.

제가 영어를 엄청 잘 하진 않습니다만, 제게는 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()
    }
}

DataSource만 다른 파일로 분리해 놓고 나니, ViewController가 훨씬 가벼워진 모습입니다. 20줄 밖에 줄지 않지 않았냐고요? 그렇게 생각 할 수도 있습니다. 하지만

  1. 메소드의 개수가 줄었고
  2. 각 메소드들이 떠 짧거나 명확해졌습니다.

특히 제가 강조하고 싶은 부분은,  @IBAction func segmentSelected 메소드에서 더 잘 드러납니다.

// 옛날 버전. "세그먼트 콘트롤이 눌리면 테이블뷰를 리로드하라"로 밖에 읽히지 않는다.
@IBAction func segementSelected(_ sender: UISegmentedControl) {
    tableView.reloadData()
}

// 리팩토링버전. "선택된 세그먼트에 해당되는 데이터로 테이블을 채워라"라는 의도가 보인다.
@IBAction func segementSelected(_ sender: UISegmentedControl) {
    tableView.dataSource = dataSources[sender.selectedSegmentIndex]
    tableView.reloadData()
}

리팩토링 결과, “선택된 세그먼트에 해당되는 내용으로 테이블을 채워라”라는 의도를 더 잘 담게 되었네요. 이전 코드를 보면, “세그먼트가 눌리면 테이블뷰를 리로드해라” 라는 식으로 밖에 읽히지 않지요.

한 편, 위에서 분리해낸 각각의 DataSource는 아래와 같습니다.

class SmileyDataSource: NSObject, UITableViewDataSource {
    var dataList: [String] = []
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let item = dataList[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "smileyCell", for: indexPath)
        cell.textLabel?.text = item
        return cell
    }
}
class CatDataSource: NSObject, UITableViewDataSource {
    var dataList: [String] = []
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let item = dataList[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "catCell", for: indexPath)
        cell.textLabel?.text = item
        return cell
    }
}

원래는 하나의 DataSource 안에서 여러번 if-문이 등장했지만, DataSource가 두 개가 되니 if-문이 전혀 보이지 않게 되었네요. 또한 코드의 의도도 훨씬 더 잘 전달 됩니다. “CatDataSource는 Cat들을 어떻게 보여주는지를 담당한다!”는 의도를 누구라도 읽을 수가 있지요.

이전의 DataSource의 의도를 한 줄로 표현 하면 어떻게 될까요? “SegementControl에서 Cat이 선택되었을 때는 Cat들을 보여주는 방법을 담당하고 Smiley가 선택되었을 때는 Smiley들을 보여주는 방법을 담당한다!” 정도가 되려나요? 아주 못 알아먹진 않을 정도지만, 여기서 조금만 더 복잡해 지면 이해하기가 아주 어려울 것 같습니다.

맺으며

DataSource를 별도의 객체, 별도의 파일로 관리해서 얻는 이득은 이 이외에도 아주 많습니다.

  • 다른 ViewController에서 재활용 할 수 있게 되고
  • Test하기 쉬워지고
  • Cell들이 ViewController에 대해 지나치게 많이 알게 되는 것을 미연에 막을 수도 있게 됩니다.

아마 대부분의 코드에서 UITableViewDataSource만 분리해 내도, ViewController의 크기는 꽤나 홀쭉해질 것입니다. 게다가 분리하기가 그리 어렵지도 않지요. 가장 중요한 정보인 tableView는 모든 UITableViewDataSource들이 자연스럽게 알고있고(파라미터로 받으니까), 또 꼭 필요한 정보들은 ViewController에서 알려주면 되니까요.

어떠신가요? 벌써 리팩토링하고 싶어 손이 글질근질 하지 않으신가요?

Published in 프로그래밍

Comments

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d 블로거가 이것을 좋아합니다: