스파르타 코딩 클럽 - iOS 스타터 6기/본 캠프

50. 스파르타 코딩 클럽 - BookSearchApp #2

seongpil Heo 2025. 5. 16. 14:51
 

49. 스파르타 코딩 클럽 - BookSearchApp #1

📚 BookSearchApp Kakao API 를 이용하여 책을 검색하고, 정보를 받아온 뒤 선택한 책을 CoreData를 사용하여 내부저장소에 저장하는 앱을 만들어보자!ViewController 2개와 Modal을 사용하는 ViewController 1개까

coding-pill.tistory.com

이전 글 참고

  ✓  현재까지 작업 내용

[ 1. 최근 본  책 기능 구현 ]

1.1 사용자가 책 상세 보기 화면까지 살펴본 책이 있을 경우, 검색결과 리스트의 최상단에 최근 본 책을 보여줍니다.

// 모달까지 들어갔던 책의 정보를 담을 recentlyBook 변수 선언
var recentlyBook: [Book] = []
// section이 0일 때 recentlyBook.count 만큼 아이템 만들기
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    if section == 0 {
        return recentlyBook.count
    } else {
        return bookData.count
    }
}
// 검색 결과에서 셀 클릭 시 최근 본 책으로 데이터 저장
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
	if indexPath.section == 1 {
        let selectedBook = bookData[indexPath.item]
        self.recentlyBook.append(selectedBook)
    }
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
	
    // section이 0일 때 cell 설정
    if indexPath.section == 0 {

        guard let cell1 = collectionView.dequeueReusableCell(withReuseIdentifier: RecentlyCollectionViewCell.identifier, for: indexPath) as? RecentlyCollectionViewCell else {
            return collectionView.dequeueReusableCell(withReuseIdentifier: "DefaultCell", for: indexPath)
        }
		
        // 선택된 셀의 데이터인 recentlyBook[indexPath.item]을 book에 담기
        let book = recentlyBook[indexPath.item]

	    // imageUrl 변수에 book.thumbnail의 값 넣기 (String) 
        let imageUrl = book.thumbnail
        
        // cell1.imageUpdate 메서드에 매개변수로 imageUrl 을 넣고 이미지 뷰에 이미지 넣기
        cell1.imageUpdate(imageURL: imageUrl)

        return cell1
    }

 

 

1.2 최근 본 책을 '탭' 하면 책 상세 화면이 등장합니다.

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    if indexPath.section == 0 {
        let selectedBook = recentlyBook[indexPath.item]

        let modalVC = ModalViewController(bookData: selectedBook)
        modalVC.modalPresentationStyle = .automatic
        present(modalVC, animated: true, completion: nil)

    }

 

 

1.3 가장 최근에 본 책이 가장 최상단 (가로 스크롤 구현시 가장 왼쪽)에 위치합니다.

// 선택된 셀의 책 데이터
let selectedBook = bookData[indexPath.item]

// 0번째 인덱스 위치 (가장 왼쪽)
self.recentlyBook.insert(selectedBook, at: 0)

 

 

1.4 최근 본 책과 담은 책에 중복 방지 추가

아래 Trouble Shooting [ 2. 중복값 체크하기 ]에서 설명


  🎯  Trouble Shooting

[ 1. 헤더가 사라지지 않아 - Step4 최근 본 책이 없다면 섹션을 노출하지 않습니다 ]

Step4의 요구사항 중 '최근 본 책이 없다면 섹션을 노출하지 않습니다'라는 요구사항을 구현하는 방법으로

나는 viewForSupplementaryElementOfKind 메서드 안에서 indexPath.section과 recentlyBook을 조건으로 사용하여

섹션이 0이고, recentlyBook.count 가 0보다 크면 헤더를 표시

섹션이 0이고, recentlyBook.count 가 0이면 header.isHidden의 값을 true로 설정하여

헤더의 영역을 안 보이게 지우려고 했다.

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    if kind == UICollectionView.elementKindSectionHeader {
        guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CustomHeaderView.identifier, for: indexPath) as? CustomHeaderView else {
            return UICollectionReusableView()
        }

        if indexPath.section == 0 && recentlyBook.count > 0 {
            header.configure(with: "최근 본 책")
            header.isHidden = false
        } else if indexPath.section == 0 && recentlyBook.count == 0 {
            header.isHidden = true
        } else if indexPath.section == 1 {
            header.configure(with: "검색 결과")
        }

        return header
    }

    return UICollectionReusableView()

}

 

이유 : header.isHidden을 사용하는 경우 뷰는 숨겨지지만 공간은 그대로 존재한다.

 

해결 방법 : compositional layout을 설정하는 부분에서 처리해 주기

// 섹션 0 레이아웃 만들기
private func createSectionZeroLayout() -> NSCollectionLayoutSection {
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))

    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    item.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 0)

    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/4), heightDimension: .absolute(100))

    let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])

    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .continuous

    // 헤더의 높이를 조건을 주어서 설정
    // recentlyBook.count가 0이면 높이는 0, 0이 아니면 높이는 60
    let headerHeight: CGFloat = self.recentlyBook.count == 0 ? 0 : 60

    let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(headerHeight))

    let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)

    section.boundarySupplementaryItems = [header]

    return section
}

문제 해결!

 

 

[ 2. 중복값 체크하기! ]

사용자가 같은 책은 여러 번 클릭했을 때와 같은 책을 여러번 담기 버튼을 눌렀을 때

내 코드에는 중복 검사 로직이 없어서 최근 본 책에도 중복으로 담기고 있고,

CoreData 부분에도 중복으로 담기고 있는 문제가 있다.

 

이유 : 최근 본 책의 데이터를 추가하는 과정과 coreData에 담은 책 데이터를 추가하는 과정에서 별도의 중복 검사 로직이 없음

 

해결 방법 : 각 부분에 중복을 검사하는 로직을 작성하고 coreData의 중복 검사 로직은 CoreDataManager에 작성한다.

 

- 최근 본 책 -

// recentlyBook에 이미 들어있는 경우 중복 방지
if let existingIndex = recentlyBook.firstIndex(where: { $0.isbn == selectedBook.isbn }) {
    recentlyBook.remove(at: existingIndex)
}

 

 

- CoreDataManager -

// 중복 검사
func isBookExists(isbn: String) -> Bool {
    let request = NSFetchRequest<NSFetchRequestResult>(entityName: "BookData")
    request.predicate = NSPredicate(format: "isbn == %@", isbn)

    do {
        let count = try self.container.viewContext.count(for: request)
        return count > 0
    } catch {
        print("중복 확인 실패: \(error)")
        return false
    }
}

 

- 담은 책 - 

// 데이터의 중복 검사
if coredata.isBookExists(isbn: bookData.isbn) {

    // 중복이라면 화면에 alert 표시
    let alert = UIAlertController(title: "알림", message: "이미 담긴 책입니다.", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "확인", style: .default))
    self.present(alert, animated: true)
    return
}

 

 

[ 3. 사용자가 alert의 확인을 누르기 전에 alert가 사라지고 Modal이 내려가는 문제 ]

 

내가 alert의 확인 버튼을 누르지도 않았는데 왜 alert와 modal이 먼저 내려가??

 

 // 담기 완료 후 화면에 alert 표시
let alert = UIAlertController(title: "알림", message: "책 담기 완료!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "확인", style: .default))
self.present(alert, animated: true)


// Modal 내리기
self.dismiss(animated: true, completion: nil)

 

 

이유 : UIAlertController는 비동기적으로 표시되는데, self.present(alert, animated: true) 다음 줄에서 바로

self.dismiss(animated: true)가 호출되기 때문에 모달이 바로 사라진다.

 

해결 방법 : alert 코드의 확인 버튼 안에 dismiss 메서드를 작성하여 확인 버튼을 누를 때까지 alert 화면이 유지되고, 확인을 누르면 그 이후에 Modal이 내려간다.

// 담기 완료 후 화면에 alert 표시
let alert = UIAlertController(title: "알림", message: "책 담기 완료!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "확인", style: .default) { _ in
    // Modal 내리기
    self.dismiss(animated: true, completion: nil)
})
self.present(alert, animated: true)