TL;DR
- ๋ฌดํ์คํฌ๋กค
- Intersection Observer API
- React-Query์ Intersection Observer API๋ฅผ ์ฌ์ฉํด ๋ฌดํ์คํฌ๋กค ๊ตฌํํ๊ธฐ
- ์ค์ ์๋น์ค์ ์ ์ฉํ๊ธฐ
๋ฐฐ๊ฒฝ
์ธ์ฌ์ดํธ์์์์๋ ๋ด๊ฐ ๊ฐ์ง ๊ฒฝํ์ ์ ๋ฆฌํ๊ณ ์ด๋ฅผ '๊ฒฝํ ์นด๋'๋ก ์ ์ฅํ ์ ์๋ค.
์ด ๊ฒฝํ์นด๋๋ ๋ณธ๊ฒฉ์ ์ผ๋ก ์๊ธฐ์๊ฐ์๋ฅผ ์์ฑํ ๋ ์ฐธ๊ณ ํ์ฌ ์์ฑํ ์ ์๋ค.
์ด ๋, ํ ํ์ด์ง์์ ๋ด๊ฐ ๊ฐ์ง ๊ฒฝํ์นด๋์ ๋ด์ฉ์ ํ์ธํ๊ณ ์ด๋ฅผ ํ ๋๋ก ์๊ธฐ์๊ฐ์๋ฅผ ์์ฑ์ ๋น ๋ฅด๊ฒ ํ ์ ์๋๋ก ๊ธฐํํ์๋ค.
๋ฌดํ์คํฌ๋กค
๋ฌดํ์คํฌ๋กค์ ์ฌ์ฉ์๊ฐ ์คํฌ๋กค์ ์๋๋ก ๋ด๋ฆฌ๋ฉด ์๋ก์ด ์ฝํ ์ธ ๊ฐ ์๋์ผ๋ก ๋ก๋๋์ด ๋ณด์ฌ์ฃผ๋ ๋ฐฉ์์ด๋ค.
์ฌ์ฉ์๊ฐ ํ์ด์ง๋ฅผ ์ด๋ํ์ง ์์๋ ๊ณ์ํด์ ์๋ก์ด ์ฝํ ์ธ ๋ฅผ ๋ณผ ์ ์์ด ํธ๋ฆฌํ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ ์ ์๋ค.
๋ฌดํ์คํฌ๋กค์ ๊ตฌํํ๊ณ , ๊ฒฝํ์นด๋ ์กฐํ์ ๋ฌดํ์คํฌ๋กค์ ์ ์ฉํ์ฌ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ ๋ด์ฉ์ ์๊ฐํ๊ณ ์ ํ๋ค.
1. ์๋ฒ์ ๋ฐ์ดํฐ ์ ํ๊ธฐ
๋ฌดํ์คํฌ๋กค์ ์ํด์๋ ์๋ฒ์ ํด๋ผ์ด์ธํธ ๋ ๋ค ๊ตฌํ์ด ํ์ํ๋ค.
ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ์ฃผ๊ณ ๋ฐ์์ง ๊ฒฐ์ ํด์ผํ๋ค.
ํน์ ํ์ด์ง ํ๋จ์ ๋๋ฌํ์ ๋, API๋ฅผ ํธ์ถํ๊ธฐ ์ํด์๋ ์๋์ ๊ฐ์ ๋ฐ์ดํฐ๊ฐ ํ์ํ๋ค.
- ๋ฐ์ดํฐ๋ฅผ ๋ช๊ฐ์ฉ ๋ณด๋ด์ค๊ฑด์ง
- ํ์ด์ง ๋ฒํธ
- ํ์ฌ ๋ณด๋ด๋ ๋ฐ์ดํฐ๊ฐ ๋ง์ง๋ง์ธ์ง ์๋์ง
- ํด๋ผ์ด์ธํธ ์ธก์์ ๋์ด์ ์์ฒญํ ์ง ๋ง์ง๋ฅผ ๊ฒฐ์ ํ๊ธฐ ์ํด์ ํ์ํ๋ค
- ํ์ฌ ๋ณด๋ด๋ ํ์ด์ง ๋ฒํธ
์ด๋ฅผ ๋ฐํ์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ๋ฉํ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๊ฒ ๋์๋ค.
page: ํ์ฌ ํ์ด์ง ๋ฒํธ
take: ๋ฐ์ดํฐ๋ฅผ ๋ช๊ฐ์ฉ ๋ฐ์ ๊ฒ์ธ์ง
pageCount: ์ด ํ์ด์ง์ ๊ฐ์
hasPreviousPage: ์ด์ ํ์ด์ง๊ฐ ์กด์ฌํ๋์ง
hasNextPage: ๋ค์ ํ์ด์ง๊ฐ ์กด์ฌํ๋์ง
2. React-Query์ useInfiniteQuery
๋ฌดํ์คํฌ๋กค ๊ตฌํ์ ์ํ ํ ์ ๋ฆฌ์กํธ ์ฟผ๋ฆฌ์์ ์ ๊ณตํด์ฃผ๊ณ ์๋ค.
https://tanstack.com/query/v4/docs/react/reference/useInfiniteQuery
์๋ ์ฝ๋๋ฅผ ์ดํด๋ณด์
const {
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
...result
} = useInfiniteQuery({
queryKey,
queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})
useQuery๋ ๋ค๋ฅธ ์ ์ getNextPageParam๊ณผ getPreviousPageParam์ด๋ค.
์ ์ฌํ๊ธฐ ๋๋ฌธ์ getNextPageParam๋ง ๋ณด๋๋ก ํ๊ฒ ๋ค.
getNextPageParam
getNextPageParam๊ฐ falsyํ ๊ฐ์ ๋ฐํํ๋ฉด, ์ถ๊ฐ๋ก fetchPage๋ฅผ ์คํํ์ง ์๋๋ค.
truthyํ ๊ฐ์ ๋ฐํํ ๊ฒฝ์ฐ, ํ๋์ ๊ฐ์ ๋ฐํํด์ผํ๊ณ ์ด ๊ฐ์ fetchPage์ pageParam์ผ๋ก ์ ๋ฌ๋๋ค.
์ธ์๋ก๋ lastPage์ allPages๋ฅผ ๋ฐ์ ์ ์๋ค.
- lastPage๋ fetchPage์์ ๋ฆฌํดํ ๊ฐ
- allPages๋ ์ง๊ธ๊น์ง ๋ฐ์ ์ ์ฒด ํ์ด์ง์ ๋ฐฐ์ด
๊ฐ๋จํ ์์
์์์ ๋งํ ๋ด์ฉ์ ๋ฐํ์ผ๋ก getNextPageParam์ ๊ฐ๋จํ๊ฒ ์์ฑํด๋ณผ ์ ์๋ค.
{
getNextPageParam: (lastPage: AxiosResponse<TestResponseType>) =>
lastPage.data.isLast ? undefined : lastPage.data.currentPage + 1,
}
์ด์ useInfiniteQuery์ ๋ฐํ ๊ฐ๋ค์ ์ด์ฉํด์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ฉด ๋๋ค.
- data: ๋ชจ๋ ํ์ด์ง์ ๋ฐ์ดํฐ๋ฅผ ํฌํจํ๋ค
- fetchNextPage: ๋ค์ ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ํจ์
- hasNextPage: ๋ค์ ํ์ด์ง๊ฐ ์๋์ง ํ์ธํด์ fetch๋ฅผ ํธ์ถํ ์ง ๊ฒฐ์ ํ๋ค
- isFetchingNextPage: fetch๋ฅผ ํ๊ณ ์๋ ์ค์ธ์ง ํ์ธํด์ ์ค๋ณต ํธ์ถ์ ๋ง๋๋ค
3. Intersection Observer
scrollEvent๋ก ๋ฌดํ์คํฌ๋กค์ ๊ตฌํํ๊ฒ ๋๋ค๋ฉด
scrollEvent๊ฐ ๋๊ธฐ์ ์ผ๋ก ์คํ๋๊ธฐ ๋๋ฌธ์ ๋ฉ์ธ ์ค๋ ๋์ ์ํฅ์ ์ค๋ค
ํน์ ์์ ์ ๊ด์ฐฐํ๊ธฐ ์ํด getBoundingClientRect() ๋ฅผ ์ฌ์ฉํด์ผํ๋๋ฐ ๋ฆฌํ๋ก์ฐ๊ฐ ๋ฐ์ํ๋ค.
Intersection Observer
Intersection Observer API๋ target ์์์ root ์์ ์ฌ์ด์ ๊ต์ฐจ ๋ฐ์์ ๋น๋๊ธฐ์ ์ผ๋ก ๊ด์ฐฐํ๋ค.
IntersectionObserverEntry ์์ฑ์ ์ฌ์ฉํ๋ฉด getBoundingClientRect()๋ฅผ ์ฌ์ฉํ์ง ์์๋ ๋๋ค. ์ฆ, ๋ฆฌํ๋ก์ฐ๋ฅผ ๋ฐฉ์งํ ์ ์๋ค.
Intersection Observer๋ฅผ ์ด์ฉํ ์ปค์คํ ํ ๊ตฌํ
import { useCallback, useEffect, useRef } from 'react';
/**
* Intersection Observer API๋ฅผ ์ฌ์ฉํ๋ ํ
์
๋๋ค.
* - target์ผ๋ก ์ค์ ํ ์์์ ๋ทฐํฌํธ์ ๊ต์ฐจํ๋์ง ๊ตฌ๋ณํฉ๋๋ค.
* - ๋ฌดํ์คํฌ๋กค์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
*
* @param onIntersect
* @param options root, rootMargin, threshold
*
* @returns target ์์์ ์ ๋ฌํ ref
*/
const useIntersection = (
onIntersect: (entry: IntersectionObserverEntry, observer: IntersectionObserver) => void,
options?: IntersectionObserverInit
) => {
const ref = useRef<HTMLDivElement>(null);
/**
* target ์์๊ฐ ๊ต์ฐจ๋์์ ๋ ์คํํ ํจ์
*
* @param entries IntersectionObserverEntry ๊ฐ์ฒด์ ๋ฆฌ์คํธ
* @param observer ์ฝ๋ฐฑํจ์๊ฐ ํธ์ถ๋๋ IntersectionObserver
*/
const callback = useCallback(
(entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
entries.forEach((entry) => {
if (entry.isIntersecting) onIntersect(entry, observer);
});
},
[onIntersect]
);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(callback, options);
observer.observe(ref.current);
return () => observer.disconnect();
}, [ref, options, callback]);
return ref;
};
export default useIntersection;
4. ์ค์ ์๋น์ค์ ์ ์ฉํ๊ธฐ
react-query์ ๊ตฌํํ useIntersection ํ ์ ์ด์ฉํด์ ์ค์ ์ปดํฌ๋ํธ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์๋ณด์.
const ExperienceCardList = () => {
const { data, fetchNextPage, hasNextPage, isFetching } = useGetInfiniteExperiences(
{ take: CARD_COUNT_PER_LOAD } // ๊ฐ์ ธ์ฌ ์นด๋ ๊ฐ์ ์ค์
);
// ๊ฐ page์์ ์๋ต ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ผ์ฐจ์ ๋ฐฐ์ด๋ก ์ ์ฅ
const experiences = useMemo(() => (data ? data.pages.flatMap(({ data }) => data) : []), [data]);
// viewport์ ref๋ฅผ ์ ๋ฌํ ์์๊ฐ ๊ต์ฐจํ ๊ฒฝ์ฐ ์ฝ๋ฐฑ์ด ํธ์ถ
const ref = useIntersection((entry, observer) => {
observer.unobserve(entry.target);
if (hasNextPage && !isFetching) fetchNextPage();
});
return (
<Container>
<ul>
{experiences?.map((experience) => (
<li key={id}>
<ExperienceCard {...experience} />
</li>
))}
{/* ์ ์ฒด ๋ฆฌ์คํธ์ ๋งจ ์๋๊ฐ viewport ๋ด๋ก ๋ค์ด์ฌ ๊ฒฝ์ฐ fetch */}
<div ref={ref}></div>
</ul>
</Container>
);
}
๊ฒฝํ์นด๋ ๋ฆฌ์คํธ์ ์ตํ๋จ๊ณผ ๊ต์ฐจํ๋ฉด ๋ค์ ์นด๋ ๋ฐ์ดํฐ๋ค์ ๋ฐ๋ก๋ฐ๋ก ๊ฐ์ ธ์จ๋ค.
'๐ ํ๋ก์ ํธ > ์ธ์ฌ์ดํธ์์' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[์ธ์ฌ์ดํธ์์] Next.js์ parallel route๋ฅผ ์ด์ฉํ์ฌ ํ์ด์ง ์ฑ๋ฅ ๊ฐ์ ํ๊ธฐ (1) | 2023.08.09 |
---|