[내일배움캠프 11주차 (03/18)]

yeseul jang·2026년 3월 18일

내일배움캠프

목록 보기
23/32

시연 영상

시연 영상

🤨 홈 화면 관련 고민했던 점

1. CollectionView를 diffable datasource로 변경해야하는가?

  • 지금 코드를 보면 홈 화면 데이터는
    viewDidLoad -> fetchHomeSectionsData() -> sections 저장 -> reloadData()
    이 흐름이고, 한 번 받아와서 보여주는 형식이다. 게다가 섹션도 고정이고, 아이템만 채워지는 구조다.
  • diffable datasource로 바꾸려면 Hashable, snapshot 구성, supplementaryView 처리 방식까지 같이 정리해야 해서 복잡도는 올라감.
  • 앞으로 홈 화면 데이터가 자주 바뀌거나, 검색/필터/좋아요/동적 섹션 추가 같은 기능이 붙을 가능성이 있으면 그때 diffable datasource로 가는 게 더 의미 있다고 생각되긴 함

2. pageControl을 지금 클로저로 전달하게 해 두었는데 이것도 RxSwift 이용하는 걸로 바꾸는게 좋을까?

  • 현재 구조를 보면 HomeView 안에서 visibleItemsInvalidationHandler로 페이지를 계산하고, 그걸 changeToCurrentPage 클로저로 HomeViewController에 전달해서 해당 footerpageControl.currentPage를 바꾸고 있음.
  • 페이지 변경을 UI 내부 이벤트로 보고 그 이벤트를 한 군데로 전달하고 전달받은 VCpageControlUI를 갱신하는 구조.
  • 비즈니스 로직도 아니고, 이 내용을 다른 뷰에서 알 필요도 없고, 뷰 내부 레벨에서의 동기화하는 식이라 불필요하다고 판단함

🥹 검색화면 고민했던 점

1. 역할 나누기

  • HomeViewController
    • 검색 버튼 눌림 감지
    • 검색어 꺼내기
    • SearchViewController 생성
    • 검색어 전달
    • 다음 화면 push
  • SearchViewController
    • 전달받은 검색어를 searchBar에 넣기
    • 그 검색어를 ViewModel에 전달
    • ViewModel output 받아서 컬렉션뷰 갱신
  • SearchViewModel
    • 검색어로 API 요청
    • song / album / podcast 결과 가공
    • sections / loading / error 내보내기

2. 서치뷰 흐름

검색어 입력
-> 잠깐 멈출 때까지 기다림
-> 같은 검색어면 무시
-> 최신 검색어만 사용
-> 비어 있으면 빈 결과
-> 아니면 로딩 시작
-> API 요청
-> 성공하면 sections 방출 + 로딩 종료
-> 실패하면 에러 메시지 방출 + 로딩 종료 + 빈 결과 방출
-> UI가 쓰기 좋은 Output으로 반환

3. 서치뷰 예외처리

    func transform(input: Input) -> Output {
        let loadingRelay = BehaviorRelay<Bool>(value: false)
        let errorRelay = PublishRelay<String>()
        
        let sections = input.queryText
            .debounce(.milliseconds(400), scheduler: MainScheduler.instance)
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .distinctUntilChanged()
            .flatMapLatest { [weak self] query -> Observable<[SearchSectionModel]> in
                guard let self else { return .just([]) }
                
                if query.isEmpty {
                    return .just([])
                }
                
                loadingRelay.accept(true)
                
                return self.fetchSearchData(query: query)
                    .asObservable()
                    .do(onNext: { _ in
                        loadingRelay.accept(false)
                    })
                    .catch { error in
                        loadingRelay.accept(false)
                        let message = self.makeErrorMessage(from: error)
                        errorRelay.accept(message)
                        return .just([])
                    }
            }
            .asDriver(onErrorJustReturn: []) 
        

[]가 나오는 경우는 최소 3개:

  1. 초기 상태
    • 아직 검색 안 함
    • query가 빈 문자열이라 .just([])
  2. 검색 결과 없음
    • API 성공
    • 결과가 진짜 0개
  3. 에러 발생 후 대체값
    • catch에서 .just([])

그냥 다 빈값이라고 한 번에 처리 해버리면 어색할 수 있음

3-1. 해결 방안

struct Output {
        let sections: Driver<[SearchSectionModel]>
        let isLoading: Driver<Bool>
        let errorMessage: Signal<String>
        let isEmpty: Driver<Bool>
    }
    
    func transform(input: Input) -> Output {
        let loadingRelay = BehaviorRelay<Bool>(value: false)
        let errorRelay = PublishRelay<String>()
        
        let sections = input.queryText
            .debounce(.milliseconds(400), scheduler: MainScheduler.instance)
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .distinctUntilChanged()
            .flatMapLatest { [weak self] query -> Observable<[SearchSectionModel]> in
                guard let self else { return .empty() }
                
                if query.isEmpty {
                    return .just([])
                }
                
                loadingRelay.accept(true)
                
                return self.fetchSearchData(query: query)
                    .asObservable()
                    .do(onNext: { _ in
                        loadingRelay.accept(false)
                    })
            }
            .asDriver(onErrorRecover: { error in
                loadingRelay.accept(false)
                let message = self.makeErrorMessage(from: error)
                errorRelay.accept(message)
                return .empty()
            })
        let isEmpty = sections
            .map { $0.isEmpty }
            .distinctUntilChanged()
        
        return Output(
            sections: sections,
            isLoading: loadingRelay.asDriver(),
            errorMessage: errorRelay.asSignal(),
            isEmpty: isEmpty
        )
    }
  • asDriver(onErrorRecover:): asDriver 메서드 변경해서 오류처리 할 수 있도록 변경
  • 빈값을 내보내는 부분들을 .empty() 변경해서 상황을 더 명확하게 나눔
  • Outputlet isEmpty: Driver<Bool> 추가해서 sections가 비었을 때 어떤 UI를 보여줄건지 결정할 수 있도록 함
profile
iOS 개발

0개의 댓글