사용자가 본 특정 항목으로 찾아가는게 어렵다.
방식에따라 페이지 로드 시간이 오히려 오래 걸릴 수 있다.
-> 모든 게시물을 클라이언트에 저장해서 콘텐츠를 로드하는 방식
-> 다음 게시물을 api호출해서 받아오는 방식
게시물의 실제 양을 가늠하기 어렵다.(이건 단점으로 애매,,?)
완성 화면
Intersection Observer API
VSScroll Event
말 그대로 브라우저에서 사용자가 스크롤을 할때마다, 지정해준 함수가 동작하게 해주는 이벤트다. 사용자가 스크롤을 내릴때마다 동작하기 때문에 불필요한 동작이 많이 일어나서 스크롤 이벤트는 사용하지 않았다!
현재 보고있는 viewport와 target으로 설정한 요소의 교차점을 찾아내주며, 해당 요소가 viewport의 포함 되는지 아닌지 쉽게 말하자면 현재 사용자의 화면에 보이는지 안보이는지 구별해 주는 기능이다.
Scroll Evnet
와 달리 비동기로 동작하여 렌더링 성능이나 호출 같은 문제가 없이 사용가능하다. 그래서 얘를 채택하였다!
PokemonList 컴포넌트
export default function PokemonList({ isSearch }: { isSearch: Boolean }) {
const {
data: pokemonList,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useGetPokemonListQuery();
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
export const useGetPokemonListQuery = () =>
useInfiniteQuery(
["pokemonList"],
async ({ pageParam }) => await pokemonApis.getPokemonList({ pageParam }),
{
getNextPageParam: ({ next }): getPokemonListI => {
return next;
},
},
);
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
const searchPokemon = useAtomValue(searchedPokemonAtom);
const target = useCallback(
(node: HTMLElement | null) => {
if (!node) {
return;
} else {
const observer = new IntersectionObserver(
([entry]) => {
const { isIntersecting, target } = entry;
if (isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
observer.unobserve(target);
}
},
{
root: null,
threshold: 0,
},
);
observer.observe(node);
}
},
[hasNextPage, fetchNextPage, isFetchingNextPage],
);
return (
<ul className="grid grid-cols-3 gap-6 max-w-[1200px] m-auto">
{isSearch
? pokemonList?.pages?.map(
({ results }, pageIndex: number, { length: pagesLength }) =>
results.map(
(
{ name, url }: pokemonNameUrlI,
cardIndex: number,
{ length: cardLength }: { length: number },
) => {
const isTarget =
pageIndex + 1 === pagesLength &&
cardIndex + 1 === cardLength;
return (
<PokemonCard
key={name}
name={name}
url={url}
isTarget={isTarget}
target={target}
/>
);
},
),
)
: searchPokemon?.map(({ name, url }: pokemonNameUrlI) => {
return <PokemonCard key={name} name={name} url={url} />;
})}
</ul>
);
}
pokemonCard 컴포넌트
export default memo(function PokemonCard({
name,
url,
isTarget,
target,
}: pokemonNameUrlI) {
const { data: imgUrl } = useGetPokemonInfoQuery({ url, key: "imgUrl" });
const checkKo = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/;
const pokemonName = checkKo.test(name) ? name : pokemonKoName[name];
const targetCard = (node: HTMLElement | null) => {
target && target(node);
};
return (
<li ref={isTarget ? targetCard : null}>
<div className="relative h-[20em] border">
<Image
src={imgUrl ? imgUrl : "/image/monsterBall.png"}
className="p-4"
alt={pokemonName}
fill
priority
sizes="auto"
/>
</div>
<p>{pokemonName}</p>
</li>
);
});
useInfiniteQuery의 아래의 기능을 사용했다
const { fetchNextPage, // 다음 페이지 가져오는 함수 hasNextPage, // 다음 페이지는 있는지 구별은 getNextPageParam의 리턴값으로 한다 undefine이면 false isFetchingNextPage, // false면 다음 페이지 가져오는중, true면 다 가져옴 } = useInfiniteQuery({ queryKey, // 쿼리키 queryFn: ({ pageParam = 1 }) => fetchPage(pageParam), //pageParam값으로 데이터를 가져온다 초기값은 할당해 주지 않으면 undefine가 나옴 ...options, getNextPageParam: (lastPage, allPages) => lastPage.nextCursor, //return값으로 지정해 준것이 pageParam값으로 들어감 그리고 다음 페이지가 없다면 undefine값을 리턴 해주면 된다. }) 출처 : react-query 공식문서
{
"pages": [
[첫번째 불러온 데이터],
[두번째 불러온 데이터]
//이렇게 불러온 데이터들을 array에 담아서 준다.
],
"pageParams": [
//현재 데이터를 불러오기위해 사용한 api url
]
}
이렇게 받아온 데이터를 아래와 같이 map을 동작시켜주었다.
map은 첫번째 인자로 배열안에 있는 각각 값, 두번째 인자로는 index값, 세번째 값으로는 배열 자체를 반환해준다.
pokemonList?.pages?.map(
({ results }, pageIndex: number, { length: pagesLength }) =>
results.map(
(
{ name, url }: pokemonNameUrlI,
cardIndex: number,
{ length: cardLength }: { length: number },
) => {
const isTarget =
pageIndex + 1 === pagesLength &&
cardIndex + 1 === cardLength;
// 여기서 현재 맵이 돌고있는 페이지와 전체 페이지의 수가 같을 때, 가장 마지막에 있는 card가 일치할 때만
// true값을 반환해 주어 가장 맨 마지막에 있는 card를 target로 삼을 수 있게 해두었다.
return (
<PokemonCard
key={name}
name={name}
url={url}
isTarget={isTarget}
target={target}
/>
);
},
),
)
const target = useCallback(
(node: HTMLElement) => {
const observer = new IntersectionObserver(
([entry]) => {
const { isIntersecting, target } = entry;
//isIntersecting은 현재 요소가 있는지 없는지 true, false 값으로 나온다.
//target는 말 그대로 타겟에 대한 핸들링
if (isIntersecting) {
//현재 요소가 있고, 다음 페이지가와 페칭준비가 끝났다면 아래의 코드를 실행한다
fetchNextPage();
//다음 페이지를 받아오고
observer.unobserve(target);
//원래 있던 타겟을 더이상 관찰하지 않는다.
}
},
{
root: null,
//viewport의 기준값 null이면 브라우저를 대상으로함
threshold: 0,
//타깃의 가시성이 얼마나 확보되었을때, 0으로 해두어 타겟이 전부 보일때 함수가 실행됩니다.
},
);
observer.observe(node);
//다시 받아온 데이터중 가장 마지막 요소를 타겟으로 지정
},
[fetchNextPage],
);
useCallback를 사용한 이유는, 자식요소의 불필요한 리렌더링을 막기위해 사용하였다.
export default memo(function PokemonCard({
name,
url,
isTarget,
target,
}: pokemonNameUrlI) {
const { data: imgUrl } = useGetPokemonInfoQuery({ url, key: "imgUrl" });
const checkKo = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/;
const pokemonName = checkKo.test(name) ? name : pokemonKoName[name];
const targetCard = (node: HTMLElement | null) => {
!!node && target && target(node);
//여기서 타겟을 지정해 준다!
};
return (
<li ref={isTarget ? targetCard : null}>
<div className="relative h-[20em] border">
<Image
src={imgUrl ? imgUrl : "/image/monsterBall.png"}
className="p-4"
alt={pokemonName}
fill
priority
sizes="auto"
/>
</div>
<p>{pokemonName}</p>
</li>
);
});
전에 프로젝트 할때는, 기능 구현에 급급해서 제대로된 원리는 파악하지 않고 그대로 사용했었는데 이번에 다시 보니 정말 아무것도 모르고 복붙으로만 코드를 짯었던 것 같다! 이제는 전보다는 더 나은 방식으로 사용할 줄 알게 된거 같아서 기분이 뭔가 좋다,,!