React Query와 Intersection Observer API를 사용하여 프로젝트에 무한 스크롤링 구현해보기
무한 스크롤링을 구현하고 싶은데 어떻게 할 수 있을까?
현재, 진행 중인 프로젝트에서는 리액트 쿼리의 useQuery를 이용해서 비동기 상태 관리를 하고 있다.
이 상태에서 스크롤을 계속해서 내리면 데이터가 마지막이 보일 때까지 계속해서 요청을 하고 싶은데 어떻게 구현을 할 수 있을지 알아봤다.
리액트 쿼리를 사용하고 있으면, useInfiniteQuery가 내장되어 있어 사용할 수가 있는데, 이를 Intersection Obeserver API라는 내장 API와 함께 사용하여 현재 뷰포트의 마지막을 인지한 후 다음 데이터를 불러오게 할 수 있다!
뷰포트(viewport) 상에서 내가 지정한 관찰 대상자가 보이는지 관찰하는 관찰자이다.
(다 구현하고 이 글을 쓰면서 알게 된 사실인데, 이 API는 비동기로 실행기 되기 때문에 디바운싱이나 쓰로틀링 구현이 필요없다고 한다.. 괜히 쓰로틀링 따로 함수 만들어서 적용했었네^^ 지워야겠다)
이 API는 사용 될 수 있는 요소가 많은데 세 가지만 얘기하자면
이제 내 프로젝트에 구현을 해보도록 하자
일단, 나는 페이지 컴포넌트에서 무한 스크롤을 구현할 건데, 이 API 코드까지 있어버리면 너무 길고 정신 없을 거 같아서 커스텀 훅으로 따로 만들었다!

이렇게 만들었는데, 일단 관찰 대상을 정해주기 위해 ref를 정의해준다 (훅 이름은 잊어주세요.. throttle도 구현해야 하는 줄 알고 그랬었습니다아..)
그러고나서 useEffect 안에서 변수명을 observer로 하고 new IntersectionObserver(callback, options)로 정의해줬다.
여기서 callback 부분에 entries와 observer를 넣을 수 있는데, 나는 entries만을 넣어줬다(이 entries는 관찰 대상을 뜻한다)

이렇게 조건문을 달아줬는데, 나는 관찰대상이 하나밖에 없으니 entries[0]을 특정해서 isIntersecting(현재 뷰포트에 보여지는가)를 넣어줬다.
그 다음 hasNextPage와 isFetchingNextPage는 boolean 값인데 useInfiniteQuery에서 받아온 값을 이 훅을 호출할 때 인자로 넣어주기에 조건문에 함께 넣어준다.
여기서 !isFetchingNextPage로 넣어준 이유는, fetching 중이지 않을 때 다음 데이터를 fetch하기 위해서 넣은 것이다!
그래서 이 세가지의 조건을 모두 충족해야 fetchNextPage 함수가 실행하도록 했다.
그리고 다음 인자인 options에는

root, rootMargin, threshold 세 가지를 넣어준다.
root은 현재 viewport의 루트가 되는데, Default로 null이고 이 값이면 그냥 현재 보여지는 화면을 지정하게 된다.
rootMargin은 루트의 마진을 뜻하는데 이것도 딱히 마진을 정해주고 싶지 않으니 0px로 설정했다.
그리고 threshold는 관찰 대상이 현재 viewport에서 얼만큼 보여야 callback이 일어날 지 정해주는 건데, 1.0이면 100% 보일 때이고 나는 0.5로 설정해서 관찰 대상이 50프로만 보여도 callback이 실행되도록 했다.

그 다음에는 이렇게 useEffect 내에서 ref의 current가 발견되었을 때, observer.observe(loadMoreRef.current)를 실행시킨다. 그러면 아까 new IntersectionObserver에 정의해준 함수가 실행된다.
return에는 setTimeout을 clearTimeout 해주는 것처럼 observer.unobserve(loadMoreRef.current)도 꼭 넣어주자!
이제 이 커스텀 훅을 페이지에서 사용하는 것과 useInfiniteQuery 코드를 보여주겠다.
우선 이건 useInfiniteQuery 코드이다.

여기서 pageParam은 요청할 때의 페이지를 나타낸다. initialPageParam을 1로 설정해뒀다.
그래서 api 요청을 함수의 리턴값을 보면

이렇게 객체{} 형식으로 담아서, data와 nextCursor를 반환한다.
이 nextCursor는 getNextPageParam에서 쓰게 되는 것이다.
여기서 더이상 데이터가 없거나 데이터 형식이 false일 때도 꼭 return을 해줘야 하는데 {data: [], nextCursor: undefined}로 해줘야 더이상 불러올 데이터가 없을 때 계속적인 요청을 하지 않는다.
-> 이걸 해주지 않아서, 처음에 데이터가 더이상 없어도 무한으로 요청을 하는 거에 당황했었다.
저 useInfiniteQuery로 받은 fetchNextPage, hasNextPage, isFetchingNextPage 값을 내가 만든 useIntersectionObserver 커스텀 훅에 인자로 넣어줘서 ref를 받는다!

이 ref를 무한 스크롤링을 구현하는 페이지의 맨 마지막에 div를 삽입하여 넣어줬다. 
그러면 스크롤링을 하다가 맨 밑에 다다르면, useIntersectionObserver가 실행이 되고, 다음 데이터가 있으면 더 패치를 하고 없으면 그냥 그대로 끝으로 냅둔다.
이렇게 구현하니 데이터가 존재하는한 무한적으로 스크롤을 할 수 있게 되는 기능이 완성!
Intersection Observer API과 useInfiniteQuery를 모두 처음 사용하다보니 조금 시간이 걸렸지만, 안심식당 전체 조회와 검색된 지역명에 따른 조회 모두 하나의 쿼리로 만들어 조건문에 따라 다른 함수를 호출하게 만들었다.
결론적으로, 둘다 조회를 모두 무한 스크롤링과 함께 할 수 있게 되었다!