YunDaeHyeon
← Lab

UITableView에 대해

스터디2025.06.17
  • UIKit
  • Swift

🤔 Table View란?

UITableView에 대해 학습하기 전, UIKit에서의 Table View의 정의가 궁금했다.

Dispay data in a single column of customizable rows.

공식 문서에서의 Table View는 커스터마이징이 가능한 단일 열의 데이터 표시 행이라고 한다.

이때 단일 열이라는 점에서 CollectionView와의 차이가 있다고 생각이 든다.

또 테이블 뷰는 세로로 컨텐츠를 스크롤 할 수 있으며 행과 섹션으로 구분된다.

문서에서의 TableViews 는 다음과 같은 객체들간의 집합이라고 말한다.

  1. Cell

→ 테이블 뷰에 데이터를 시각적으로 표시하기 위한 각각의 행을 말한다. → UIKit에서 제공하는 기본 셀 디자인을 사용하거나 사용자 지정 셀을 정의할 수 있다.

  1. TableViewController

→ https://developer.apple.com/documentation/uikit/uitableviewcontroller

→ TableView를 관리하기 위해 사용되는 컨트롤러이다.

  1. DataSource → https://developer.apple.com/documentation/uikit/uitableviewdatasource

→ 데이터 소스 객체이며 이 프로토콜을 채택함으로써 테이블에 표시하기 위한 데이터를 바인딩 할 수 있다.

  1. Delegate

→ https://developer.apple.com/documentation/uikit/uitableviewdelegate

→ 델리게이트 객체이며 이 프로토콜을 채택함으로써 테이블 컨텐츠와의 상호작용을 관리할 수 있다.


ViewController에서 TableView를 사용하기 위해선 DataSource와 Delegate를 채택해야한다.

📌 UITableViewDataSource

UITableViewDataSource가 하는 일은 아래와 같다.

  • 표의 섹션과 행의 수를 보고합니다.
  • 표의 각 행에 대한 셀을 제공합니다.
  • 섹션 헤더와 푸터에 제목을 제공합니다.
  • 테이블의 인덱스가 있는 경우 이를 구성합니다.
  • 기본 데이터를 변경해야 하는 사용자 또는 테이블에서 시작한 업데이트에 응답합니다.

즉, TableView가 어떤 데이터를 표시할지 제공해주는 프로토콜이 DataSource 이다!

UITableView를 사용하기 위해 반드시 채택해야하는 프로토콜이다.

DataSource를 채택할 시 필수로 구현해야하는 메서드는 다음과 같다.

numberOfRowsInSection은 각 세션별 행(row)의 개수를 반환한다!

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
}

이때, numberOfSections 을 통해 섹션의 개수를 설정할 수 있다.

하지만 필수는 아니며 기본값은 1이다.

cellForRowAt은 테이블뷰가 특정 위치(indexPath)에 표시할 셀을 요청 했을 때 그 셀을 만들거나, 재사용하여 되돌려주는 역할을 수행한다.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
     guard let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as? CustomTableViewCell else {
         return UITableViewCell()
     }
     cell.configure(with: items[indexPath.row])
     return cell
}

이때 셀을 만들거나 재사용한다는 의미는 dequeueReusableCell() 등을 사용하여 레지스터에 등록해둔 ReuseIdentifier 에 해당하는 셀을 큐에서 꺼내어 생성하거나 재활용함을 의미한다!


📌 UITableViewDelegate

UITableViewDelegate가 하는 일은 다음과 같다.

  • 사용자 정의 헤더 및 푸터 보기를 만들고 관리합니다.
  • 행, 머리글, 바닥글에 사용자 정의 높이를 지정합니다.
  • 더 나은 스크롤 지원을 위해 높이 추정값을 제공합니다.
  • 행 내용을 들여쓰기합니다.
  • 행 선택에 응답합니다.
  • 테이블 행에서 스와이프 및 기타 작업에 응답합니다.
  • 표의 내용 편집을 지원합니다.

즉, TableView의 행위 혹은 UI 관련 이벤트와 인터랙션 제어를 담당한다.

대표적으로 셀이 선택되었을 때 이벤트, 각 셀의 높이, 헤더와 푸터 등 UI를 커스텀할 때 활용된다.

Delegate의 경우 채택했을 때 필수로 구현해야하는 메서드는 없지만 대표적으로 활용되는 메서드는 heightForRowAt과 didSelectRowAt이 있다.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return 80 // 원하는 높이
}

heightForRowAt의 경우 각 셀의 높이를 지정할 수 있으며 기본값은 tableView.rowHeight 값이다.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    print("\(indexPath.row)번째 셀이 선택되었어요!")
}

didSelectRowAt은 사용자가 특정 셀을 탭했을 때, 클릭된 탭의 index를 가져온다.


📌 직접 만들어보기

백문이 불여일견. 직접 만들어보면서 학습해보자.

아래 예제는 100% 코드베이스 기반으로 작성하였습니다. 😉

1. ViewController에 TableView 인스턴스 생성 후 바인딩

//
//  ViewController.swift
//  TableViewStudy
//
//  Created by 윤대현 on 9/16/25.
//

import UIKit

class ViewController: UIViewController {
    
    private let tableView = UITableView()
    
    // 테이블뷰에 표시할 더미데이터
    private var items: [String] = [
        "1번째 셀",
        "2번째 셀",
        "3번째 셀",
        "4번째 셀",
        "5번째 셀"
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupTableView()
    }
    
    private func setupUI() {
        self.view.backgroundColor = .black
        self.title = "제목"
        
        view.addSubview(tableView)
        // Auto Resizing Mask 기능 끄기 (오토 레이아웃을 위해!)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        // 상단은 safe area, 나머지는 화면 꽉차게
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    private func setupTableView() {
        tableView.dataSource = self
        tableView.delegate = self
				// 재사용할 셀의 타입을 String 형태의 identifier로 변환해 레지스터 저장
        tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: "CustomCell")
    }
}

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as? CustomTableViewCell else {
            return UITableViewCell()
        }
        cell.configure(with: items[indexPath.row])
        return cell
    }
}

2. UITableViewCell 작성

//
//  CustomTableViewCell.swift
//  TableViewStudy
//
//  Created by 윤대현 on 9/16/25.
//

import UIKit

class CustomTableViewCell: UITableViewCell {
    
    let titleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16)
        label.textColor = .label
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupUI()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupUI()
    }
    
    private func setupUI() {
        contentView.addSubview(titleLabel)
        
        NSLayoutConstraint.activate([
            titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
            titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)
        ])
    }
    
    func configure(with text: String) {
        titleLabel.text = text
    }
}

위 코드를 실행하면 아래와 같이 나온다!

3. TableView 스타일 설정

이때 ViewController에서 private let tableView = UITableView()을 통해 UITableView를 생성했는데, 인스턴스를 생성할 때 따로 생성자를 넘겨주면 TableView의 스타일을 설정할 수 있다.

UITableView의 생성자(init(frame:style:) 에 넘길 수 있는 값은 다음과 같다.

실제 코드로 어떤 차이가 있는지 확인해보자!

.plain

.grouped

.insetGrouped

기본 값은 plain으로 보인다. 다만, grouped과 plain 차이는 패딩과 배경색이 살짝 회색으로 변하는 거 빼고는 차이가 없는 것 같다.

확실히 디자인 적으로 예쁜 스타일은 insetGrouped인 것 같다.

4. 섹션 구분하기

약간 심화로 들어가서 섹션의 개수를 3개로 나누고, 각각 섹션에 셀을 넣어보자.

private let header: [String] = [
     "섹션 1",
     "섹션 2",
     "섹션 3"
]

ViewController 내부에 섹션의 헤더로 표시하기 위한 문자열 배열을 선언 한 뒤 numberOfSections과 titleForHeaderInSesction 을 통해 섹션을 구분지어보자.

func numberOfSections(in tableView: UITableView) -> Int {
    header.count
}

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    header[section]
}

스타일을 .insetGrouped로 했을 때 실행 결과는 다음과 같다.

5. 셀의 높이와 클릭 이벤트

이번엔 UITableViewDelegate를 사용하여 각 셀의 높이와 클릭 시 셀이 초록색으로 보이도록 해보자.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return 100
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.cellForRow(at: indexPath)?.backgroundColor = .green
}

다음과 같이 동작함을 확인할 수 있다!

6. 셀 스와이프

셀을 오른쪽이나 왼쪽으로 스와이프 했을 때 버튼이 나오는 동작을 종종 볼 수 있다.

그 동작을 구현하기 위해선 Delegate 에 존재하는 trailingSwipeActionsConfigurationForRowAt와 leadingSwipeActionsConfigurationForRowAt 을 구현하면 된다.

  • trailingSwipeActionsConfigurationForRowAt : 오른쪽 → 왼쪽
  • leadingSwipeActionsConfigurationForRowAt : 왼쪽 → 오른쪽 스와이프

반환되는 타입은 UISwipeActionsConfiguration이므로 UIContextualAction을 사용해 구현하면 된다.

// 오른쪽 -> 왼쪽 스와이프
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    let modity = UIContextualAction(style: .normal, title: "수정") { (UIContextualAction, UIView, success: @escaping (Bool) -> Void) in
        print("수정 클릭")
        success(true)
    }
    modity.backgroundColor = .systemBlue
    
    let delete = UIContextualAction(style: .normal, title: "삭제") { (UIContextualAction, UIView, success: @escaping (Bool) -> Void) in
        print("삭제 클릭")
        success(true)
    }
    delete.backgroundColor = .systemRed
    
    return UISwipeActionsConfiguration(actions:[delete, modity])
}

반환될 때 actions: []이 있는데 actions 배열의 index가 0인 요소가 우측에 붙는다.

실제 실행 화면은 다음과 같다.


🤔 UITableView와 UITableViewController의 차이는 뭘까?

평소 테이블뷰를 구현하기 위해 ViewController에 TableView 인스턴스를 생성한 뒤, DataSource와 Delegate를 코드로 연결해서 사용을 많이 한다.

문뜩 UIViewController에 UITableView 를 사용하는 방식과 애초에 UITableViewController을 사용하는 방식이 어떤 차이점이 있는지 궁금해서 UITableViewController의 정의를 공식 문서에서 찾아보았다.

TableViewController는 테이블뷰의 컨텐츠를 관리하고 대응할 때 필요한 프로토콜을 모두 채택하고 있다.

그 외에도 아래와 같은 요소들이 있다.

  • 셀 선택/해제 시 UI 자동 처리
  • ScrollView 이벤트 자동 처리
  • Pull-to-Refresh 등 TableView를 커스텀하기 위한 기능을 쉽게 구현할 수 있음

또한 Static Cell과 같은 정적 셀을 구현하기 위해선 반드시 UITableViewController를 사용해야한다고 한다.

즉.. 정적 셀을 구성하기 위해선 UITableViewController를 사용해 구현해야하고, 동적 셀을 구현하기 위해선 ViewController + UITableView 방식과 UITableViewController 모두 활용할 수 있다고 이해했다.

실제 UITableView 방식으론 Static Cell을 구현할 수 없는지 확인해보자.

새로운 프로젝트를 생성한 뒤 ViewController에 UITableView를 끼워넣었다.

그 뒤 인스펙터에서 속성을 Static Cells로 변경하니 다음과 같은 오류가 출력되며 정적 셀을 구성하기 위해선 UITableViewController 을 사용해야 한다는 걸 확인할 수 있었다.

최종적으로 ViewController + UITableView 방식과 UITableViewController 의 차이점을 간단하게 표로 작성한 결과는 다음과 같다.

구분UITableViewControllerUIViewController + UITableView
Delegate / DataSource자동 연결 (별도 지정 불필요)직접 지정 필요 (tableView.dataSource = self)
셀 선택/해제 처리자동 반응 (선택 상태 유지/해제 관리)선택/해제를 직접 구현해야 함
ScrollView 관련 처리자동 처리 (키보드, inset 조정 등 기본 제공)별도 코드 작성 필요
Pull-to-RefreshrefreshControl 프로퍼티로 바로 적용 가능UIRefreshControl을 추가 후 수동 연결 필요
구조 단순성테이블만 있는 단순 화면에 적합기본 화면 구조를 빠르게 구축 가능
다른 UI 배치제약적 (테이블 외 UI를 추가하기 까다로움)자유롭게 다양한 View와 혼합 가능
확장성단순한 목록 뷰에 유리복잡한 화면 구성, 오버레이, 상태 뷰 추가에 유리
복수 TableView 지원기본적으로 1개 테이블에 최적화여러 개의 TableView/CollectionView 혼합 가능
추천 사용 상황단순 리스트, 빠른 프로토타입실제 서비스용, 복잡한 UI/비즈니스 로직 필요 시