Recoil과 IntersectionObserver를 이용한 무한 스크롤링

박상은·2022년 8월 22일
0

🧺 bleshop 🧺

목록 보기
4/10

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]);

// 나머지 레이아웃과 주제에 맞지 않는 구현부분은 제외함

0개의 댓글