today,weekly I learn

IntersectionObserver api - 리액트, 무한스크롤

rhdaud2 2024. 10. 21. 15:19

 

원티드 프리온보딩 FE 10월 챌린지 사전 미션으로 진행

(https://www.wanted.co.kr/events/pre_challenge_fe_26)

https://github.com/jonghoon7431/wanted-pre-onboarding-fe-26

 

GitHub - jonghoon7431/wanted-pre-onboarding-fe-26: 원티드 프리온보딩 FE 챌린지 10월 (2024) 사전 미션(무한스

원티드 프리온보딩 FE 챌린지 10월 (2024) 사전 미션(무한스크롤). Contribute to jonghoon7431/wanted-pre-onboarding-fe-26 development by creating an account on GitHub.

github.com

 

 

 

 

제공된 mockdata

import { MockData } from "../types/mockdataType";

export const MOCK_DATA: MockData[] = [
  {
    productId: "66e1c1df3594bb65169e0f9b",
    productName: "Elegant Granite Fish",
    price: 792.0,
    boughtDate: "Sat Jun 01 1985 20:00:06 GMT+0900 (한국 표준시)",
  },
  {
    productId: "66e1c1df3594bb65169e0f9c",
    productName: "Intelligent Steel Towels",
    price: 287.0,
    boughtDate: "Sun Jul 02 2017 01:41:40 GMT+0900 (한국 표준시)",
  },
	//...
    
 ]

 

제공된 호출용 promise 함수

  const PER_PAGE = 10;

// 페이지는 1부터 시작함
  const getMockData = (pageNum: number) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        const datas: MockData[] = MOCK_DATA.slice(
          PER_PAGE * pageNum,
          PER_PAGE * (pageNum + 1)
        );
        const isEnd = PER_PAGE * (pageNum + 1) >= MOCK_DATA.length;

        resolve({ datas, isEnd });
      }, 1500);
    });
  };

 

 

 

1. useIntersectionObserver 훅 생성

 

import { useCallback, useRef } from "react";

export default function useIntersectionObserver(callback: () => void) {
  const observer = useRef<IntersectionObserver | null>(null);

  const observe = useCallback(
    (el: Element) => {
      if (observer.current) observer.current.disconnect();
      observer.current = new IntersectionObserver( //IntersectionObserver 생성
        (entries) => {
          console.log(entries);
          if (entries[0].isIntersecting) {
            //대상 요소가 관찰자 루트와 교차할 경우
            callback(); //callback fn 실행
          }
        },
        { threshold: 0.1 }
      );
      if (el) observer.current.observe(el);
    },
    [callback]
  );

  const unobserve = useCallback((el: any) => {
    if (observer.current) {
      observer.current.unobserve(el);
    }
  }, []);

  return [observe, unobserve] as const;
}

 

2. ProductList.tsx 파일 작성

 

import { useCallback, useEffect, useRef, useState } from "react";
import useIntersectionObserver from "../hooks/useIntersectionObserver";
import { MOCK_DATA } from "../mockdata/mockdata";
import { MockData } from "../types/mockdataType";

const ProductList = () => {

  //2-1. data 담을 state, 로딩 여부 및 데이터가 더 존재하는지 담을 useState생성
  const [data, setData] = useState<MockData[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  const PER_PAGE = 10;
  const page = useRef(0);

  // 페이지는 1부터 시작함
  const getMockData = (pageNum: number) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        const datas: MockData[] = MOCK_DATA.slice(
          PER_PAGE * pageNum,
          PER_PAGE * (pageNum + 1)
        );
        const isEnd = PER_PAGE * (pageNum + 1) >= MOCK_DATA.length;

        resolve({ datas, isEnd });
      }, 1500);
    });
  };
  
  //ref 선언
  const loadMoreRef = useRef(null);


  //useIntersectionObserver에 넘겨줄 콜백함수
  const loadMore = useCallback(() => {
    if (isLoading || !hasMore) return;

    setIsLoading(true);
    getMockData(page.current).then((result: any) => {
      setIsLoading(false);
      setData((prev) => [...prev, ...result.datas]);
      page.current += 1;
      setHasMore(!result.isEnd);
    });
  }, [page, isLoading, hasMore]);

  const [observe, unobserve] = useIntersectionObserver(loadMore);

  useEffect(() => {
    const currentRef = loadMoreRef.current;
    if (currentRef) {
      observe(currentRef);
    }
    return () => {
      if (currentRef) {
        unobserve(currentRef);
      }
    };
  }, [observe, unobserve]);

  //총액 계산
  const totalPrice = data.reduce((acc, data) => acc + data.price, 0);

  return (
    <main>
      <h1 className="flex justify-center text-[32px] my-6">
        프리온보딩 FE 챌린지 10월 (2024) 리액트 오픈소스 펼쳐보기 사전 미션
      </h1>
      <ul className="flex flex-col gap-4 items-center">
        <p>가격 총액: {totalPrice}</p>
        {data.map((data) => (
          <li key={data.productId} className="border-2 p-2 text-[20px]">
            <p>id: {data.productId}</p>
            <p>name : {data.productName}</p>
            <p>price : {data.price}</p>
            <p>bought date : {data.boughtDate}</p>
          </li>
        ))}
      </ul>
      {isLoading && (
        <div className="flex justify-center text-[24px] m-4">Loading</div>
      )}
      {!isLoading && hasMore && (
        <div className="flex justify-center text-[24px] m-4" ref={loadMoreRef}>
          Loading
        </div>
      )}
      {!hasMore && (
        <div className="flex justify-center text-[24px] m-4">
          No more products to load
        </div>
      )}
    </main>
  );
};

export default ProductList;

 

'today,weekly I learn' 카테고리의 다른 글

중요 요청 체이닝  (0) 2024.11.26
zustand 타입스크립트에서 사용하기(기본사용법)  (0) 2024.11.19
최종 프로젝트 간단한 리팩토링  (1) 2024.10.16
리뷰 기능 수정  (2) 2024.09.30
React fragment tag <>  (2) 2024.09.25