이 포스팅은 iOS Experience 시리즈 18 편 중 6 번째 글 입니다.

  • Part 1 - 01: 스토리보드의 장단점
  • Part 2 - 02: 코드리뷰 Part 1
  • Part 3 - 03: 코드리뷰 Part 2
  • Part 4 - 04: IBOutlet에서의 Optional
  • Part 5 - 05: Optional Chaining의 동작 방법
  • Part 6 - This Post
  • Part 7 - 07: 코드리뷰 Part 3
  • Part 8 - 08: 패키지 매니저
  • Part 9 - 09: URL Loading System
  • Part 10 - 10: Lazy를 잘 안쓰는 이유
  • Part 11 - 11: iOS Gitignore
  • Part 12 - 12: Toast UI에 대한 생각
  • Part 13 - 13: XCTest
  • Part 14 - 14: RunLoop
  • Part 15 - 15: UIApplication
  • Part 16 - 16: 코드리뷰 Part 4
  • Part 17 - 17: MVC to MVVM
  • Part 18 - 18: VIPER
▼ 목록 보기

dequeueReusableCell

let korean: [String] = ["가","나","다","라","마","바","사","아","자","차","카","타","파","하"]
let english: [String] = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]

extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch section {
        case 0:
            return korean.count
        case 1:
            return english.count
        default:
            return 0
        }
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        
        let text: String = indexPath.section == 0 ? korean[indexPath.row] : english[indexPath.row]
        
        if indexPath.row == 1 {
            cell.backgroundColor = UIColor.red
        }
        cell.textLabel?.text = text
        return cell
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return section == 0 ? "한글" : "영어"
    }
}
  • 테이블 뷰에서 가장 중요한 것은 무엇보다 재사용 셀이다.
  • 대용량의 데이터를 보여주기 위해 이러한 선택은 필수 불가결하다.
  • 실제로 이를 알아보기 위해 indexPath의 row가 1인 경우 빨간색으로 칠하게 해주었다.
  • 만약 재사용이 되지 않는다면, 각각의 섹션에서 1번째 row만이 빨간색으로 칠해져야 한다.

스크린샷 2021-07-06 오후 11 39 45table view cell을 재사용한 경우

  • 결과는 그렇지 않았다. 아래로 스크롤할 수록 빨간 row는 많이 등장했다.
  • 다시 위로 스크롤하고 내리면 점점 더 많이 생긴다.
  • 이는 기존에 빨간색으로 칠해진 셀이 흰색으로 다시 바뀌지 않았기 때문.
extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch section {
        case 0:
            return korean.count
        case 1:
            return english.count
        default:
            return 0
        }
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
//        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let cell: UITableViewCell = UITableViewCell()
        
        let text: String = indexPath.section == 0 ? korean[indexPath.row] : english[indexPath.row]
        
        if indexPath.row == 1 {
            cell.backgroundColor = UIColor.red
        }
        cell.textLabel?.text = text
        return cell
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return section == 0 ? "한글" : "영어"
    }
}

  • 이번에는 한번 인스턴스를 요청할 때마다 계속 생성해 보았다.

스크린샷 2021-07-06 오후 11 46 38인스턴스를 생성한 경우

  • 예상한 결과가 도출되었다.

Empty Cell을 return 해야 하는 경우

  • Json을 파싱해서 데이터를 뿌려주고 싶은 상황이다.
  • 그런데, 받아온 데이터가 완전하지 못하다.
  • 아래와 같은 Json 파일이 있다고 하자.
[
      {"korean_name":"한국","asset_name":"kr"},
      {"korean_name":"독일","asset_name":"de"},
      {"korean_name":"중국","asset_name":"cn"},
      {"korean_name":"이탈리아","asset_name":"it"},
      {"korean_name":"미국","asset_name":"us"},
      {"korean_name":"프랑스","asset_name":"fr"},
      {"korean_name":"일본","asset_name":"jp"}
      
]
  • 이 상황에서, 사진을 함께 보여주어야 하는데, 해당 사진의 이름은 asset_name앞에 flag가 붙은 상태이다.
  • 즉, flag_kr이 사진의 이름이 되는 것
  • 그런데 중국의 사진이 없는 상황이다. 이러한 경우 파싱은 가능하지만, 해당 뷰를 뿌려주기 위한 사진이 없기 때문에 조치가 필요하다.
// MARK: - TableViewDataSource
extension MainViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = self.tableView?.dequeueReusableCell(withIdentifier: Constants.mainCellIdentifier, for: indexPath) as? MainTableViewCell else {
            fatalError("ReuseCell 다운 캐스팅 실패")
        }
        let insertCountry = self.countryList[indexPath.row]
        guard let image = UIImage(named: insertCountry.imageAssetName) else {
            fatalError("image 불러오기 실패")
        }
        cell.countryImageView?.image = image
        cell.countryLabel?.text = insertCountry.koreanName
        return cell
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.countryList.count
    }
}
  • 이와 같은 상황을 처리하기 위해, 일단 dequeue로 셀을 가져오긴한다.
  • 이 경우, 일단 cell을 customCell로 다운캐스팅해야하는데 실패할 경우 에러를 띄워 이를 해결한다.
  • 또한 현재 파싱된 데이터에 대한 이미지가 없을 경우도 에러를 띄운다.
  • 이렇게 할경우 개발을 하는 도중에, 어디서 문제가 발생했는지 알 수 있다.
  • 하지만, 런타임에서 잘못된 데이터가 들어온 경우 크래시가 나버린다.
  • 이는 굉장히 심각한 문제를 야기한다.
  • 그렇기 때문에 유효성 검사를 잘하는 것이 중요하다.
  • 지금은, 그런 유효성 검사를 통과했음에도 불구하고 해당 문제를 런타임에서 문자가 없도록 하는 방법을 고민해볼 것이다.
// MARK: - TableViewDataSource
extension MainViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = self.tableView?.dequeueReusableCell(withIdentifier: Constants.mainCellIdentifier, for: indexPath) as? MainTableViewCell else {
            print("ReuseCell 다운 캐스팅 실패")
            return tableView.emptyCell(at: indexPath)
        }
        let insertCountry = self.countryList[indexPath.row]
        guard let image = UIImage(named: insertCountry.imageAssetName) else {
            print("image 불러오기 실패")
            return tableView.emptyCell(at: indexPath)
        }
        cell.countryImageView?.image = image
        cell.countryLabel?.text = insertCountry.koreanName
        return cell
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.countryList.count
    }
}
  • 위에서는 Error를 던져서 컴파일 타임에 멈추도록 했지만, 이번에는 빈 Cell을 리턴하도록 했다.
  • Guard문 같은 경우 조기 종료를 하기 위해 Return을 꼭해야 하는데, 해당 함수의 Return Type이 UITableViewCell이기 때문에 빈 Cell을 던져주는 것으로 문제를 해결.
  • 그런데, 가장 위의 예에서도 보았지만, 만약 이러한 잘못된 데이터가 많은 경우 위의 코드는 많은 양의 빈 Cell 인스턴스를 만들수 있다.
  • 상당한 메모리 낭비와 부족상태를 야기할 수 있다.
  • 그렇기 때문에 이러한 빈 셀 역시도 Dequeue 방법을 사용해야 한다.
import UIKit

extension UITableView {
    private static let emptyCellIdentifier = "_UITableViewEmptyCell" // 내부에서만 사용한 identifier
    
    /// 빈 tableviewcell(기본)을 ReusableCell에 등록하고 사용한다.
    func emptyCell(at indexPath: IndexPath) -> UITableViewCell {
        self.register(UITableViewCell.self, forCellReuseIdentifier: UITableView.emptyCellIdentifier)
        
        return self.dequeueReusableCell(withIdentifier: UITableView.emptyCellIdentifier, for: indexPath)
    }
}
  • 방법중의 하나로 이러한 것을 생각해볼 수 있다.
  • UITableView를 확장하고, 빈 셀을 리턴할 때마다, Dequeue해서 반환해주는 것.
  • 이 때, 기존의 Dequeue 같은 경우는 화면을 벗어날 경우 자동으로 Dequeue로 들어갔지만, 이번에는 이러한 등록과정을 거쳐주어야 한다.
  • 새롭게 빈 셀을 요청할 때마다, 해당 Cell을 등록하고, Dequeue하여 넘겨준다.

인스턴스 생성후 넘겼을 때 소멸자 호출

  • 그러면 실제로 뷰에서 넘어간 경우, 인스턴스가 메모리에 남아있을까?
  • 정말 위의 방법대로 해야되는지 궁금했다.
class CustomUITableViewCell: UITableViewCell {
    
    deinit {
        print("TestTableViewCell deinit")
    }
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
//        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let cell: CustomUITableViewCell = CustomUITableViewCell()
        
        let text: String = indexPath.section == 0 ? korean[indexPath.row] : english[indexPath.row]
        
        if indexPath.row == 1 {
            cell.backgroundColor = UIColor.red
        }
        cell.textLabel?.text = text
        return cell
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return section == 0 ? "한글" : "영어"
    }
}
  • 이렇게 기본 UITableview 를 상속하여 소멸자에 print를 추가하고 실행시켜보았다.
TestTableViewCell deinit
TestTableViewCell deinit
TestTableViewCell deinit
TestTableViewCell deinit
TestTableViewCell deinit
TestTableViewCell deinit
TestTableViewCell deinit
TestTableViewCell deinit
TestTableViewCell deinit
TestTableViewCell deinit
  • 스크롤을 함에 따라, 소멸자가 호출되는 것을 알 수 있었다.
  • 결국.. 사실 dequeue를 쓰나 안쓰나, 뷰에 보이는 셀에 대해 유한개의 인스턴스만 유지된다는 것을 알 수 있다.
  • 하지만 Dequeue를 썼을 때, 해당 인스턴스를 생성하고 소멸하지 않으므로
  • 이러한 부하에 있어서 이를 줄일 수 있다.
  • 생성자, 소멸자 호출은 Cost가 비싼 작업이기 때문이다.
  • Dequeue를 쓰는 경우, 실제로 해당 셀 인스턴스를 초기화하는 작업을 진행하기 때문에 보다 싸다고 할 수 있다.