Intersection Observer란 무엇일까? 참고
Intersection Observer란, JS의 브라우저 뷰포트와 설정한 요소의 교차점을 관찰하며, 요소가 뷰포트에 포함되는지 안되는지(즉, 사용자 화면에서 보이는 지 안 보이는 지) 구별하는 기능을 말한다. 비동기적으로 실행되기에, scroll같은 이벤트 기반의 요소 관찰에서 발생하는 렌더링 성능이나 이벤트 연속 호출 같은 문제 없이 사용할 수 있다.
1. Intersection Observer 생성
가장 처음으로 Intersection Observer를 통해 관찰하려는 대상을 연결합니다. 특정 DOM에 접근할 수 있도록 useRef를 사용하여 target인 div 태그를 선택합니다.
import { useRef } from 'react';
export default function View() {
const target = useRef<HTMLDivElement | null>(null);
return(
...
<div ref={target} className="flex h-[100px] w-full justify-center">
)
}
이후, 첫 렌더링시에, Intersection Observer 인스턴스를 생성합니다.
const getMoreArtWork = async () => {
setIsLoaded(true);
if (hasNextPage) {
await fetchNextPage();
}
setIsLoaded(false);
};
const onIntersect = useCallback(
async (
[entry]: IntersectionObserverEntry[],
observer: IntersectionObserver,
) => {
if (entry.isIntersecting && !isLoaded) {
observer.unobserve(entry.target);
await getMoreArtWork();
observer.observe(entry.target);
}
},
[fetchNextPage, hasNextPage],
);
useEffect(() => {
let observer;
if (target) {
observer = new IntersectionObserver(onIntersect, {
threshold: 0.1,
});
observer.observe(target.current);
}
return () => observer && observer.disconnect();
}, [onIntersect]);
- Intersection Observer의 첫 번째 인자로는 Observer에 타깃이 관찰되었을 때 실행되는 콜백함수가 들어가고, 두 번째 인자로는 해당 Observer의 옵션을 넣어줍니다.
- 첫 번째 인자로 들어가는 콜백함수는 onIntersect 함수이고, Observer의 옵션으로는 threshold를 0.1로 주었습니다.
- Intersection Observer의 옵션으로는 뷰포트 대신 사용할 객체를 지정할 수 있는 root, root의 범위를 설정할 수 있는 rootMargin, 옵저버가 실행되기 위해 눈에 보이는 타겟의 비율인 threshold가 존재합니다.
- 해당 프로젝트에서는 타겟이 0.1만큼만 보여도 콜백함수가 실행될 수 있게끔, 옵저버의 threshold 옵션을 0.1로 설정했습니다.
- 콜백함수는 entries, observer 이라는 두개의 인수를 가지는데, entries는 위에서 생성한 Intersection Observer의 배열 인스턴스입니다.
- 연결한 target 객체는 div태그 하나이기 때문에 구조분해 할당으로 첫번째 entry만 받아옵니다.
- 해당 entry는 다양한 속성을 가지고 있는데, 가시성을 확인하는 isIntersecting이라는 boolean 값을 통해 true라면 임시로 observer를 해제합니다.
- 해당 타겟이 노출되었을 때(isIntersecting === true) observer를 해제해야 observer가 중첩되지 않습니다.
- 따라서, 이후에 getMoreArtWork()를 통해 데이터를 받아온 후, 이후에 target을 관찰했을 때, 콜백함수가 실행될 수 있도록, 다시 observer를 연결합니다.
- getMoreArtWork() 함수는 hasNextPage(다음 페이지가 존재한다면)가 true인 경우 다음페이지의 데이터를 호출합니다.
2. React-query InfiniteQuery 생성
import homeApi from '@apis/home/homeApi';
import { useInfiniteQuery } from 'react-query';
interface InfiniteQueryProps {
artworks: KeywordArtwork[];
current_page: number;
nextPage: boolean;
}
export const useGetInfiniteArtWork = () => {
const getArtWork = async ({ pageParam = 1 }): Promise<InfiniteQueryProps> => {
const data = await homeApi.getCustomizedArtWork(pageParam, 20);
const { artworks, nextPage } = data;
return {
artworks,
current_page: pageParam,
nextPage,
};
};
const { data, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery(
['useInfiniteArtWork'],
getArtWork,
{
getNextPageParam: (lastPage: InfiniteQueryProps) => {
if (lastPage.nextPage) return lastPage.current_page + 1;
return undefined;
},
retry: false,
refetchOnWindowFocus: false,
},
);
return { data, fetchNextPage, hasNextPage, refetch };
};
- 위와 같이 useGetInfiniteArtWork라는 무한스크롤 훅을 생성했습니다.
- getArtWork는 useInfiniteQuery에 전달될 쿼리 함수입니다. useInfiniteQuery가 현재 어떤 페이지에 머물고 있는지 확인할 수 있는 파라미터 값으로 초기 값으로 1페이지를 설정하여 넘겨줍니다.
- API로 부터 받아온 데이터로는 작품 데이터인 artworks와 다음 페이지가 있는지 없는지를 나타내는 nextPage가 있고, 해당 값을 리턴합니다.
- useInfiniteQuery의 getNextPageParam은 바로 직전에 조회한 쿼리 함수의 리턴 값을 파라미터로 갖습니다. 또한 getNextPageParam의 리턴 값은 다음 페이지가 호출될 때의 pageParam 값으로 사용됩니다.
- 따라서, 직전에 조회한 쿼리함수의 리턴 값인 nextPage가 true라면 pageParam에 1을 더하고, nextPage가 false라면 undefined를 리턴하여 hasNextPage 값은 false를 갖습니다.
3. 데이터 가공
import { useGetInfiniteArtWork } from '@hooks/queries/useGetInfiniteArtWork';
export default function View() {
const { data, hasNextPage, fetchNextPage } = useGetInfiniteArtWork();
const artworkLists = useMemo(
() => data?.pages.flatMap((page) => page.artworks),
[data?.pages],
);
...
return (
<div>
{artworkLists?.map((art) => (
<ExhibitionItem
key={art.id}
image={art.image}
education={art.education}
title={art.title}
id={art.id}
pick={art.pick}
/>
))}
</div>
)
}
- 이제 useGetInfiniteArtWork 훅을 사용했을 때 받아온 데이터를 보면, pageParams와 pages가 배열형태로 받아와집니다.
- 데이터 자체를 꺼내서 쓰기 위해서는 pages 배열내의 artworks의 데이터를 얻어와야하기 때문에 flatMap 함수를 사용하여 artworks를 추출합니다.
- 렌더링 시 해당 데이터또한 재가공하는 일이 없도록 data에서 넘어오는 pages의 값이 바뀌지 않는다면 가공된 데이터 값이 변경되지 않도록 메모이제이션을 통해 최적화를 해줍니다.
'개발 일지 > 개발 일지' 카테고리의 다른 글
[개발 일지] React성능 최적화 (2) : React Query의 캐싱 기능 (1) | 2023.05.05 |
---|---|
[개발 일지] React성능 최적화 (1) : React.memo로 사용자 경험 개선하기 (0) | 2023.05.03 |
[개발 일지] Next.js에서 접근성 향상하기 (접근성 점수 84점 -> 97점 향상시킨 썰) (0) | 2023.04.27 |
[개발 일지] React Query - Custom Hook 만들기 (+Typescript) (0) | 2023.04.13 |
[개발 일지] JWT + 소셜 로그인을 정복해보자 (React + Spring Boot, Kakao Login, Naver Login) (0) | 2023.04.11 |
[개발 일지] Suspense를 이용하여 모든 api에 Loading 화면을 설정하자 (NextJS, Suspense) (0) | 2023.02.18 |
[개발 일지] Axios instance 설정 끝장내기 (Axios, instance, interceptors, JWT) (0) | 2023.02.11 |
[개발 일지] 채팅 기능을 구현해보자 (Websocket, Stomp, SockJS) (0) | 2023.02.10 |