UITableView 기초: iOS 앱의 리스트와 셀

저자: IT Sectr 게시일: 2026-02-23 읽는 시간: 9 분

UITableView — iOS에서 세로 목록을 표시하기 위한 UIKit의 주요 구성 요소입니다. 테이블의 각 행은 미리 정의된 스타일을 가진 UITableViewCell입니다. UITableView의 기본 사항인 DataSource, 델리게이트, 셀 재사용 및 iOS 앱에서의 테이블 성능에 대해 설명합니다. Apple(Human Interface Guidelines, 2026)에 따르면 UITableView는 iOS에서 가장 많이 사용되는 UI 구성 요소로 남아 있으며, App Store 상위 100개 앱 중 85%에 최소 하나의 테이블이 포함되어 있습니다. 복잡한 레이아웃의 경우 UICollectionView에 관한 문서를 읽어보세요.

핵심 포인트

  • UITableView — 세로 목록을 위한 UIKit 구성 요소, iOS 2.0(iPhone OS 2)부터 사용 가능.
  • UITableViewCell — 4개의 내장 스타일과 사용자 지정 옵션을 갖춘 테이블 셀.
  • UITableViewDataSource — 각 섹션의 행과 셀 수를 정의하는 프로토콜.
  • UITableViewDelegate — 선택, 편집 및 행 높이 구성을 처리하는 프로토콜.
  • UITableViewDiffableDataSource — 자동 변경 계산이 가능한 최신 DataSource(iOS 13+).

iOS에서 UITableView란?

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 스타일: Basic, Subtitle, Value1, Value2

UITableViewCell은 사용자 지정 셀을 만들 필요 없이 80%의 시나리오를 처리하는 4개의 내장 스타일을 제공합니다. 각 스타일은 textLabel, detailTextLabel 및 imageView의 조합으로 구성됩니다. 스타일은 init(style:reuseIdentifier:)를 통해 셀을 초기화할 때 선택됩니다.

스타일textLabeldetailTextLabelimageView예시
.default (.basic)왼쪽, 굵게없음선택 사항메뉴, 항목 목록
.subtitle왼쪽, 굵게textLabel 아래, 회색선택 사항연락처, 재생 목록
.value1왼쪽, 굵게오른쪽, 회색없음값이 있는 설정
.value2오른쪽, 파란색왼쪽, 회색없음전화번호부(iOS 6 스타일)
swift
// 사용자 지정 셀로 테이블 만들기
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을 권장합니다.

DataSource 및 Delegate: 필수 메서드

UITableViewDataSource — 테이블에 데이터를 제공하는 프로토콜입니다. 필수 메서드: tableView(_:numberOfRowsInSection:)tableView(_:cellForRowAt:). 이것들이 없으면 테이블은 단일 행도 표시할 수 없습니다. UITableViewDelegate — 모양과 동작을 관리하는 프로토콜: 행 높이, 선택, 편집, 컨텍스트 작업.

swift
// 여러 섹션 및 편집 예제
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의 핵심 성능 메커니즘입니다.

swift
// 캐싱이 있는 사용자 지정 셀
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 성능: 모범 사례

성능은 긴 목록이 있는 앱에서 UITableView의 성능이 중요합니다. 일반적인 문제: cellForRowAt에서의 무거운 작업으로 인한 느린 스크롤(jank), 빈번한 heightForRowAt 호출, prefetching 부족. Apple(WWDC 2025)은 1000개 이상의 요소가 있는 테이블을 위한 5가지 핵심 방법을 강조합니다.

  • 고정 높이 — 모든 셀에 heightForRowAt 대신 상수 rowHeight를 사용하세요. 차이: 500개 행 테이블에서 60 FPS 대 30 FPS.
  • Prefetching — 셀이 화면에 나타나기 전에 비동기 데이터 로딩(이미지, API)을 위해 UITableViewDataSourcePrefetching을 구현하세요.
  • 서브뷰 줄이기 — contentView의 각 서브뷰는 렌더링 시간을 증가시킵니다. UIImageView 대신 간단한 그래픽(구분선, 아이콘)에는 draw()를 사용하세요.
  • 백그라운드에서 무거운 작업 — 날짜 형식 지정, 현지화, 계산은 cellForRowAt이 아닌 모델에서 수행하세요. 결과에는 NSCache를 사용하세요.
  • DiffableDataSource — reloadData()를 apply(snapshot)으로 대체하세요 — 애니메이션이 무료로 제공되며 테이블이 완전히 다시 로드되지 않습니다.
swift
// 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을 추가합니다.

자주 묻는 질문

UITableView가 스크롤되지 않습니다 — 어떻게 해야 하나요?

확인 사항: (1) contentSize > frame.size — 테이블에 높이보다 더 많은 콘텐츠가 있어야 합니다. (2) 테이블이 제스처를 가로채는 다른 UIScrollView 내부에 있지 않음. (3) isScrollEnabled = true. (4) numberOfRowsInSection이 올바른 수를 반환함. 일반적인 문제는 estimatedRowHeight 없이 heightForRowAt = UITableView.automaticDimension을 사용하는 테이블로, 무한 높이 계산을 유발합니다.

깜빡임 없이 UITableView를 업데이트하려면?

reloadData() 대신 performBatchUpdates 또는 DiffableDataSource를 사용하세요. reloadData()는 애니메이션 없이 전체 다시 로드를 발생시켜 시각적 깜빡임을 만듭니다. 대상 업데이트에는 reloadRows(at:with:), insertRows, deleteRows를 사용하세요. performBatchUpdates 내의 모든 변경 사항이 동시에 애니메이션됩니다. DiffableDataSource는 수동 호출 없이 자동으로 이 작업을 수행합니다.

UITableView와 UICollectionView의 차이점은?

UITableView는 미리 정의된 셀 스타일과 내장 편집 기능을 갖춘 단일 열 세로 목록입니다. UICollectionView는 유연한 레이아웃(그리드, 목록, 캐러셀, 워터폴)으로 요소를 임의로 배치할 수 있습니다. 간단한 목록(설정, 연락처, 메시지)에는 UITableView를 선택하세요. 갤러리, 카탈로그, 보드 및 비선형 레이아웃에는 UICollectionView를 선택하세요.

UITableView에서 콘텐츠 아래 빈 행을 제거하려면?

tableView.tableFooterView = UIView(frame: .zero)를 설정하세요. 기본적으로 UITableView는 마지막 항목 아래에서 하단 가장자리까지 빈 행(구분선)을 표시합니다. 빈 UIView를 tableFooterView로 설정하면 제거됩니다. grouped 스타일의 경우 필요하지 않습니다 — 테이블은 마지막 섹션에서 둥근 모서리와 함께 실제 행만 표시합니다.

일정한 높이로 UITableView를 만들려면?

tableView.rowHeight = 80tableView.estimatedRowHeight = 0을 설정하거나 estimatedRowHeight를 생략하세요. 고정 높이를 사용하면 UITableView가 heightForRowAt을 호출하지 않아 100개 이상의 항목이 있는 목록의 렌더링 속도가 5~10배 빨라집니다. 사용자 지정 높이(다른 콘텐츠)가 있는 셀의 경우 셀프 사이징(estimatedRowHeight + UITableView.automaticDimension)을 사용하세요.

요약

  • UITableView — DataSource 및 Delegate 아키텍처를 갖춘 세로 목록용 주요 UIKit 구성 요소.
  • UITableViewCell — 4가지 내장 스타일(.default, .subtitle, .value1, .value2) 및 contentConfiguration을 통한 사용자 지정.
  • 재사용 큐 — 재사용 가능한 셀의 풀; register + dequeueReusableCell + prepareForReuse는 필수.
  • DiffableDataSource(iOS 13+) — 자동 변경 계산 및 애니메이션이 있는 타입 세이프 DataSource.
  • 성능: 고정 rowHeight, prefetching, 적은 서브뷰, 백그라운드에서 무거운 작업.
  • App Store 상위 100개 앱 중 85%의 iOS 앱이 콘텐츠 표시에 UITableView를 사용합니다.
  • 그리드, 캐러셀 및 복잡한 레이아웃의 경우 UITableView 대신 UICollectionView를 사용하세요.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기