next.js
,prisma
,axios
를 사용하는 프로젝트입니다.
Intersection Observer API의 사용법에 대한 포스트
기존에 무한 스크롤링을 구현할 때는 항상 스크롤 이벤트를 사용해서 구현했습니다.
하지만에 최근에 공부를 하면서 Intersection Observer API
의 존재에 대해 알게 되었고, 응용하면 무한 스크롤링을 구현할 수 있겠다고 생각해서 선택했습니다.
( 아직 어떤 원리로 동작하는지, 어떤 상황에 어떤 방법을 선택해야하는지에 대한 이해도는 없습니다. )
상태 관리 라이브러리로는 recoil
을 선택했기에 사용했습니다.
하지만 현재 구현한 방식에는 굳이 recoil
을 사용하지 않아도 구현할 수 있기에 아직 사용하는 이유나 사용의 편의성은 못느꼈습니다. 그래도 익숙해지기 위해서 사용했습니다.
처음 생각에는 상품 데이터를 15개씩 받아오는 비동기 selector
( 이하 productsState
)와 마지막 상품의 식별자를 갖는 selector
( 이하 productLastIdxState
)를 만들어서 사용하면 된다고 생각했습니다.
productsState
에서 상품들을 화면에 렌더링하고 IntersectionObserver
를 이용해서 마지막 상품을 감시해서 뷰포트 내부에 들어온다면 productLastIdxState
값을 이용해서 다음 상품들을 요청하도록 구조를 잡았습니다.
하지만 이렇게 했을 경우 발생하는 문제가 새로운 데이터를 받아오고 나서 기존 데이터를 유지할 수 없다는 문제가 생겼습니다.
상품들을 추가로 10개를 받아왔을 경우에 기존의 15개에 대한 데이터는 버리고 10개만 유지하게 되는 문제가 발생해서 다른 방법을 생각해봤습니다.
이후에 많은 삽질을 했지만 그 내용은 생략하겠습니다.
atom
( 이하 productsState
)을 이용해서 상품들의 데이터를 갖고 컴포넌트에서 상품들의 데이터를 요청하면 누적해서 productsState
에 추가해주는 방법으로 구현했습니다.
export const productsState = atom<Product[]>({
key: "productsState - " + v1(),![](https://velog.velcdn.com/images/1-blue/post/796ccff3-d79a-4731-a73c-891aef05db97/image.gif)
default: [],
});
/**
* 2022/08/22 - 최근 상품들의 마지막 상품의 식별자 - by 1-blue
*/
export const productLastIdxState = selector<number>({
key: "productLastIdxState",
get: ({ get }) => {
const products = get(productsState);
if (products.length === 0) return -1;
return products[products.length - 1].idx;
},
set: ({ set }) => {
set(productsState, []);
},
});
// 상품 요청 개수
const limit: LIMIT = 15;
// 2022/08/22 - 화면에 랜더링할 상품들 - by 1-blue
const [products, setProducts] = useRecoilState(productsState);
// 2022/08/22 - 가장 최근에 요청한 상품의 마지막 식별자 ( 해당 식별자를 기준으로 다음 상품들의 데이터를 요청 ) - by 1-blue
const [productLastIdx, setProductLastIdx] = useRecoilState(productLastIdxState);
// 2022/08/22 - 마지막 상품의 ref - by 1-blue
// 원래 ref는 주로 "useRef()"를 이용하지만, 현재 상황에서는 상품을 다시 요청할 때마다 ref가 변경되고 그에 맞게 리렌더링을 해야 하기 때문에 "useState()"를 사용했습니다.
const [lastProductRef, setLastProductRef] = useState<HTMLLIElement | null>(null);
// 2022/08/22 - 처음 한번 상품들의 데이터 요청 - by 1-blue
useEffect(() => {
(async () => {
// 이전에 상품 데이터들을 받아왔을 경우를 대비해서 미리 초기화
setProductLastIdx(-1);
// axios로 상품들을 요청하는 메서드
const { data: { products } } = await apiService.productService.apiGetProducts({ limit, lastIdx: -1 });
// 받아온 상품들을 atom에 넣음
setProducts(products);
})();
}, [setProducts, setProductLastIdx]);
// 2022/08/22 - observer로 인해 실행할 이벤트 함수 ( 제일 마지막 상품이 뷰포트에 들어오면 실행할 이벤트 함수 ) - by 1-blue
const onScroll = useCallback(
async ([{ isIntersecting }]: IntersectionObserverEntry[]) => {
if (!lastProductRef) return;
// 지정한 엘리먼트가 "threshold"만큼을 제외하고 뷰포트에 들어왔다면 실행
if (isIntersecting) {
const { data: { products } } = await apiService.productService.apiGetProducts({ limit, lastIdx: productLastIdx });
// 기존 데이터 + 새로운 데이터
setProducts((prev) => [...prev, ...products]);
}
},
[lastProductRef, productLastIdx, setProducts]
);
// 2022/08/22 - observer 등록 ( 제일 마지막 상품이 뷰포트에 들어오면 실행할 이벤트 함수를 등록 ) - by 1-blue
useEffect(() => {
if (!lastProductRef) return;
if (products.length % limit !== 0) return;
let observer = new IntersectionObserver(onScroll, {
threshold: 0.1,
rootMargin: "20px",
});
observer.observe(lastProductRef);
return () => observer?.disconnect();
}, [lastProductRef, onScroll, products]);
// 나머지 레이아웃과 주제에 맞지 않는 구현부분은 제외함