DEVLOG
Next.js ํ๋ก์ ํธ :: infinite scroll ๊ตฌํํ๊ธฐ ๋ณธ๋ฌธ
๐ ๋ชฉ์ฐจ
apiํํ ํ์ธํ๊ธฐ
ํด๋ผ์ด์ธํธ ๊ฐ๋ฐ์ ํ๊ธฐ์ apiํํ๋ฅผ ํ์ธํด์ผํ๋ค. api๊น์ง ์ง์ ๋ง๋ค๋ฉด ์ข์ง๋ง,, ์ผ๋จ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์ ์ง์คํ๋ ค๊ณ ์คํAPI๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ api๊ฐ ์ด๋ป๊ฒ ๋ค์ด์ค๋์ง ํ์ธํด๋ณด์!
pagination์ผ๋ก ๊ฐ๋ฐ์ ํ๋ infinite scroll๋ก ๊ฐ๋ฐ์ ํ๋ ๋ค์ด์ค๋ api์์ return๊ฐ์ผ๋ก ๋ฐ์์ผ ํ๋ ๊ฐ์ด ์๋ค.
- ํ์ฌ ํ์ด์ง์ ์ ์ฒด ํ์ด์ง์ ๊ฐ์
- ๋ค์ ํ์ด์ง๊ฐ ์กด์ฌํ๋์ง์ ๋ํ ์ฌ๋ถ
๋๊ฐ์ง ์ค ํ๊ฐ์ง๊ฐ ๊ฐ์ผ๋ก ๋ค์ด์์ผํ๋ค. ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ์ ๋ถ ๋ถ๋ฌ์์ ํด๋ผ์ด์ธํธ์์ ์ชผ๊ฐ๋๊ฒ ์๋๋ผ ํ์ํ ๋งํผ๋ง ๋ฐ์ดํฐ๋ฅผ ํธ์ถํด์ผํ๊ธฐ ๋๋ฌธ!
๋ด๊ฐ ์ฌ์ฉํ๋ TMDB api์์๋ ํ์ฌ ํ์ด์ง์ ์ ์ฒด ํ์ด์ง์๋ฅผ ์ ์ ์์๋ค.
๐ useInfiniteQuery
useInfiniteQuery๋ก ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ
react-query
๋ฅผ ์ฌ์ฉํ๊ฒ ๋ ์ฅ์ ์ค ํ๋์ธ ๋ฌดํ์คํฌ๋กค ๊ตฌํ์ด ๊ฐ๋ฅํ useInfiniteQuery
๋ฅผ ์ฌ์ฉํด์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ๋ด๋ณด์๋ค.useInfiniteQuery
๋ ํ๋ผ๋ฏธํฐ ๊ฐ๋ง ๋ณ๊ฒฝํ์ฌ ๋์ผํ useQuery
๋ฅผ ๋ฌดํ์ ํธ์ถํ ๋ ์ฌ์ฉํ๋ฉฐ ์ฌ์ฉ๋ฒ์ useQuery
์ ๋น์ทํ๋ค.
useInfiniteQuery
์์ ์ฌ์ฉ๋๋ pageParam
์ useInfiniteQuery
๊ฐ ํ์ฌ ์ด๋ค ํ์ด์ง์ ์๋์ง๋ฅผ ํ์ธํ ์ ์๋ ํ๋ผ๋ฏธํฐ ๊ฐ์ด๋ค.
๋ค์ api๋ฅผ ์์ฒญํ ๋ ์ฌ์ฉ๋ pageParam๊ฐ์ getNextPageParam
์์ ์ ํ ์ ์๋ค.
์ด์ ๋ํ ์์ธํ ๋ด์ฉ์ ์๋ ๋งํฌ์์ ํ์ธํ ์ ์๋ค!
useInfiniteQuery
const {
data,
fetchNextPage,
hasNextPage = true,
} = useInfiniteQuery(
["searchDatas"],
async ({ pageParam }) => {
const data = await searchMovie({
page: pageParam || 1, //์ด๊ธฐ์๋ pageParam์ด undefined์ด๋ฏ๋ก 1๋์
keyword: keyword,
});
return {
data: data.results,
total: data.total_pages,
page: data.page,
};
},
{
getNextPageParam: (lastPage) => {
if (lastPage.total >= lastPage.page) {
return lastPage.page + 1;
}
return undefined;
},
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
}
);
console.log(data);
์ ์ฝ๋๋ก ์ถ๋ ฅ๋๋ data๋ฅผ ์ฐ์ด๋ณธ ๊ฒฐ๊ณผ page์ pageParams์ ๋ฐ์ดํฐ๊ฐ ๊ฐ๊ฐ ๋ฐฐ์ด์์ ์ด์ค๋ฐฐ์ด๋ก ์์ด๋ ๊ฒ์ ํ์ธํ๋ค.
data?.pages.map((item: InfiniteDataProps) => item.data).flat()
๊ทธ๋์ ์ด์ค์ผ๋ก ๋ค์ด๊ฐ ๋ฐฐ์ด์ ํ๋๋ก ํ์ด๋ด๊ธฐ ์ํด flat()์ ์ด์ฉํ๋ค.
return (
<MainLayout>
<section className="search-section">
<p>
"{keyword}" ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ {total_results}๊ฐ ์์ต๋๋ค.
</p>
<CardList
data={data?.pages.map((item: any) => item.data).flat() || []}
></CardList>
</section>
<button onClick={() => fetchNextPage()}>๋ ๋ถ๋ฌ์ค๊ธฐ</button>
</MainLayout>
);
intersectionObserver ๋ฅผ ์ถ๊ฐํ๊ธฐ ์ ์์๋ก ๋ ๋ถ๋ฌ์ค๊ธฐ ๋ฒํผ์ ๋ง๋ค์ด ํ
์คํธ ํด๋ณด์๋ค!
ํ
์คํธ๊ฒฐ๊ณผ
๋ ๋ถ๋ฌ์ค๊ธฐ ๋ฒํผ ํด๋ฆญ์ ์๋๋ก ๋ฐ์ดํฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ๋ ๋ถ๋ฌ์ก๋ค!
useInfiniteQuery Hook ๊ฐ๋ฐ
๊น๋ํ ์ฝ๋์ ์ฝ๋์ฌ์ฌ์ฉ์ ์ํด ํด๋น๋ถ๋ถ์ hook์ผ๋ก ์ฒ๋ฆฌํด๋ณด์๋ค.
hook ์ฝ๋
export const useInfiniteQueryList = (
//hook์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๊ธฐ ์ํด ํจ์๋ฅผ parameter๋ก ๋ฐ์์ ์ฌ์ฉ
callback: (keyword: string, page: number) => Promise<movieListResult>,
keyword: string
) => {
const {
data: pages,
fetchNextPage,
hasNextPage = true,
} = useInfiniteQuery(
["searchDatas"],
async ({ pageParam }) => {
const res = await callback(keyword, pageParam || 1);
return {
data: res.results,
total_pages: res.total_pages,
total_result: res.total_results,
page: res.page,
};
},
{
getNextPageParam: (lastPage) => {
if (lastPage.total_pages > lastPage.page) {
return lastPage.page + 1;
}
return undefined;
},
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
}
);
return {
pages,
fetchNextPage,
hasNextPage,
};
};
์ฌ์ฉ ์ฝ๋
const { pages, fetchNextPage } = useInfiniteQueryList({callbackํจ์}, {๊ฒ์ ํค์๋});
const { pages, fetchNextPage } = useInfiniteQueryList(searchMovie, keyword);
๐ intersectionObserver
์ด์ ๋๋ถ๋ฌ์ค๊ธฐ ๋ฒํผ์ด ์๋๋ผ ํ์ด์ง์ ํ๋จ์ ๋๋ฌํ์๋๋ ์ฒดํฌํ๋ ์ด๋ฒคํธ๋ฅผ ์ธ์ํด์ ํ๋จ์ ๋๋ฌํ๋ฉด ์์์ ๋ง๋ hook์ ์คํํ๋ ๋ถ๋ถ์ ๊ฐ๋ฐํ ์์ ์ด๋ค.
ํ๋จ์ ๋๋ฌํ์์ ์ ์ ์๋ ์ด๋ฒคํธ๋ ๋ค์ ์ธ๊ฐ์ง๊ฐ ์๋ค.
- Scroll Event
- Throttle
- requestAnimationFrame
- Intersection Observer API
๊ฐ๊ฐ์ ๋ํ ๊ตฌํ๊ณผ ์ฅ๋จ์ ์ ์๋ ๋งํฌ ์นด์นด์ค ๊ธฐ์ ๋ธ๋ก๊ทธ์์ ์์ธํ๊ฒ ํ์ธํ ์ ์์๋ค! ๊ฐ์นด์ค!
๋ฌดํ์คํฌ๋กค๊ตฌํํ๊ธฐ
export function useIntersectionObserver({
target, // ๊ฐ์งํ ๋์, ref๋ฅผ ๋๊ธธ ์์
onIntersect, // ๊ฐ์ง ์ ์คํํ callback ํจ์
root = null, // ๊ต์ฐจํ ๋ถ๋ชจ ์์ (์๋ฌด๊ฒ๋ ๋๊ธฐ์ง ์์ผ๋ฉด document๊ฐ ๊ธฐ๋ณธ)
rootMargin = "0px", // root์ target์ด ๊ฐ์งํ๋ ์ฌ๋ฐฑ์ ๊ฑฐ๋ฆฌ
threshold = 1.0, // ์๊ณ์ . 1.0์ด๋ฉด root๋ด์์ target์ด 100% ๋ณด์ฌ์ง ๋ callback์ด ์คํ๋๋ค.
}: any) {
useEffect(() => {
let observer: any;
if (target && target.current) {
observer = new IntersectionObserver(onIntersect, {
root,
rootMargin,
threshold,
});
observer.observe(target.current);
}
return () => observer && observer.disconnect();
}, [target, rootMargin, threshold, onIntersect, root]);
}
๊ธฐ์กด์ ๋ ๋ถ๋ฌ์ค๊ธฐ ๋ฒํผ์ด ์๋ ๊ณณ์ ์๋์ ๋๋ฌํ ๊ฒ์ ์ฒดํฌํ๊ธฐ ์ํ ref๋ฅผ ๋ง๋ค์ด์ค๋ค. ์ด ref๋ useIntersectionObserver hook์ target์ผ๋ก ๋๊ฒจ์ค ๊ฒ ์ด๋ค.
<CardList data={data?.pages.map((item: any) => item.data).flat() || []}></CardList>
<div ref={bottom} />
์ธํผ๋ํฐ ์คํฌ๋กค ๊ตฌํ ๋!
์ด ์ญ์ hook์ ํตํด ๋ฌถ์ด๋์ ์์ผ๋ก ํ์ํ ๊ณณ์๋ ์๋ ์ฝ๋๋ง ์ฌ์ฉํ๋ฉด ๋๋ค!
์ฌ์ฉ ์ฝ๋
const { data, fetchNextPage } = useInfiniteQueryList({callbackํจ์}, {ํค์๋});
const bottom = useRef(null);
const onIntersect = ([entry]: any) => entry.isIntersecting && fetchNextPage();
useIntersectionObserver({
target: bottom,
onIntersect,
});
...
return(
...
<div ref={bottom} />
...
);
๐ Next?
์ง๊ธ๊น์ง ๋ง๋ ํ๋ก์ ํธ๋ฅผ vercel์ ์ด์ฉํด ๋ฐฐํฌํด๋ณด๋ ค๊ณ ํ๋ค!
์ฐธ๊ณ ์ฌ์ดํธ
https://velog.io/@hdpark/React-Query%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-Next.js-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4
https://been.tistory.com/55
'frontend > next.js' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Next.js ํ๋ก์ ํธ :: ๋ฐฐํฌ(gh-pages, netlify, vercel) (1) | 2023.07.12 |
---|---|
Next.js ํ๋ก์ ํธ :: carousel ๊ตฌํํ๊ธฐ (0) | 2023.06.22 |
Next.js ํ๋ก์ ํธ :: getServersideProps (0) | 2023.06.22 |
Next.js ํ๋ก์ ํธ :: Open API (TMDB), React-Query (0) | 2023.06.22 |
Next.js ํ๋ก์ ํธ :: ์ธํ (0) | 2023.06.22 |