탐색 페이지가 완성되었습니다!
탐색 페이지에서는 국가별, 장르별 필터링을 적용해 정렬 기준을 정해서 탐색을 할 수 있습니다. 영화 리스트에는 Intersection Observer
, Infinite Query
를 사용한 무한스크롤을 적용하였습니다.
탐색 페이지는 크게 세 가지 필터 옵션(국가, 장르, 정렬)과 영화 목록을 보여주는 부분으로 나뉩니다. 각 필터는 사용자가 선택한 값을 반영해 영화 목록을 다시 렌더링합니다. 이때, 선택한 필터는 useState를 통해 관리되며, 사용자가 탐색하기
버튼을 클릭하면 필터를 일괄 적용해 영화 데이터를 새로 불러옵니다.
실제 탐색하기를 눌러 API 탐색 파라미터에 반영되기 전 어떠한 필터가 선택되어있는지에 대한 낙관적 업데이트가 필요하므로 필터 상태 관리는 다음과 같이 이루어집니다.
// 장르
const [selectedGenreIdTemp, setSelectedGenreIdTemp] = useState<number[]>([]);
const [selectedGenreId, setSelectedGenreId] = useState<number[]>(selectedGenreIdTemp);
이렇게 임시 상태를 관리해 UI 상에서 보여주다가, 사용자가 검색을 클릭하면 최종적으로 필터가 반영됩니다.
react-query
의 useInfiniteQuery
와 Intersection Observer
를 사용하여 무한스크롤을 적용하였습니다.
useInfiniteQuery
를 사용한 페이징 처리useInfiniteQuery
는 데이터를 page 단위로 가져오는 API 호출을 관리할 수 있습니다. 즉, 하나의 요청이 끝날 때 다음 페이지를 자동으로 요청하여 데이터 목록을 계속 불러옵니다. queryFn
, getNextPageParam
, 그리고 initialPageParam
등을 통해 페이징 요청을 쉽게 처리할 수 있어서 아주 편리합니다.
src/hooks/react-query/use-query-discover.ts
const query = useInfiniteQuery<TMovieListsFetchRes, Error>({
queryKey: [QUERY_KEY.movieDiscoveredResults, language, sortBy, genres, withOriginalLanguage],
initialPageParam: 1,
queryFn: async ({ pageParam }) =>
await discoverRequest.fetchMovieDiscoverResults(
language,
sortBy,
genres,
pageParam as number | undefined,
withOriginalLanguage,
),
getNextPageParam: (lastPage, pages) => {
return lastPage.total_pages > pages.length ? pages.length + 1 : undefined;
},
});
initialPageParam
- 첫 번째 페이지를 요청할 때 사용할 페이지 번호입니다. 기본적으로 첫 번째 페이지(1)를 불러오도록 설정하였습니다.
getNextPageParam
- 이 함수는 다음 페이지를 언제 요청할지 결정합니다. 만약 불러온 페이지가 전체 페이지보다 적으면, 다음 페이지 번호를 반환하고, 더 이상 페이지가 없다면 undefined를 반환하여 요청을 중지하는 로직입니다.
IntersectionObserver
를 통한 무한 스크롤 트리거위 커스텀 훅에서 페이지 데이터를 불러오는 로직을 구성했으니, 이제는 스크롤이 끝에 도달했을 때 다음 페이지를 요청하는 부분을 구현해야 합니다. 이를 위해 IntersectionObserver
를 사용하여 특정 DOM 요소가 화면에 나타날 때마다 데이터를 추가로 요청하도록 설정하였습니다.
Intersection Observer API 는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage(); // 다음 페이지 데이터를 요청하는 함수
}
},
{ threshold: 1 },
);
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current); // 로드할 DOM 요소가 화면에 나타나면 관찰 시작
}
return () => {
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); // 컴포넌트가 언마운트될 때 관찰 해제
};
}, [hasNextPage, fetchNextPage]);
...
return (
<S.Container>
...
<div ref={loadMoreRef} />
{isFetchingNextPage && <MovieListSkeleton height={160} />} //로드될 동안 보여줄 로딩아이콘
</S.Container>
);
IntersectionObserver
는 특정 DOM 요소가 뷰포트에 들어올 때 콜백 함수를 실행하는 역할을 합니다. 여기서 entries[0].isIntersecting
는 대상 요소가 화면에 나타났는지를 확인하는 조건입니다. 이 조건이 true가 되고, 다음 페이지가 남아 있으면(hasNextPage가 true), useInfiniteQuery
가 제공하는 fetchNextPage
함수를 호출하여 사용자는 다음 페이지의 데이터를 요청해 끊김 없이 영화 목록을 스크롤하면서 볼 수 있게 됩니다.