fix: 책 검색 모달 탭 유지 및 무한스크롤 기능 추가#166
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough검색 결과 페이징을 도입했습니다. 변환 유틸에 startIndex 파라미터를 추가해 ID 산정 방식을 변경하고, BookList에 인터섹션 옵저버 기반의 무한 스크롤을 추가했습니다. BookSearchBottomSheet와 useBookSearch 훅은 다음 페이지 로딩, 상태 플래그, API 호출 흐름을 반영하도록 확장되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as User
participant BL as BookList (IntersectionObserver)
participant BS as BookSearchBottomSheet
participant HOOK as useBookSearch
participant API as getSearchBooks
participant CVT as convertToSearchedBooks
U->>BL: 스크롤하여 마지막 아이템 노출
BL-->>BL: 조건 확인(isSearchMode && hasNextPage && !isLoadingMore)
BL->>BS: onLoadMore()
BS->>HOOK: loadMoreSearchResults()
HOOK->>API: 검색 API 호출(page = currentPage+1)
API-->>HOOK: 응답(data, last)
HOOK->>CVT: convertToSearchedBooks(items, startIndex = 기존길이)
CVT-->>HOOK: 변환된 SearchedBook[]
HOOK-->>BS: 상태 업데이트(append, hasNextPage, isLoadingMore=false)
BS-->>BL: props 업데이트(hasNextPage/isLoadingMore)
BL-->>U: 더 많은 결과 표시
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (7)
src/api/books/getSearchBooks.ts (1)
61-69: SearchedBook 인터페이스 중복 정의 통합 권장타입 드리프트를 방지하고 유지보수성을 높이기 위해
SearchedBook인터페이스를 한 곳에서만 정의하고 나머지에서는 import 해서 사용하는 구조로 리팩토링을 권장합니다.확인된 중복 정의 위치:
- src/api/books/getSearchBooks.ts (line 13):
export interface SearchedBook { … }- src/pages/search/Search.tsx (line 16):
export interface SearchedBook { … }제안:
- 공통 타입 정의 전용 파일(e.g.
src/types/SearchedBook.ts) 또는 기존getSearchBooks.ts에서만exportSearch.tsx등 다른 파일에서는 해당 타입을 import하여 사용src/components/common/BookSearchBottomSheet/BookList.tsx (2)
44-58: 무한스크롤 중복 호출 가능성: IntersectionObserver 콜백에 isLoadingMore 체크 및 안전 처리 필요현재 콜백 조건에
isLoadingMore가 포함되어 있지 않아, 관찰 대상이 한 번에 여러 차례 교차되면 onLoadMore가 연속 호출될 수 있습니다. 네트워크 중복 요청과 결과 중복 누적을 방지하려면 콜백에서 로딩 중 여부를 추가로 확인하고, Promise 거부를 안전하게 무시(또는 로깅)하세요. 또한 약간의 rootMargin을 주면 사용자 도달 직전에 프리페치가 가능합니다.적용 예시:
- observerRef.current = new IntersectionObserver(entries => { - if (entries[0].isIntersecting && hasNextPage && onLoadMore && isSearchMode) { - onLoadMore(); - } - }); + observerRef.current = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasNextPage && onLoadMore && isSearchMode && !isLoadingMore) { + onLoadMore().catch(() => {}); + } + }, + { root: null, rootMargin: '160px 0px', threshold: 0 } + );
37-39: 불필요한 ref 제거 제안
loadingRef가 선언만 되고 별도 사용되지 않습니다. 제거하여 간소화하세요.- const loadingRef = useRef<HTMLDivElement | null>(null); ... - <LoadingContainer ref={loadingRef}> + <LoadingContainer>Also applies to: 86-88
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)
107-116: isSearchMode 계산은 훅에서 제공하도록 일관성 유지 제안여기서
isSearchMode={searchQuery.trim() !== ''}를 직접 계산하고 있는데, 훅(useBookSearch) 내부에서 동일 로직의 불변값을 계산해 함께 반환하면(예:isSearchMode) 뷰/훅 간 중복을 줄이고 변경 시 한 곳만 고치면 됩니다.- isSearchMode={searchQuery.trim() !== ''} + isSearchMode={isSearchMode}훅에서
isSearchMode를 이미 계산하고 있다면 공개(return) 목록에 포함시켜 재사용하는 방향을 권장합니다.src/components/common/BookSearchBottomSheet/useBookSearch.ts (3)
82-99: 검색 API 호출 시 isFinalized를 true로 고정: 요구사항 확인 필요
getSearchBooks(query.trim(), page, true)로 항상 확정 도서만 검색하는 형태라면, 사용자가 기대하는 결과(예: 모든 상태의 도서 검색)와 다를 수 있습니다. 서버 사양/요구사항을 다시 확인해 주세요. 필요 시 UI에서 필터로 제어하거나 훅의 옵션으로 노출하는 것이 더 유연합니다.
101-113: 검색 응답 레이스 컨디션 방지: 최신 요청만 반영하도록 가드 필요디바운스/무한스크롤 특성상, 늦게 도착한 이전 요청 응답이 최신 검색 결과를 덮어쓸 수 있습니다. 간단한 요청 키 가드로 해결 가능합니다.
핵심 변경 예시:
try { + const searchKey = `${query.trim()}::${page}`; + latestSearchKeyRef.current = searchKey; if (isNewSearch) { setIsLoading(true); setCurrentPage(1); } else { setIsLoadingMore(true); } setError(null); const response = await getSearchBooks(query.trim(), page, true); if (response.isSuccess) { + if (latestSearchKeyRef.current !== searchKey) { + // 이미 더 최신 요청이 진행/완료됨 → 이번 결과는 폐기 + return; + } const startIndex = isNewSearch ? 0 : searchResults.length; const convertedResults = convertToSearchedBooks(response.data.searchResult, startIndex);이 변경을 위해 훅 상단에 ref를 하나 추가해야 합니다(아래 참고).
추가로 필요한 import/선언(선택 적용):
// 상단 import에 추가 import { useRef } from 'react'; // 훅 내부 상단에 추가 const latestSearchKeyRef = useRef('');Also applies to: 126-131
134-142: loadMore 가드 보강 제안초기 검색 로딩 중(isLoading=true)에도 교차 이벤트가 발생하면 loadMore가 호출될 수 있습니다. 가드를 한 줄 보강하면 안전합니다.
- if (!searchQuery.trim() || isLoadingMore || !hasNextPage) { + if (!searchQuery.trim() || isLoadingMore || isLoading || !hasNextPage) { return; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
-
src/api/books/getSearchBooks.ts(1 hunks) -
src/components/common/BookSearchBottomSheet/BookList.tsx(3 hunks) -
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx(2 hunks) -
src/components/common/BookSearchBottomSheet/useBookSearch.ts(5 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (1)
BookList(159-163)
src/api/books/getSearchBooks.ts (1)
src/pages/search/Search.tsx (1)
SearchedBook(16-23)
src/components/common/BookSearchBottomSheet/useBookSearch.ts (1)
src/api/books/getSearchBooks.ts (2)
getSearchBooks(41-59)convertToSearchedBooks(61-70)
src/components/common/BookSearchBottomSheet/BookList.tsx (1)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (3)
BookList(159-163)BookItem(165-177)LoadingText(220-224)
🔇 Additional comments (2)
src/components/common/BookSearchBottomSheet/BookList.tsx (1)
69-75: 키 구성 방식 적절함
key={${book.id}-${book.isbn}}로 페이지 누적 시에도 키 충돌 가능성을 잘 피했습니다. 무한스크롤 시 안정성이 좋아집니다.src/components/common/BookSearchBottomSheet/useBookSearch.ts (1)
206-207: 탭 표시 정책 단순화 LGTM검색 모드가 아닐 때 탭을 항상 노출하도록 간소화한 부분이 PR 목적과 일치합니다.
#️⃣ 연관된 이슈
생략
📝 작업 내용
이번 PR에서는 책 검색 바텀시트의 사용성을 크게 개선하는 두 가지 주요 문제를 해결했습니다.
1. 탭 사라짐 문제 해결
기존에는 '내 모임 책' 탭으로 전환했을 때 등록된 책이 없어서 예외처리 화면(빈 상태)이 표시되면 탭 자체가 사라지는 문제가 있었습니다. 이는 사용자가 다른 탭으로 다시 전환할 수 없게 만드는 치명적인 UX 문제였습니다. useBookSearch.ts의
showTabs로직을 수정하여 검색 모드가 아닐 때는 빈 상태와 관계없이 항상 탭이 표시되도록 개선했습니다.2. 검색 결과 무한스크롤 기능 구현
검색 결과가 많을 때 첫 번째 페이지만 표시되고 추가 결과를 볼 수 없었던 문제를 해결했습니다. API는 이미 페이지네이션을 지원하고 있었지만 클라이언트에서 이를 활용하지 못하고 있었습니다. Intersection Observer API를 활용하여 사용자가 목록 하단에 도달하면 자동으로 다음 페이지를 로드하는 무한스크롤을 구현했습니다. 또한 페이지 간 책 ID 중복을 방지하기 위해 각 페이지의 시작 인덱스를 고려한 고유 ID 생성 로직도 추가했습니다.
🕸️ 주요 변경사항
currentPage,hasNextPage,isLoadingMore) 추가performSearch함수: 신규 검색과 추가 로딩을 구분하여 결과를 누적하는 로직 구현Intersection Observer를 이용한 무한스크롤 감지 및 로딩 상태 표시startIndex파라미터 추가Summary by CodeRabbit