UITableView — iOS에서 세로 목록을 표시하기 위한 UIKit의 주요 구성 요소입니다. 테이블의 각 행은 미리 정의된 스타일을 가진 UITableViewCell입니다. UITableView의 기본 사항인 DataSource, 델리게이트, 셀 재사용 및 iOS 앱에서의 테이블 성능에 대해 설명합니다. Apple(Human Interface Guidelines, 2026)에 따르면 UITableView는 iOS에서 가장 많이 사용되는 UI 구성 요소로 남아 있으며, App Store 상위 100개 앱 중 85%에 최소 하나의 테이블이 포함되어 있습니다. 복잡한 레이아웃의 경우 UICollectionView에 관한 문서를 읽어보세요.
핵심 포인트
UITableView는 단일 열에 스크롤 가능한 행 목록을 표시하도록 설계된 UIKit 프레임워크의 클래스입니다. 각 행은 UITableViewCell 객체로 표현됩니다. UITableView는 iPhone OS 2(2008)에서 처음 등장했으며 그 이후로 iOS에서 목록을 위한 주요 구성 요소로 자리 잡았습니다. UITableView의 아키텍처는 MVC 패턴을 기반으로 합니다. 데이터는 DataSource가, 모양과 동작은 Delegate가, 표시는 테이블 자체가 관리합니다.
두 가지 테이블 스타일: .plain(선택적 섹션이 있는 연속 목록)과 .grouped(모서리가 둥글고 여백이 있는 그룹화된 섹션)입니다. iOS 13에서는 .insetGrouped가 추가되었습니다 — 가장자리에 여백이 있는 그룹 스타일로, 설정 및 건강 앱에서 사용됩니다. 스타일 선택은 기본 모양에 영향을 미칩니다. plain 테이블은 스크롤 시 섹션 헤더를 상단에 고정하고(sticky header), grouped 테이블은 그렇지 않습니다.
iOS 8부터 UITableView는 셀프 사이징 셀을 지원합니다. 활성화하려면 tableView.estimatedRowHeight(예: 80)와 tableView.rowHeight = UITableView.automaticDimension을 설정하세요. 테이블은 셀 내부의 Auto Layout 제약 조건을 기반으로 행 높이를 자동으로 계산합니다. 셀프 사이징은 복잡한 셀의 경우 성능이 저하되므로 500개 이상의 요소가 있는 테이블에는 고정 높이(rowHeight)를 사용하세요.
UITableViewCell은 사용자 지정 셀을 만들 필요 없이 80%의 시나리오를 처리하는 4개의 내장 스타일을 제공합니다. 각 스타일은 textLabel, detailTextLabel 및 imageView의 조합으로 구성됩니다. 스타일은 init(style:reuseIdentifier:)를 통해 셀을 초기화할 때 선택됩니다.
| 스타일 | textLabel | detailTextLabel | imageView | 예시 |
|---|---|---|---|---|
| .default (.basic) | 왼쪽, 굵게 | 없음 | 선택 사항 | 메뉴, 항목 목록 |
| .subtitle | 왼쪽, 굵게 | textLabel 아래, 회색 | 선택 사항 | 연락처, 재생 목록 |
| .value1 | 왼쪽, 굵게 | 오른쪽, 회색 | 없음 | 값이 있는 설정 |
| .value2 | 오른쪽, 파란색 | 왼쪽, 회색 | 없음 | 전화번호부(iOS 6 스타일) |
// 사용자 지정 셀로 테이블 만들기
class ContactListController: UIViewController {
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.rowHeight = 60
view.addSubview(tableView)
// Auto Layout
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
extension ContactListController: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
contacts.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let contact = contacts[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = contact.name
content.secondaryText = contact.phone
content.image = UIImage(systemName: "person.circle")
cell.contentConfiguration = content
return cell
}
}
UIContentConfiguration(iOS 14+) — textLabel/detailTextLabel에 직접 접근하는 대신 contentConfiguration을 통해 셀을 구성하는 현대적인 방법입니다. cell.defaultContentConfiguration()을 사용하거나 사용자 지정 UIContentConfiguration을 만드세요. 장점: Dynamic Type, 다크 모드 및 VoiceOver에 대한 기본 지원. Apple(WWDC 2024)은 모든 새로운 UITableView에 contentConfiguration을 권장합니다.
UITableViewDataSource — 테이블에 데이터를 제공하는 프로토콜입니다. 필수 메서드: tableView(_:numberOfRowsInSection:) 및 tableView(_:cellForRowAt:). 이것들이 없으면 테이블은 단일 행도 표시할 수 없습니다. UITableViewDelegate — 모양과 동작을 관리하는 프로토콜: 행 높이, 선택, 편집, 컨텍스트 작업.
// 여러 섹션 및 편집 예제
extension ContactListController: UITableViewDelegate {
// 각 행의 사용자 지정 높이
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// 선택 처리
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let contact = contacts[indexPath.row]
showDetail(for: contact)
}
// Swipe-to-delete
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
-> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "삭제") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
델리게이트 성능: 크기 메서드(heightForRowAt)는 스크롤할 때마다 표시되는 각 행에 대해 호출됩니다. 균일한 높이의 경우 직접 rowHeight를 설정하세요 — heightForRowAt보다 10배 빠릅니다. 셀프 사이징 셀의 경우 estimatedRowHeight와 automaticDimension을 설정하세요 — 테이블은 보이는 행에 대해서만 heightForRowAt을 호출하고 나머지에는 estimatedRowHeight를 사용합니다.
재사용 큐 — 재사용 가능한 UITableView 셀의 풀입니다. 셀이 화면 밖으로 스크롤되면 삭제되지 않고 큐에 배치됩니다. 새 셀은 처음부터 생성되지 않습니다. 시스템은 dequeueReusableCell(withIdentifier:for:)을 호출하여 큐에서 셀을 반환하거나 큐가 비어 있으면 새 셀을 만듭니다. 이것이 UITableView의 핵심 성능 메커니즘입니다.
// 캐싱이 있는 사용자 지정 셀
class ContactCell: UITableViewCell {
private let avatarView = UIImageView()
private let nameLabel = UILabel()
private let subtitleLabel = UILabel()
override init(style: UITableViewCell.Style, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
contentView.addSubview(avatarView)
contentView.addSubview(nameLabel)
contentView.addSubview(subtitleLabel)
// Auto Layout 제약 조건 구성
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... constraints ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// 비동기 아바타 로딩
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// 등록 및 사용
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// cellForRowAt에서:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — 셀을 재사용 큐에 넣기 전에 호출되는 메서드입니다. 텍스트, 이미지, 애니메이션, 로딩 작업 등 모든 셀 상태를 재설정하세요. prepareForReuse가 없으면 재사용된 셀이 이전 데이터를 표시할 수 있습니다. 새 이미지가 로드될 때까지 이전 이미지가 잠시 표시됩니다. Apple(WWDC 2023)은 prepareForReuse에서 URLSessionTask.cancel()을 통해 비동기 작업을 취소할 것을 권장합니다.
섹션은 UITableView에서 논리적으로 관련된 행을 그룹화합니다. 각 섹션에는 헤더와 푸터가 있을 수 있습니다. 섹션 수는 numberOfSections(in:) 메서드로 결정됩니다(기본값은 1). grouped 및 insetGrouped 스타일의 경우 섹션은 여백과 둥근 모서리로 시각적으로 구분됩니다.
| DataSource 메서드 | 설명 | 반환 값 |
|---|---|---|
| numberOfSections | 섹션 수(기본값 1) | Int |
| numberOfRowsInSection | 섹션 내 행 수 | Int |
| titleForHeaderInSection | 섹션 헤더 제목 텍스트 | String? |
| titleForFooterInSection | 섹션 푸터 제목 텍스트 | String? |
| viewForHeaderInSection | 헤더용 사용자 지정 뷰 | UIView? |
| heightForHeaderInSection | 헤더 높이 | CGFloat |
인덱싱: 섹션 간 빠른 탐색을 위해 오른쪽에 표시되는 문자열 배열인 인덱스 제목을 추가하세요. sectionIndexTitles(for:)를 구현하면 각 섹션의 첫 글자 배열이 반환됩니다. 26개 이상의 섹션이 있는 경우 UX가 크게 향상됩니다. iOS 15+에서 테이블은 sectionHeaderTopPadding을 지원합니다 — 상태 표시줄과 첫 번째 헤더 사이의 패딩으로, 조밀한 레이아웃을 위해 0으로 재설정할 수 있습니다.
성능은 긴 목록이 있는 앱에서 UITableView의 성능이 중요합니다. 일반적인 문제: cellForRowAt에서의 무거운 작업으로 인한 느린 스크롤(jank), 빈번한 heightForRowAt 호출, prefetching 부족. Apple(WWDC 2025)은 1000개 이상의 요소가 있는 테이블을 위한 5가지 핵심 방법을 강조합니다.
// Prefetching으로 최적화
extension ContactListController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView,
prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
ImageCache.shared.prefetch(url: contact.avatarUrl)
}
}
func tableView(_ tableView: UITableView,
cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
ImageCache.shared.cancelPrefetch(url: contact.avatarUrl)
}
}
}
// 부드러운 업데이트를 위한 DiffableDataSource 사용
class ModernTableController: UIViewController {
private var dataSource: UITableViewDiffableDataSource<Section, Contact>!
func applyContacts(_ contacts: [Contact]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Contact>()
snapshot.appendSections([.main])
snapshot.appendItems(contacts)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
실용적인 결과: 이러한 최적화를 적용하면 iPhone 12 이상 기기에서 스크롤 FPS가 30에서 60으로 향상됩니다(Apple 테스트 데이터, 2025 기준). IT Sectr 프로젝트에서는 모든 새 테이블에 DiffableDataSource를 사용하고 이미지(아바타, 미리보기, 앱 아이콘)가 있는 테이블에 prefetching을 추가합니다.
자주 묻는 질문
확인 사항: (1) contentSize > frame.size — 테이블에 높이보다 더 많은 콘텐츠가 있어야 합니다. (2) 테이블이 제스처를 가로채는 다른 UIScrollView 내부에 있지 않음. (3) isScrollEnabled = true. (4) numberOfRowsInSection이 올바른 수를 반환함. 일반적인 문제는 estimatedRowHeight 없이 heightForRowAt = UITableView.automaticDimension을 사용하는 테이블로, 무한 높이 계산을 유발합니다.
reloadData() 대신 performBatchUpdates 또는 DiffableDataSource를 사용하세요. reloadData()는 애니메이션 없이 전체 다시 로드를 발생시켜 시각적 깜빡임을 만듭니다. 대상 업데이트에는 reloadRows(at:with:), insertRows, deleteRows를 사용하세요. performBatchUpdates 내의 모든 변경 사항이 동시에 애니메이션됩니다. DiffableDataSource는 수동 호출 없이 자동으로 이 작업을 수행합니다.
UITableView는 미리 정의된 셀 스타일과 내장 편집 기능을 갖춘 단일 열 세로 목록입니다. UICollectionView는 유연한 레이아웃(그리드, 목록, 캐러셀, 워터폴)으로 요소를 임의로 배치할 수 있습니다. 간단한 목록(설정, 연락처, 메시지)에는 UITableView를 선택하세요. 갤러리, 카탈로그, 보드 및 비선형 레이아웃에는 UICollectionView를 선택하세요.
tableView.tableFooterView = UIView(frame: .zero)를 설정하세요. 기본적으로 UITableView는 마지막 항목 아래에서 하단 가장자리까지 빈 행(구분선)을 표시합니다. 빈 UIView를 tableFooterView로 설정하면 제거됩니다. grouped 스타일의 경우 필요하지 않습니다 — 테이블은 마지막 섹션에서 둥근 모서리와 함께 실제 행만 표시합니다.
tableView.rowHeight = 80과 tableView.estimatedRowHeight = 0을 설정하거나 estimatedRowHeight를 생략하세요. 고정 높이를 사용하면 UITableView가 heightForRowAt을 호출하지 않아 100개 이상의 항목이 있는 목록의 렌더링 속도가 5~10배 빨라집니다. 사용자 지정 높이(다른 콘텐츠)가 있는 셀의 경우 셀프 사이징(estimatedRowHeight + UITableView.automaticDimension)을 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.