기존코드

저는 처음에 Cell에 Indicatorview를 넣어서 indexPath 값으로 변환 해줘야겠다 생각했어요

하지만 이방식으로 구현하면 Cell 마다 Indicatorview가 보이고 디자이너가 요구하는 사항을 충족하지 못하더라구요 항상 Indicatorview 때문에 머리가 아팠는데 이번 기회에 CompositionalLayout에서 Indicatorview를 구현 하는 방법을 알았습니다 하도 모르겠어서 삽집하다가 마지막에 생각난 아이디어가 CompositionalLayout에서 설정한 특정 섹션을 가져올 수 없을까 생각했는데 그 방법이 있어서 해당 방법을 사용했습니다.

코드양은 변경된 코드는 얼마 없지만 미치도록 삽질 했음

import UIKit

import SnapKit
import Then

final class HomeViewController: UIViewController {

    // MARK: - Properties

    private let bannerData = BannerResponse.mockData()
    private let bannerPageIndicatorImage = [
        ImageLiterals.pagebutton_01,
        ImageLiterals.pagebutton_02,
        ImageLiterals.pagebutton_03,
        ImageLiterals.pagebutton_04,
        ImageLiterals.pagebutton_05,
        ImageLiterals.pagebutton_06
    ]

    // MARK: - UI Components

    private lazy var homeCollectionView = UICollectionView(
        frame: .zero,
        collectionViewLayout: self.createCompositionalLayout()
    )
    private let bannerPageLabel = UILabel()
    private let bannerPageIndicatorImageView = UIImageView()

    // MARK: - LifeCycle

    override func viewDidLoad() {
        super.viewDidLoad()
        setUp()
        setStyle()
        setLayout()
        setRegister()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        updateBannerOverlayPosition()
    }

    // MARK: - Setup

    private func setUp() {
        view.addSubviews(
            homeCollectionView,
            bannerPageLabel,
            bannerPageIndicatorImageView
        )
    }

    // MARK: - SetStyle

    private func setStyle() {
        homeCollectionView.do {
            $0.backgroundColor = .white
            $0.delegate = self
            $0.dataSource = self
            $0.showsHorizontalScrollIndicator = false
        }

        bannerPageLabel.do {
            $0.backgroundColor = .gray07
            $0.font = .pretendard(.body7_r_13)
            $0.layer.cornerRadius = 12
            $0.clipsToBounds = true
            $0.textColor = .white
            $0.textAlignment = .center
            $0.text = "1/\(bannerData.count)"
        }

        bannerPageIndicatorImageView.do {
            $0.contentMode = .scaleAspectFit
            $0.image = bannerPageIndicatorImage[0]
        }
    }

    // MARK: - SetLayout

    private func setLayout() {
        homeCollectionView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
    }

    // MARK: - SetRegister

    private func setRegister() {
        homeCollectionView.do {
            $0.register(BannerCell.self, forCellWithReuseIdentifier: BannerCell.identifier)
        }
    }

    // MARK: - Layout Configuration

    private func createCompositionalLayout() -> UICollectionViewCompositionalLayout {
        return CompositionalLayoutFactory.create()
    }

    //MARK: - Banner Section Indicator Method

    private func updateBannerOverlayPosition() {
        let indexPath = IndexPath(item: 0, section: HomeSectionType.banner.rawValue)
        guard let attributes = homeCollectionView.layoutAttributesForItem(at: indexPath) else { return }

        let cellFrame = attributes.frame
        let convertedFrame = homeCollectionView.convert(cellFrame, to: view)

        bannerPageLabel.snp.remakeConstraints {
            $0.size.equalTo(CGSize(width: 44, height: 28))
            $0.top.equalToSuperview().offset(convertedFrame.minY + 16)
            $0.trailing.equalToSuperview().inset(15)
        }

        bannerPageIndicatorImageView.snp.remakeConstraints {
            $0.size.equalTo(CGSize(width: 93, height: 8))
            $0.top.equalToSuperview().offset(convertedFrame.maxY - 20)
            $0.centerX.equalToSuperview()
        }
    }

    private func updateBannerPageIndicator(currentPage: Int) {
        bannerPageLabel.text = "\(currentPage + 1)/\(bannerData.count)"
        bannerPageIndicatorImageView.image = bannerPageIndicatorImage[currentPage]
    }
}

// MARK: - UICollectionView Delegate & DataSource

extension HomeViewController: UICollectionViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        updateBannerOverlayPosition()
    }

    func collectionView(_ collectionView: UICollectionView,
                        willDisplay cell: UICollectionViewCell,
                        forItemAt indexPath: IndexPath) {
        guard HomeSectionType(rawValue: indexPath.section) == .banner else { return }

        let currentPage = indexPath.item
        DispatchQueue.main.async { [weak self] in
            self?.updateBannerPageIndicator(currentPage: currentPage)
        }
    }
}

extension HomeViewController: UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return HomeSectionType.allCases.count
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        guard let sectionType = HomeSectionType(rawValue: section) else { return 0 }
        switch sectionType {
        case .banner:
            return bannerData.count
        }
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let sectionType = HomeSectionType(rawValue: indexPath.section) else {
            return UICollectionViewCell()
        }

        switch sectionType {
        case .banner:
            let cell = collectionView.dequeueReusableCell(
                withReuseIdentifier: BannerCell.identifier,
                for: indexPath) as! BannerCell
            cell.configure(image: bannerData[indexPath.item].Image)
            return cell
        }
    }
}

이유 설명
셀 내부에 넣으면 셀 재사용 시 매번 인디케이터 상태를 셀마다 처리해야 함 → 복잡함, 오류 증가
셀 외부 오버레이 하나의 인디케이터만 관리하면 되고, 위치만 계산하면 됨 → 안정적이고 깔끔함

1. 라벨/인디케이터 뷰는 컬렉션 뷰 바깥에 따로 선언

추후 가능 하다면 멸도의 뷰로 묶어서 관리하도록 리팩토링을 해야 겠습니다.

private let bannerPageLabel = UILabel()
private let bannerPageIndicatorImageView = UIImageView()

→ 이 두 뷰는 UICollectionView 위에 오버레이되며, 배너 섹션 위에 띄워지는 구조입니다.