[React] 무한 스크롤로 결정했는데 페이지네이션으로 갈아 엎게 되면서 두 개 다 공부한 내용 정리

초코침·2023년 8월 7일
0

React

목록 보기
5/14
post-thumbnail

PliP 프로젝트에서 장소 검색 결과를 보여주는 방법으로 무한스크롤과 페이지네이션 중에 고민하다 무한스크롤로 결정했는데,

무한스크롤로 만들고 나니 검색된 장소 표시를 위한 마커가 스크롤할 때마다 지도 위에 새롭게 뿅뿅 생겨나는 게 UX가 별로라고 느껴져서 페이지네이션으로 변경했다.

결과적으로 무한스크롤과 페이지네이션을 모두 구현해보게 됐다. 오히려 좋음!

무한스크롤 구현하기

React Query에서는 파라미터를 변경하여 동일한 쿼리를 무한으로 호출할 때 사용할 수 있는 useInfiniteQuery 훅을 제공한다.

이 훅을 사용해 무한 스크롤을 아주 간편하게 구현할 수 있었다.


우선, 스크롤이 하단까지 이동되었는지를 확인하기 위한 방법으로 react-intersection-observer 라이브러리를 사용했다.

해당 라이브러리를 설치하고 커스텀 훅 useIntersectionObserver를 만들어 범용적으로 사용할 수 있도록 했다.

이 훅은 커스텀훅이 반환하는 ref를 가진 컴포넌트가 뷰포트 안에 들어왔을 때 파라미터로 받은 callback 함수를 실행시키는 역할을 한다.

// useIntersectionObserver.tsx
import { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';

const useIntersectionObserver = (callback: () => void) => {
  const { ref, inView } = useInView({ threshold: 0.5 });

  useEffect(() => {
    if (inView) {
      callback();
    }
  }, [inView]);

  return { ref };
};

export default useIntersectionObserver;
// useIntersectionObserver 사용 예시
const { ref } = useIntersectionObserver(() => {
	// 더 불러올 결과가 있다면(hasNextPage) 불러오기
});

다음으로 useInfiniteQuery 훅을 활용하는 코드를 작성해야 한다.

useInfiniteQuery는 useQuery와 마찬가지로 queryKey, queryFn 를 props로 넘겨주고 추가로 getNextPageParam을 넘겨야 한다.


위에서 useInfiniteQuery는 파라미터를 변경해 동일한 쿼리를 호출한다고 했었는데, 그 변경할 파라미터가 pageParam이다.

따라서 pageParam에 따라 요청할 수 있게 query function은 pageParam을 받도록 작성돼야 한다.

이때 pageParam은 기본적으로 undefined기 때문에 기본값을 주어 1 페이지부터 불러올 수 있도록 했다.

queryFn: ({ pageParam = 1 }) => searchPlaceByKeyword({ pageParam, keyword, currentX, currentY }); // keyword, currentX/Y는 검색을 위해 사용한 변수라 중요한 건 pageParam이다.

이때 pageParam은 getNextPageParamgetPreviousPageParam을 통해 값을 전달 받기 때문에 보여줄 내용이 더 남아 있는 경우 다음 페이지 번호를 리턴하도록 getNextPageParam을 다음과 같이 작성해줬다.

여기서 lastPage와 allPages는 각각 마지막으로 받은 데이터, 전체 데이터를 의미하며, 다음 페이지가 없을 경우 undefined를 리턴한다(이는 hasNextPage를 false로 만든다.).

getNextPageParam: (lastPage, allPages) => lastPage.pagination.current < lastPage.pagination.last && lastPage.pagination.current + 1

내 상황의 경우 이전 페이지를 참조할 필요는 없었기 때문에 getPreviousPageParam은 작성하지 않았다.


이렇게 작성한 useInfiniteQuery는 data를 비롯해 fetchNextPagehasNextPage 등을 반환해 주는데 이를 useIntersectionObserver의 콜백으로 넘겨주면 무한 스크롤을 완성할 수 있다.

참고로 이때 hasNextPagegetNextPageParam의 리턴값에 따라 결정된다.

// useIntersectionObserver 사용 예시
const { ref } = useIntersectionObserver(() => {
	if(hasNextPage) fetchNextPage();	
});

페이지네이션 구현하기

이미 무한스크롤을 구현을 해놨기 때문에 useInfiniteQuery 코드를 조금만 수정하면 진짜 금방(20분 정도면 될 줄 알았다 ㅋㅋ) 끝날 줄 알았는데 그렇지 않았다.


useInfiniteQuery 대신 useQuery를 사용하기로 변경하면서 시간이 좀 더 걸렸기 때문이다.

물론 infinite query로 페이지네이션을 아예 구현할 수 없지는 않겠지만,

이전 또는 이후 페이지가 아닌 (페이지 번호를 클릭하여) 현재 페이지에서 많이 떨어져 있는 다른 페이지로도 바로 이동할 수 있어야 한다는 점에서 getNextPageParam, getPreviousPageParam를 사용하기에 애매한 부분이 있다고 생각했다.


이미 작성해 놓은 무한스크롤 코드를 버려야 해서 너무나 아쉬운 마음이 들었지만.. 아무튼 이러한 이유로 infinite query의 방식이 페이지네이션과는 결이 다르다고 생각했기 때문에 useQuery로 변경하게 되었다.

그리고 무엇보다 공식 문서에서도 페이지네이션을 useQuery를 사용해 구현하고 있다.


useQuery로 페이지네이션을 구현하는 컨셉은 (다 하고 보니 의외로) 간단했다.

현재 페이지 번호를 상태로 갖고 있고 그 상태가 변하면 새로운 요청을 보낼 수 있도록 쿼리 요청을 할 때 페이지 번호를 함께 주면 된다. 요청을 보낼 때 페이지 번호가 필요하게 되므로 페이지 번호도 쿼리 키로 가지고 있어야 한다.

queryKey: [ pageParam, ... ],
queryFn: () => searchPlace({ pageParam, ... }),

단, 페이지 번호가 바뀌면 새로운 요청이 발생하는 것이기 때문에 쿼리의 상태가 loading으로 변경된다.

보통 loading 상태가 되면 그에 걸맞는 폴백 컴포넌트를 두기 때문에, loading일 때 “검색 중”이라는 문구를 보여주도록 했다면 페이지 번호를 클릭할 때마다 검색 중이라는 문구를 보게 된다.

일반적으로 사용자는 페이지 번호를 누를 때, 이미 검색은 끝났고 그저 다음 페이지에 해당하는 내용을 보여줄 것으로 기대하지 새롭게 검색을 하기 위한 대기 시간이 발생할 것을 기대하진 않을 것이다.

따라서 위 처럼 페이지 번호를 클릭할 때마다 검색 중이라는 문구가 보인다면 자연스런 UX를 구현했다고 보긴 어렵다.


useQuery에서는 이렇게 페이지네이션 같은 기능 구현 시에 유용할 수 있는 keepPreviousData 옵션을 제공한다.

useQuery({
	// ...
	keepPreviousData: true,
})

keepPreviousData를 true로 설정하면 쿼리 키(여기서는 페이지 번호)가 변경되어 새 데이터가 요청되는 동안(loading) 마지막으로 가져온 데이터를 계속 사용할 수 있게 해 준다.

즉, loading 상태더라도 “검색 중”이라는 폴백 대신 이전 페이지의 데이터를 계속해서 보여주다가 데이터가 성공적으로 수신되면 그 때 새로운 데이터를 보여준다.

결론적으로,

react query 사용법을 익힐 수 있는 경험이 됐다.

useInfiniteQuery로 무한 스크롤을 간편하게 구현할 수 있어 완성하고 내심 기뻤는데 페이지네이션으로 갈아 엎어야겠다는 생각이 들어서 슬펐지만
두 훅을 사용하고 비교해 보면서 많은 공부가 되었다.

그리고 이 검색 api에 페이지네이션 관련 데이터가 잘 나와 있어서 구현하는 데에 편리했다.

앞으로 이렇게 요청과 관련해서 공부할 일이 생긴다면 잘 구현되어 있는 오픈 api 활용해 공부하면 될 것 같다.

profile
블로그 이사중 🚚 (https://sungjihyun.vercel.app)

0개의 댓글