UICollectionView는 iOS용 UIKit의 강력한 구성 요소로, 객체 컬렉션을 그리드, 리스트 및 커스텀 레이아웃으로 표시합니다. UITableView와 달리 UICollectionView는 단일 열에 제한되지 않습니다. UICollectionViewLayout을 통해 요소를 자유롭게 배치할 수 있습니다. UICollectionView의 핵심, FlowLayout 및 CompositionalLayout의 작동 방식, Swift에서 커스텀 셀을 만드는 방법을 설명합니다. IT Sectr에서는 갤러리, 카탈로그 및 게시판에 UICollectionView를 사용합니다. 테이블과의 비교를 위해 UITableView에 대한 문서를 읽어보세요.
핵심 사항
UICollectionView는 iOS 6(2012)에서 도입된 UIKit 프레임워크의 클래스입니다. UICollectionViewLayout 객체가 관리하는 유연한 레이아웃으로 셀(UICollectionViewCell)의 정렬된 컬렉션을 표시합니다. UICollectionView는 UITableView(리스트)와 동일한 문제를 해결하지만 요소를 자유롭게 배치할 수 있습니다: 그리드, 캐러셀, 모자이크, 캐스케이드.
아키텍처 UICollectionView는 MVC 패턴을 기반으로 합니다. 데이터는 DataSource, 레이아웃은 UICollectionViewLayout, 동작은 UICollectionViewDelegate가 담당합니다. 이러한 분리를 통해 데이터 코드를 변경하지 않고 레이아웃을 교체할 수 있습니다. Apple(WWDC 2025)에 따르면 App Store 상위 100개 iOS 앱의 72%가 콘텐츠 표시에 UICollectionView를 사용합니다. CollectionView는 iOS 6부터 다중 열 레이아웃을 지원하며 iOS 13부터 복잡한 섹션을 위해 CompositionalLayout을 지원합니다.
UICollectionView는 UITableView와 유사하게 재사용 가능한 셀 풀(reuse queue)을 사용합니다. dequeueReusableCell(withReuseIdentifier:for:) 메서드는 풀에서 셀을 반환하거나 풀이 비어 있으면 새 셀을 만듭니다. UITableView와 달리 UICollectionViewCell에는 내장된 contentView가 있으며 모든 하위 뷰를 여기에 추가해야 합니다. 셀에 직접 추가하지 마세요. contentView가 올바른 선택 및 편집 애니메이션을 담당합니다.
UICollectionViewFlowLayout — iOS 6부터 작동하는 표준 레이아웃입니다. 셀 크기, 스크롤 방향, 최소 간격 및 섹션을 구성할 수 있으며 요소를 행(flow)으로 정렬합니다. UICollectionViewCompositionalLayout — 최신 레이아웃 (iOS 13+)으로 블록 조합(그룹 → 섹션 → 레이아웃)으로 레이아웃을 설명합니다. 각 그룹은 가로, 세로 또는 커스텀일 수 있습니다.
| 매개변수 | UICollectionViewFlowLayout | UICollectionViewCompositionalLayout |
|---|---|---|
| 최소 버전 | iOS 6 | iOS 13 |
| 복잡한 레이아웃 | 단순 그리드 또는 리스트만 | 다양한 레이아웃, 그룹, 캐러셀이 있는 섹션 |
| 적응성 | 델리게이트 크기 통해 | NSCollectionLayoutDimension.fractionalWidth/Height |
| 성능 | 모든 가시 요소 렌더링 | 지연 섹션 생성 |
| iPad 지원 | 수동 적응 필요 | fractionalWidth를 통한 적응형 그룹 |
| 구현 복잡성 | 낮음 (5~10줄) | 중간 (15~30줄) |
// FlowLayout을 사용한 기본 UICollectionView
class ViewController: UIViewController {
private var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumInteritemSpacing = 8
layout.minimumLineSpacing = 12
layout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
layout.itemSize = CGSize(width: view.frame.width - 32, height: 100)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "cell")
collectionView.dataSource = self
view.addSubview(collectionView)
}
}
// DataSource
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int { 20 }
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
as! CustomCell
cell.configure(with: "Item \(indexPath.row)")
return cell
}
}
Apple 권장 사항 (WWDC 2025): 새 프로젝트에는 CompositionalLayout을 사용하세요. 적응형 레이아웃에 더 많은 기능을 제공하고 UICollectionView.Representable을 통해 UIKit 및 SwiftUI와 더 잘 작동하며 자동 크기 조절 섹션 헤더를 지원합니다. FlowLayout은 동일한 셀의 그리드와 iOS 12 이하의 최소 버전이 있는 간단한 화면에 사용하세요.
UICollectionViewCell — UICollectionView 셀의 기본 클래스입니다. 각 셀에는 contentView(메인 컨테이너), backgroundView(기본 배경), selectedBackgroundView(선택 시 배경)가 포함됩니다. 셀의 모양은 contentView 내에서 구성하세요. UICollectionViewCell에 직접 하위 뷰를 추가하지 마세요.
// 구성이 포함된 커스텀 셀
class ProductCell: UICollectionViewCell {
private let imageView = UIImageView()
private let titleLabel = UILabel()
private let priceLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
contentView.addSubview(imageView)
contentView.addSubview(titleLabel)
contentView.addSubview(priceLabel)
// Auto Layout 제약 조건
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
priceLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8)
])
}
func configure(with product: Product) {
imageView.image = UIImage(named: product.imageName)
titleLabel.text = product.title
priceLabel.text = product.price
}
}
UICollectionViewDelegate는 선택, 강조, 컨텍스트 메뉴 및 재정렬을 처리하는 메서드를 제공합니다. UITableView와 달리 UICollectionViewDelegate는 요소 탭뿐만 아니라 길게 누르기(UIContextMenuInteraction을 통한 컨텍스트 메뉴)도 제어할 수 있습니다. 탭을 처리하려면 collectionView(_:didSelectItemAt:)을 구현하세요. 선택 애니메이션이 완료되면 호출됩니다.
UICollectionViewDiffableDataSource — iOS 13에서 도입된 UICollectionViewDataSource의 타입 세이프 대체품입니다. cellForItemAt을 통한 수동 셀 관리 대신 DiffableDataSource는 NSDiffableDataSourceSnapshot을 사용합니다. 이는 데이터 상태의 스냅샷으로 이전 상태와 새 상태의 차이를 자동으로 계산하고 변경 사항을 애니메이션화합니다.
// 섹션이 있는 DiffableDataSource
enum Section {
case featured, recommended, categories
}
class ModernDataSource: UIViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Product>!
override func viewDidLoad() {
super.viewDidLoad()
configureDataSource()
applyInitialSnapshot()
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<ProductCell, Product> { cell, _, product in
cell.configure(with: product)
}
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
collectionView, indexPath, product in
collectionView.dequeueConfiguredReusableCell(
using: cellRegistration, for: indexPath, item: product
)
}
}
private func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Product>()
snapshot.appendSections([.featured, .recommended, .categories])
snapshot.appendItems(featuredProducts, toSection: .featured)
snapshot.appendItems(recommendedProducts, toSection: .recommended)
snapshot.appendItems(categories, toSection: .categories)
dataSource.apply(snapshot, animatingDifferences: true)
}
// 자동 애니메이션으로 업데이트
func updateProducts(_ products: [Product]) {
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers)
snapshot.appendItems(products)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
DiffableDataSource의 장점: (1) 자동 애니메이션 — insert, delete, reload가 기본 애니메이션으로 발생; (2) 타입 안전성 — 섹션과 항목이 타입화되어 런타임 충돌 대신 컴파일 오류; (3) 스냅샷 — 상태 스냅샷을 저장, 실행 취소, 비교 가능; (4) 성능 — DiffableDataSource는 Hellier 알고리즘을 사용하여 최소 변경 세트를 계산. Apple(WWDC 2024)은 모든 새 UICollectionView에 DiffableDataSource를 권장하며 이전 DataSource는 iOS 12 및 이전 버전에만 사용하는 것을 권장합니다.
UICollectionViewLayout — 컬렉션 요소의 배치를 완전히 제어하는 추상 클래스입니다. FlowLayout 및 CompositionalLayout이 적합하지 않은 경우(예: 원형 레이아웃, 나선형, 다이어그램) UICollectionViewLayout의 하위 클래스를 만들고 레이아웃 준비 메서드를 재정의합니다.
// 커스텀 원형 레이아웃
class CircularLayout: UICollectionViewLayout {
private var attributesCache: [UICollectionViewLayoutAttributes] = []
private let radius: CGFloat = 120
override func prepare() {
super.prepare()
attributesCache.removeAll()
guard let collectionView else { return }
let count = collectionView.numberOfItems(inSection: 0)
let center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
for i in 0..<count {
let indexPath = IndexPath(item: i, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let angle = (CGFloat(i) / CGFloat(count)) * 2 * .pi
attributes.center = CGPoint(
x: center.x + radius * cos(angle),
y: center.y + radius * sin(angle)
)
attributes.size = CGSize(width: 60, height: 60)
attributesCache.append(attributes)
}
}
override var collectionViewContentSize: CGSize {
collectionView?.bounds.size ?? .zero
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
attributesCache.filter { $0.frame.intersects(rect) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
attributesCache[indexPath.item]
}
}
성능: 커스텀 UICollectionViewLayout은 prepare()에서 속성을 캐시하고 layoutAttributesForItem(at:)에서 O(1)로 반환해야 합니다. 1000개 이상의 요소가 있는 컬렉션의 경우 shouldInvalidateLayout(forBoundsChange:)을 통해 증분 캐시 업데이트를 사용하세요. 이렇게 하면 스크롤 시 전체 레이아웃을 다시 계산하지 않아도 됩니다. Apple(WWDC 2023)은 커스텀 레이아웃보다 CompositionalLayout을 권장합니다. 레이아웃을 처음부터 작성하지 않고도 95%의 시나리오를 커버합니다.
UITableView와 UICollectionView는 리스트를 표시하는 두 가지 주요 UIKit 구성 요소입니다. UITableView는 더 간단하고 단일 열 리스트용으로 설계되었습니다. UICollectionView는 더 유연하고 모든 레이아웃을 지원합니다. 선택은 데이터의 시각적 구조와 필요한 사용자 정의에 따라 달라집니다.
| 매개변수 | UITableView | UICollectionView |
|---|---|---|
| 레이아웃 | 세로 리스트만 | 리스트, 그리드, 캐스케이드, 캐러셀, 커스텀 |
| 복잡성 | 낮음 (단순 DataSource) | 중간 (Layout + DataSource + Delegate) |
| 기본 셀 | 4가지 스타일 (basic, subtitle, value1, value2) | 비어 있음만 (contentView 통해 커스텀) |
| 편집 모드 | 내장 (delete, move, insert) | 수동 구현 필요 |
| Header/Footer | 내장 (viewForHeaderInSection) | 보충 뷰 통해 |
| 성능 | 리스트에서 높음 | 모든 레이아웃에서 높음 |
| iOS 6+ | iOS 2부터 사용 가능 | iOS 6부터 사용 가능 |
실용적인 규칙: 화면이 단순한 행 리스트(설정, 연락처, 메뉴)인 경우 UITableView를 사용하세요. 화면에 그리드, 가로 스크롤, 캐러셀, 다양한 셀 크기 또는 복잡한 그룹화가 필요한 경우 UICollectionView를 사용하세요. iOS 14부터 UITableView도 UICollectionLayoutListConfiguration을 통해 CompositionalLayout을 지원하여 두 구성 요소의 장점을 결합할 수 있습니다.
자주 묻는 질문
UICollectionViewFlowLayout에서 scrollDirection = .horizontal을 설정하세요. CompositionalLayout의 경우 적응형 그룹 너비를 위해 fractionalWidth와 함께 NSCollectionLayoutSize를 사용하세요. 가로 스크롤은 캐러셀, 갤러리 및 카테고리에 적합합니다. 페이지 매김의 경우 collectionView.isPagingEnabled = true를 설정하거나 itemSize = bounds.width와 함께 UICollectionViewFlowLayout을 사용하세요.
확인 사항: (1) 셀이 register(_:forCellWithReuseIdentifier:)를 통해 등록되었는지, (2) DataSource가 항목 수 > 0을 반환하는지, (3) 셀이 올바른 reuseIdentifier로 디큐되었는지, (4) itemSize가 0이 아닌지(FlowLayout의 경우). 일반적인 실수는 itemSize = CGSize.zero로 셀 높이가 0이 되는 것입니다. itemSize를 명시적으로 설정하거나 델리게이트 collectionView(_:layout:sizeForItemAt:)을 구현하세요.
FlowLayout의 경우: layout.minimumInteritemSpacing(행 내 항목 사이) 및 layout.minimumLineSpacing(행 사이). CompositionalLayout의 경우: NSCollectionLayoutGroup.interItemSpacing 및 그룹/섹션의 contentInsets. sectionInset은 섹션의 외부 여백을 설정합니다. Safe Area를 올바르게 처리하려면 sectionInsetReference = .fromContentInset을 사용하세요.
그룹 애니메이션에는 performBatchUpdates를 사용하세요: collectionView.performBatchUpdates { insertItems, deleteItems, reloadItems }. 안전하고 애니메이션된 업데이트를 위해 UICollectionViewDiffableDataSource를 사용하세요: dataSource.apply(snapshot, animatingDifferences: true)를 호출하면 수동 insert/delete 없이 모든 변경 사항이 자동으로 계산되고 적용됩니다.
SwiftUI에서 UICollectionView에 해당하는 것은 LazyVGrid 및 LazyHGrid(iOS 14+)입니다. SwiftUI Grid는 작성이 간편하지만(선언적 구문) 500개 이상의 요소와 레이아웃 사용자 정의에서 UICollectionView보다 성능이 떨어집니다. 복잡한 컬렉션의 경우 UIViewRepresentable을 통해 UICollectionView를 사용하세요. IT Sectr 프로젝트에서는 카탈로그와 갤러리에 UICollectionView를 선택하고 간단한 화면에는 SwiftUI Grid를 선택합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.