타겟 요소와 상위 요소 또는 최상위 document의 뷰 포트 사이의 관찰 범위의 변화를 비동기적으로 관찰하는 방법입니다. - MDN Web Docs
과거 인터섹션에 따른 이벤트 처리를 위해 스크롤 이벤트나 Element.getBoundingClientRect()와 같은 메서드를 이용해야 했음 → 이벤트들이 매우 복잡하게 얽히기 때문에 유지/보수 및 성능 문제에 직면하게됨
타겟이 되는 요소가 뷰포트 혹은 다른 요소에 특정 부분만큼 들어가거나 나갈 때 콜백으로 걸어둔 로직을 처리하도록 할 수 있게된다.
useIntersection.tsx
import { useRef, useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { searchResultState } from 'src/recoils/SearchRecoil';
import { fetchGetData } from 'src/utils/fetchData';
export const useIntersection = () => {
const [infiniteScrollTarget, setInfiniteScrollTarget] = useState<HTMLElement | null | undefined>(null);
const observerRef = useRef<IntersectionObserver>();
const [{ keyword, totalResults, page, search }, setSearchResult] = useRecoilState(searchResultState);
const onIntersect = async ([entry]: IntersectionObserverEntry[], observer: IntersectionObserver) => {
if (entry.isIntersecting && page * 10 < totalResults) {
observer.unobserve(entry.target);
const data = await fetchGetData(keyword, page + 1);
setSearchResult((prev) => ({
keyword: prev.keyword,
search: [...prev.search, ...data.Search],
response: Boolean(data.Response),
totalResults: Number(data.totalResults),
page: prev.page + 1,
}));
}
};
useEffect(() => {
if (infiniteScrollTarget) {
observerRef.current = new IntersectionObserver(onIntersect, {
threshold: 0.6,
});
observerRef.current.observe(infiniteScrollTarget);
}
return () => observerRef.current && observerRef.current.disconnect();
}, [search, infiniteScrollTarget]);
return { setInfiniteScrollTarget };
};
Page.tsx
import React from 'react';
import { useRecoilValue } from 'recoil';
import styled from 'styled-components';
import { searchResultState } from 'src/recoils/SearchRecoil';
import SearchBar from 'src/components/SearchBar';
import MovieElement from 'src/components/MovieElement';
import { useIntersection } from 'src/hooks/useIntersection';
function Search(): JSX.Element {
const { search } = useRecoilValue(searchResultState);
const { setInfiniteScrollTarget } = useIntersection();
return (
<>
<SearchBar />
<AppMain>
{!search.length && <NoResult>검색결과가 없습니다.</NoResult>}
{search?.map((info) => (
<MovieElement info={info} key={info.imdbID} />
))}
<ScrollTarget ref={setInfiniteScrollTarget} />
</AppMain>
</>
);
}
export default Search;
const AppMain = styled.main`
padding: 0 7vw;
margin-bottom: 8vh;
`;
const NoResult = styled.p`
margin-top: 30px;
font-size: 30px;
text-align: center;
`;
const ScrollTarget = styled.div`
height: 150px;
`;