[TIL] 키오스크 앱 리팩토링 chapter.02

Emily·2024년 12월 8일
2

KioskApp

목록 보기
4/4
post-thumbnail

확신이 없는 코드를 치면서 좌절했던 시간을 지나, 이제는 왜 이 코드가 여기 있어야 하고 왜 저 코드가 저기 있어야하는지 조금씩 감을 잡아가는 단계가 된 것 같다. 패턴 공부의 중요성을 절실하게 깨달은 지난 며칠이었다. MVCDelegate 패턴을 공부하면서, 객체의 역할과 책임을 잘 분리하고 서로 간 의존성을 최대한 낮추는 첫 번째 방법을 터득했다고 봐도 될 것 같다. 공부한 내용을 적용하며 코드를 이리저리 옮기는 리팩토링을 진행하고자 한다.

01) 카테고리 영역 분리

기존 rootview controller 코드
class KioskViewController: UIViewController {

    private let titleView = TitleView()
    
    private let categoryView: UIView = {
        let view = UIView()
        view.backgroundColor = .clear
        view.layer.cornerRadius = 20
        return view
    }()
    
    private let buttons = [CategoryButton(.honey), CategoryButton(.red), CategoryButton(.kyochon)]
    
    private lazy var menuView = MenuView()
    private lazy var cartView = CartView(mananger: manager)
    private let sumView = SumView()
    private let footerView = FooterView()
    
    // ... //
    
}


귀여운 로고와 꼬끼오스크 타이틀은 위의 코드에서 titleView에 속하고, 허니시리즈, 레드시리즈, 교촌시리즈는 CategoryButton이라는 커스텀 버튼으로 구현하였다. buttonscategoryViewsubview인데, categoryView를 컨트롤러에서 정의하지 않고 따로 클래스를 분리하여 버튼들과 함께 옮겨보려 한다.

class CategoryView: UIView {

	private let buttons = [CategoryButton(.honey), CategoryButton(.red), CategoryButton(.kyochon)]

    override init(frame: CGRect) {
        super.init(frame: .zero)
        addSubViews(buttons)
        setup()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setup() {
        backgroundColor = .clear
        layer.cornerRadius = 20
        
        // ... //
    }
}
class KioskViewController: UIViewController {

    private let titleView = TitleView()
    private let categoryView = CategoryView()
    private lazy var menuView = MenuView()
    private lazy var cartView = CartView(mananger: manager)
    private let sumView = SumView()
    private let footerView = FooterView()
    
    // ... //
    
}

카테고리 뷰를 따로 클래스로 분리하고 버튼들을 subview로 옮겨넣었더니 Controllerview 선언 코드가 훨씬 간결해졌다.

02) 눌린 버튼 정보를 Delegate를 통해 Controller에 전송

CategoryViewControllerModel

우선 Controller가 갖는 버튼 관련 코드들을 CategoryView로 옮긴다.

before
class KioskViewController: UIViewController {
	private let buttons = [CategoryButton(.honey), CategoryButton(.red), CategoryButton(.kyochon)]
    
    private func setupCategoryView() {
        buttons.forEach {
            categoryView.addSubview($0)
            
            $0.snp.makeConstraints {
                $0.width.equalToSuperview().dividedBy(3)
                $0.height.equalToSuperview()
                $0.centerY.equalToSuperview()
            }
            
            $0.addTarget(self, action: #selector(categoryTapped(_:)), for: .touchUpInside)
        }

        buttons[0].snp.makeConstraints {
            $0.leading.equalToSuperview()
        }
        
        buttons[1].snp.makeConstraints {
            $0.centerX.equalToSuperview()
        }
        
        buttons[2].snp.makeConstraints {
            $0.trailing.equalToSuperview()
        }
        
        // 허니시리즈 버튼은 눌린 채로 시작
        setButtonSelected(for: buttons[0])
    }
    
    @objc func categoryTapped(_ sender: CategoryButton) {
        series = sender.series
        menuView.collectionView.reloadData()
        setButtonSelected(for: sender)
        
        menuView.collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
        menuView.pageControl.numberOfPages = series.chickens.count / 4 + 1
        menuView.pageControl.currentPage = 0
    }
    
    private func setButtonSelected(for button: UIButton) {
        buttons.forEach {
            $0.backgroundColor = .clear
            $0.isSelected = false
        }
        
        button.backgroundColor = .appPrimary
        button.isSelected = true
    }
}
after
class KioskViewController: CategorySelectDelegate {
	override func viewDidLoad() {
        super.viewDidLoad()
        categoryView.delegate = self
    }

    func updateCategory(_ series: ChickenSeries) {
    	// model update
        self.series = series
        menuView.collectionView.reloadData()
        
        menuView.collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
        menuView.pageControl.numberOfPages = series.chickens.count / 4 + 1
        menuView.pageControl.currentPage = 0
    }
}

Controller의 코드 양이 확연히 줄은 게 보인다. CategoryViewDelegation을 적용한 내용도 가져와보겠다.

// 프로토콜을 선언하고 눌린 버튼이 무슨 시리즈의 버튼인지 컨트롤러에 전송하기 위한 함수를 선언한다
protocol CategorySelectDelegate: AnyObject {
    func updateCategory(_ series: ChickenSeries)
}

class CategoryView: UIView {
    
    // 순환 참조 방지를 위해 weak var로 선언
    weak var delegate: CategorySelectDelegate?
    
    private let buttons = [CategoryButton(.honey), CategoryButton(.red), CategoryButton(.kyochon)]
    
    private func setup() {
        // .. auto layout 코드 생략 .. //
        
        buttons.forEach {
            $0.addTarget(self, action: #selector(categoryTapped(_:)), for: .touchUpInside)
        }

        // 허니시리즈 버튼은 눌린 채로 시작
        setButtonSelected(for: buttons[0])
    }
    
    private func setButtonSelected(for button: UIButton) {
        buttons.forEach {
            $0.backgroundColor = .clear
            $0.isSelected = false
        }
        
        button.backgroundColor = .appPrimary
        button.isSelected = true
    }
    
    @objc func categoryTapped(_ sender: CategoryButton) {
    	// 눌린 버튼 ui 처리
        setButtonSelected(for: sender)
        // delegate를 통해 눌린 버튼 정보 전송
        delegate?.updateCategory(sender.series)
    }
}

여기까지 적용하고 나니, Controller의 코드가 50줄 줄었다.

03) 선택된 카테고리 정보를 CollectionView에 전송

ModelControllerCollectionView

위에서는 버튼 → 컨트롤러 방향으로 눌린 카테고리 정보가 전달됐다면, 이번에는 컨트롤러 → 컬렉션 뷰 방향으로 정보가 전달되어 컬렉션 뷰가 카테고리에 맞는 치킨들을 뷰에 그리도록 구현해보려 한다.

우선, Model과의 직접적인 결합을 피하기 위해 ControllerCollectionViewDataSourceDelegate 코드를 다 보냈었는데, 그 코드를 subview에 보낸 뒤 Delegate를 정의하여 객체 간 통신을 구현해보도록 하겠다.

왼쪽에 블록 처리 된 친구들을 모두 오른쪽 MenuView로 옮긴다.

컬렉션 뷰 DataSource : 화면에 그려야하는 치킨의 series 정보가 필요
→ Delegate에 ChickenSeries 타입을 리턴하는 함수를 정의하여 구현
protocol CategorySelectDelegate: AnyObject {
    func updateCategory(_ series: ChickenSeries)
    // 함수 추가
    func getSeriesInfo() -> ChickenSeries
}

extension KioskViewController: CategorySelectDelegate {
    func getSeriesInfo() -> ChickenSeries {
    	// model 데이터 전송
        return series
    }
}
extension MenuView: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    	
        // delegate를 통해 받은 데이터를 collection view에 반영
        guard let series = delegate?.getSeriesInfo() else { return 0 }
        return series.chickens.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard
            let series = delegate?.getSeriesInfo(),
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ChickenCell.identifier, for: indexPath) as? ChickenCell
        else { return UICollectionViewCell() }
        
       	// delegate를 통해 받은 데이터를 collection view에 반영
        cell.bind(series.chickens[indexPath.item])
        
        return cell
    }
}
컬렉션 뷰 Delegate : 사용자가 탭한 치킨 cell의 index 정보를 Controller에 전송 필요
→ Delegate에 파라미터로 index 값을 전달하는 함수 정의
// CategoryView.swift 파일 안에 있던 프로토콜 코드를 CategorySelectDelegate.swift로 분리했다.
protocol CategorySelectDelegate: AnyObject {
    func updateCategory(_ series: ChickenSeries)
    func getSeriesInfo() -> ChickenSeries
    // 함수 정의
    func didTapChickenCell(of index: Int)
}

extension MenuView: UICollectionViewDelegate {
	func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    	// 탭 된 셀의 index를 delegate를 통해 전송
        delegate?.didTapChickenCell(of: indexPath.item)
    }
}
// controller에서 model과 소통하여 로직 처리
extension KioskViewController: CategorySelectDelegate {
	func didTapChickenCell(of index: Int) {
        let chicken = series.chickens[index]
        if let index = manager.orders.firstIndex(where: { $0.menu == chicken }) {
            manager.orders[index].count += 1
        } else {
            let newOrder = Order(menu: chicken)
            manager.orders.append(newOrder)
        }
    }
}

여기까지 적용하고 나니, 리팩토링을 시작하기 전에 271줄이었던 KioskViewController의 코드가 199줄로 줄었다. 리팩토링은 여기서 끝이 아니다. 지금은 카테고리와 메뉴 영역만 고친 것이다. 다른 영역에 적용할 것들이 많이 남았다. 앞으로 진행하는 모든 리팩토링을 글로 남기게 될지는 모르겠지만, 하여튼 이 프로젝트에서 손을 떼진 않으려 한다.

profile
iOS Junior Developer

2개의 댓글

comment-user-thumbnail
2024년 12월 9일

키오스크 .. 아직 놓아주지 않았군요.. 대단해요 분명 더 성장하실고에요

1개의 답글