반응형

 iOS 개발자가 많이 하는 실수 - UITableView / UICollectionView에서 reuseIdentifier를 잘못 관리해 셀 재사용 버그가 발생하는 실수

 

UITableView와 UICollectionView는 셀 재사용(reuse)을 통해 성능을 확보합니다.
하지만 이 재사용 메커니즘을 제대로 이해하지 못한 상태에서 코드를 작성하면:

  • 데이터가 엉뚱한 셀에 보이거나
  • 스크롤 시 셀이 깜빡이거나
  • 예전 상태가 섞여 나오는 버그

가 아주 쉽게 발생합니다.

 

1. 셀 재사용 메커니즘 간단 정리

테이블/컬렉션 뷰는 스크롤될 때마다 매번 새 셀을 만드는 것이 아니라:

  1. 화면에서 사라진 셀을 재사용 큐에 넣어두고
  2. 새로운 셀이 필요할 때
    • 재사용 큐에서 꺼내와서(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. 실무용 안전 패턴 요약

  1. reuseIdentifier = 셀 타입별 고유 문자열
    • 보통 String(describing: Self.self) 또는 "MyCell" 상수
  2. Storyboard prototype cell vs 코드 register를 혼용하지 않기
  3. prepareForReuse()에서:
    • 이미지, 토글, selection, 배경색 등 상태 초기화
    • 비동기 작업(cancel) 처리
  4. 비동기 작업 시:
    • 셀이 재사용되었는지 확인하고 UI 업데이트
    • 필요하면 model.id 같은 식별자로 매칭
  5. “unable to dequeue a cell with identifier …” 크래시가 나면:
    • identifier 오타
    • register 누락
    • Storyboard/코드 중복 등록 여부를 확인

9. 요약

  • table/collection view 셀 재사용은 성능에 필수지만,
    reuseIdentifier를 잘못 관리하면 데이터/상태가 엉키는 버그가 자주 발생한다.
  • 특히:
    • 서로 다른 타입에 같은 reuseIdentifier 사용
    • Storyboard + register 혼용
    • prepareForReuse 누락
    • 비동기 처리 중 재사용 타이밍 미고려
      는 모두 실무에서 매우 흔한 실수이다.

핵심 문장:

셀은 재사용된다. 재사용을 전제로, 식별자/초기화/비동기 처리까지 설계해야 한다.

 

반응형
Posted by 까칠코더
,