프로젝트 내에 한 리스트 페이지를 구현하면서 무한 스크롤 페이징 기능을 만들었다.
react-query의 useInfiniteQuery와 IntersectionObserver를 사용하여 구현해 보았는데,
다른 페이지에서 반복적으로 사용될 수 있는 로직이 있어 custom hooks으로 분리하여 리팩토링을 해주었다.
이를 블로그로 정리해보고자 한다.
무한 스크롤 페이징의 구현
무한 스크롤 페이징의 구현방식은 다음과 같다.
- useInfiniteQuery를 통해 리스트 데이터를 일부 호출하여 보여준다.
- IntersectionObserver를 통해 view에서 지금까지 호출된 데이터의 리스트가 다 보였을 때를 target으로 인지하고
- target이 보였을 때 && list에 다음 페이지 데이터가 있을 때 다음 페이지 데이터를 호출해 준다.
Before (custom Hooks 적용 전)
function useBooks() {
const books = useGetBooksQuery()
const targetRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!targetRef || !books.hasNextPage) return
if (targetRef.current) {
const observer = new IntersectionObserver(async (entries, observer) => {
if (entries[0].isIntersecting) {
await estimates.fetchNextPage()
}
})
observer.observe(targetRef.current)
}
}, [])
return {books, targetRef}
}
export default useBooks
function Books() {
const {books, targetRef} = useBooks()
return (
<>
// ...
<BookList data={books} targetRef={targetRef} />
</>
)
}
export default Books
books의 데이터는 변하지 않지만, UI는 언제든지 바뀔 가능성이 있으므로, 데이터와 UI를 분리하여 구성해주고자 하였다.
books에 대한 데이터를 추상화하여 useBooks
라는 hooks로 표현해 주었고,
Books
는 useBooks
에서 반환된 값을 어떻게 보여주는지 정의해 주었다.
위와 코드 구성은 디자인이 다르지만 같은 books 데이터를 필요로 할 때 useBooks
hooks를 가져다 사용할 수 있게 해준다.
변경에 유연해지려면 각 모듈이 한 가지 일만 하는 것이 중요하다.
하지만, useBooks에는 books데이터를 get 해오는 기능뿐만 아니라, infiniteScroll의 기능까지 함께 들어있었다.
다른 페이지에서 infiniteScroll기능을 구현해야 할 때 중복이 발생할 것이다.
동일하게 사용될 수 있는 infiniteScroll 코드를 custom hooks으로 빼보자.
+ BookList 컴포넌트도 전역적으로 사용할 수 있는 List로 교체해 줄 것이다.
useInfiniteScroll
export interface UseInfiniteScrollProps
extends Pick<UseInfiniteQueryResult<any>, 'isFetchingNextPage' | 'fetchNextPage' | 'hasNextPage'> {}
function useInfiniteScroll({hasNextPage, isFetchingNextPage, fetchNextPage}: UseInfiniteScrollProps) {
const [target, setTarget] = useState<HTMLDivElement | null>(null)
useEffect(() => {
if (!hasNextPage || isFetchingNextPage) return undefined
let observer: IntersectionObserver
if (target) {
observer = new IntersectionObserver(async ([entry]) => {
if (entry.isIntersecting) {
await fetchNextPage()
}
})
observer.observe(target)
}
return () => observer && observer.disconnect()
}, [target, hasNextPage, isFetchingNextPage])
return {setTarget}
}
export default useInfiniteScroll
infiniteScroll을 담당하는 로직만 useInfinteScroll hooks으로 분리해 주었다.
import {InfiniteData} from 'react-query'
export interface IListProps extends IUseInfiniteScrollProps {
name: string
isSuccess?: boolean
isLoading?: boolean
data?: InfiniteData<any>
renderItem: (item: any) => ReactNode
}
function List({
name,
renderItem,
data,
isLoading,
isSuccess,
hasNextPage,
isFetchingNextPage,
fetchNextPage
}: IListProps) {
const {setTarget} = useInfiniteScroll({hasNextPage, isFetchingNextPage, fetchNextPage})
if (isSuccess && !data) return <StyledList>데이터가 없습니다(</StyledList>
if (isLoading || !data)
return (
<StyledList>
<LoadingSkeleton />
</StyledList>
)
return (
<StyledList>
{data.pages?.map((page) =>
page.data.map((item: any, index: number) =>
renderItem(item) &&
<ListItem key={`${index}_${name}`}>{renderItem(item)}</ListItem>
)
)}
{isFetchingNextPage ? <LoadingSkeleton /> : <div ref={setTarget} />}
</StyledList>
)
}
export default List
그리고 무한 스크롤과 같이 사용될 List컴포넌트를 만들고 여기서 useInfiniteScroll을 사용해 주었다.
function Books() {
const {books} = useBooks()
return (
<>
// ...
<List {...books} name="books" renderItem={(item) => <ListItem {...item} />} />
</>
)
}
export default Books
useBooks는 데이터를 패치하는 로직만 남게 될 것이고, Books 컴포넌트는 다음과 같이 바뀌게 된다.
마무리
리팩토링 하는 과정에서 어떻게 하면 유연하고 변경에 대응 가능한 컴포넌트를 만들 수 있는지 고민을 많이 했던 것 같다.
이전에는 그냥 어떤 기준 없이 중복이니까 분리하거나, 단순히 크다는 이유로 분리하고는 했다.
아래와 같이 기준을 세우고 항상 기준 내에 부합하는지, 어떻게 하면 좀 더 변경에 유연한 컴포넌트를 만들 수 있을지,
항상 인지하면서 코드를 개발해야겠다.
1. headless 기반의 추상화(변하는 것 vs 상대적으로 변하지 않는 것)
2. 한 가지 역할만 하기 (또는 한가지 역할만 하는 컴포넌트의 조합으로 구성하기)
3. 도메인 분리하기 (도메인을 포함하는 컴포넌트와 그렇지 않은 컴포넌트 분리하기)
참고 :
'Front-end > React' 카테고리의 다른 글
[React] 타이머 구현하기 (0) | 2023.05.07 |
---|---|
[React] 디바운싱 적용하여 자동 계산 성능 개선하기 (0) | 2023.03.21 |
[React] Proxy 설정하기 (0) | 2022.09.29 |
[Next] create-next-app 분석하기 (0) | 2022.08.14 |
[React] .env로 개발/배포 환경 설정하기 (0) | 2022.07.01 |