iOS 개발자가 많이 하는 실수 - UITableView / UICollectionView에서 reuseIdentifier를 잘못 관리해 셀 재사용 버그가 발생하는 실수
Dev Study/iOS 2025. 12. 4. 20:39반응형
iOS 개발자가 많이 하는 실수 - UITableView / UICollectionView에서 reuseIdentifier를 잘못 관리해 셀 재사용 버그가 발생하는 실수
UITableView와 UICollectionView는 셀 재사용(reuse)을 통해 성능을 확보합니다.
하지만 이 재사용 메커니즘을 제대로 이해하지 못한 상태에서 코드를 작성하면:
- 데이터가 엉뚱한 셀에 보이거나
- 스크롤 시 셀이 깜빡이거나
- 예전 상태가 섞여 나오는 버그
가 아주 쉽게 발생합니다.
1. 셀 재사용 메커니즘 간단 정리
테이블/컬렉션 뷰는 스크롤될 때마다 매번 새 셀을 만드는 것이 아니라:
- 화면에서 사라진 셀을 재사용 큐에 넣어두고
- 새로운 셀이 필요할 때
- 재사용 큐에서 꺼내와서(dequeue)
- 데이터만 바꿔 끼워 보여줍니다.
이때 reuseIdentifier가 같은 셀끼리 재사용됩니다.
즉, reuseIdentifier는 “같은 타입/레이아웃/용도”의 셀을 묶는 ID라고 보면 됩니다.
2. 문제 패턴 1: 서로 다른 셀 타입에 같은 reuseIdentifier 사용
tableView.register(TitleCell.self, forCellReuseIdentifier: "Cell")
tableView.register(ImageCell.self, forCellReuseIdentifier: "Cell") // ❌ 같은 ID
또는 Storyboard에서:
- 서로 다른 디자인의 prototype cell 2개에 같은 Identifier를 지정한 경우
이럴 때:
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
- 어떤 타입의 셀이 실제로 내려올지 보장이 안 됨
- 캐스팅 시 크래시 발생 가능:
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TitleCell // ❌
해결
- 셀 타입마다 고유한 reuseIdentifier 사용
- 보통 셀 클래스 이름을 그대로 재사용:
class TitleCell: UITableViewCell {
static let reuseID = "TitleCell"
}
class ImageCell: UITableViewCell {
static let reuseID = "ImageCell"
}
등록/사용:
tableView.register(TitleCell.self, forCellReuseIdentifier: TitleCell.reuseID)
tableView.register(ImageCell.self, forCellReuseIdentifier: ImageCell.reuseID)
let cell = tableView.dequeueReusableCell(withIdentifier: TitleCell.reuseID, for: indexPath)
3. 문제 패턴 2: register / storyboard 설정 혼용
Storyboard에서 prototype cell을 쓴 경우:
- 이미 storyboard가 cell 클래스를 등록해 준 상태입니다.
그런데 코드에서 다시:
tableView.register(MyCell.self, forCellReuseIdentifier: "MyCell") // ❌
를 호출하면:
- 코드로 등록된 MyCell가 새 nibless 셀 타입으로 다시 덮어써짐
- 스토리보드 디자인(레이아웃, outlet)이 적용되지 않은 셀 인스턴스가 내려옴
- outlet이 nil이라 크래시, UI 이상 발생
해결
- Storyboard prototype cell을 쓰는 경우:
- register를 호출하지 않는다.
- 코드로만 등록할 경우:
- storyboard prototype cell을 사용하지 않는다.
두 패턴을 섞지 않는 게 안전합니다.
4. 문제 패턴 3: 셀 상태 초기화(prepareForReuse) 누락
셀은 재사용되기 때문에,
이전에 사용되던 상태가 그대로 남아서 엉뚱한 UI로 보이는 경우가 많습니다.
예:
class MyCell: UITableViewCell {
@IBOutlet weak var thumbnailImageView: UIImageView!
@IBOutlet weak var favoriteIcon: UIImageView!
func configure(with model: Model) {
titleLabel.text = model.title
favoriteIcon.isHidden = !model.isFavorite
if let url = model.imageURL {
loadImage(from: url) { image in
self.thumbnailImageView.image = image
}
}
}
}
문제:
- 비동기 이미지 로딩
- 재사용된 셀에 이전 이미지가 잠깐 보이고 나중에 바뀌는 현상 발생
- favoriteIcon, switch, checkbox 등의 상태가 잘못 유지될 수 있음
해결: prepareForReuse()에서 기본 상태 초기화
class MyCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
thumbnailImageView.image = nil
favoriteIcon.isHidden = true
// 비동기 이미지 로드 취소 등도 여기서 처리
}
}
이렇게 해야:
- 셀이 재사용될 때 항상 동일한 초기 상태에서 configure가 시작
- 이전 사용의 “잔상”이 섞이지 않음
5. 문제 패턴 4: indexPath 기반 비동기 처리 시 잘못된 셀 업데이트
다음과 같은 코드:
func configure(at indexPath: IndexPath) {
let model = models[indexPath.row]
loadImage(from: model.imageURL) { image in
self.imageView.image = image // ❌ 여기의 self는 나중에 다른 indexPath로 재사용되었을 수도 있음
}
}
- 이미지를 받아오는 사이 셀이 재사용되면
- 다른 indexPath에 표시되어야 할 셀에 이전 요청의 이미지가 들어갈 수 있음
개선 방법 1: 셀이 직접 model을 기억하고 매칭 확인
class MyCell: UITableViewCell {
private var currentID: String?
func configure(with model: Model) {
currentID = model.id
titleLabel.text = model.title
thumbnailImageView.image = nil
loadImage(from: model.imageURL) { [weak self] image in
guard let self, self.currentID == model.id else { return }
self.thumbnailImageView.image = image
}
}
override func prepareForReuse() {
super.prepareForReuse()
currentID = nil
thumbnailImageView.image = nil
}
}
개선 방법 2: 이미지 로더/다운로더에 cancellation 지원
- URLSessionDataTask.cancel()
- 이미지 캐싱 라이브러리(Kingfisher, SDWebImage 등)의 cancel API
prepareForReuse에서 해당 요청을 취소하는 패턴:
override func prepareForReuse() {
super.prepareForReuse()
imageDownloadTask?.cancel()
imageDownloadTask = nil
thumbnailImageView.image = nil
}
6. 문제 패턴 5: reuseIdentifier 오타 / 매직 스트링
tableView.register(MyCell.self, forCellReuseIdentifier: "mycell") // 등록
...
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) // ❌ 다른 문자열
- 컴파일러가 잡지 못하는 런타임 오류
- “unable to dequeue a cell with identifier …” 크래시 발생
해결: static 상수로 reuseIdentifier 관리
class MyCell: UITableViewCell {
static let reuseID = String(describing: MyCell.self)
}
사용:
tableView.register(MyCell.self, forCellReuseIdentifier: MyCell.reuseID)
let cell = tableView.dequeueReusableCell(withIdentifier: MyCell.reuseID, for: indexPath) as! MyCell
또는 generic helper 사용(TCA, Rx, Combine 기반 프로젝트에서 자주 사용).
7. UICollectionView에서 자주 발생하는 비슷한 실수
UICollectionView도 개념은 완전히 동일합니다.
collectionView.register(
MyCollectionCell.self,
forCellWithReuseIdentifier: MyCollectionCell.reuseID
)
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: MyCollectionCell.reuseID,
for: indexPath
) as! MyCollectionCell
추가로:
- Supplementary View(header/footer)도 reuseIdentifier를 별도로 관리해야 함
- cell과 header/footer를 동일 ID로 사용하면 타입 충돌 및 레이아웃 이상 발생
8. 실무용 안전 패턴 요약
- reuseIdentifier = 셀 타입별 고유 문자열
- 보통 String(describing: Self.self) 또는 "MyCell" 상수
- Storyboard prototype cell vs 코드 register를 혼용하지 않기
- prepareForReuse()에서:
- 이미지, 토글, selection, 배경색 등 상태 초기화
- 비동기 작업(cancel) 처리
- 비동기 작업 시:
- 셀이 재사용되었는지 확인하고 UI 업데이트
- 필요하면 model.id 같은 식별자로 매칭
- “unable to dequeue a cell with identifier …” 크래시가 나면:
- identifier 오타
- register 누락
- Storyboard/코드 중복 등록 여부를 확인
9. 요약
- table/collection view 셀 재사용은 성능에 필수지만,
reuseIdentifier를 잘못 관리하면 데이터/상태가 엉키는 버그가 자주 발생한다. - 특히:
- 서로 다른 타입에 같은 reuseIdentifier 사용
- Storyboard + register 혼용
- prepareForReuse 누락
- 비동기 처리 중 재사용 타이밍 미고려
는 모두 실무에서 매우 흔한 실수이다.
핵심 문장:
셀은 재사용된다. 재사용을 전제로, 식별자/초기화/비동기 처리까지 설계해야 한다.
반응형

