상세 컨텐츠

본문 제목

[React + TypeScript] 무한 스크롤 구현하기

기술

by 기먕로기 2023. 1. 4. 23:48

본문

무한 스크롤이란?

사용자가 페이지의 하단에 도달했을 때, api를 호출하여 끊임없이 컨텐츠를 로드하는 사용자 경험

 


구현 방식

1. target 엘리면트를 만든다.

2. IntersectionObserver를 이용하여 target 엘리먼트가 화면에 보여졌는지를 추적한다.
3. target 엘리먼트가 화면에 보여졌다면 api 호출을 하여 기존의 데이터에 받아온 데이터를 덧붙인다.


 

구현 내용

0. 가정

- 백에서 데이터는 10개씩 받아온다. 

1. target 엘리먼트를 만든다

옵저버 인스턴스의 observe 메서드에 target 엘리먼트를 넘겨주기 위해 ref를 만든다.

 

만약 모든 데이터를 가져왔을 경우를 대비하여 조건부 렌더링을 적용시킨다.

(page의 끝을 가져오지 않았다면 target 엘리먼트를 계속 유지하고, page의 끝까지 가져왔다면 target 엘리먼트를 렌더링하지 않는다.)

const loadMoreRef = useRef<HTMLDivElement>(null);
const [isPageEnd, setIsPageEnd] = useState<boolean>(false);

target 엘리면트에 ref 값을 설정해준다.

{!isPageEnd && <div ref={loadMoreRef} />}

2. IntersectionObserver를 사용하여 target 엘리먼트를 추척한다.

const getList = useCallback(async () => {
    try {
      const { data }: GetListType = await application.getList(
        pageIndexRef.current,
      );
      setList(list => [...list, ...data]);
      pageIndexRef.current++;
      setIsPageEnd(data.length < 10 ? true : false);
    } catch (error: any) {
      // catch..
  }, []);

위는 api를 호출하여 데이터를 가져와 기존의 데이터에 가져온 데이터를 덧붙이는 함수이다.

 

데이터는 10개씩 넘어오기 때문에 10개 미만의 데이터를 받는다면 모든 데이터를 받은 걸로 취급하고 isPageEnd를 true로 변환한다.

const handleObserver = useCallback(
    async (
      [entry]: IntersectionObserverEntry[],
      observer: IntersectionObserver,
    ) => {
      if (entry.isIntersecting) {
        observer.unobserve(entry.target);
        await getList();
        observer.observe(entry.target);
      }
    },
    [getList],
  );

  useEffect(() => {
    if (!loadMoreRef.current) return;

    const option = {
      root: null,
      rootMargin: '0px',
      threshold: 0,
    };

    const observer = new IntersectionObserver(handleObserver, option);

    loadMoreRef.current && observer.observe(loadMoreRef.current);

    return () => observer && observer.disconnect();
  }, [handleObserver, isPageEnd]);

useEffect 내부에서 observer를 생성한다.

 

IntersectionObserver의 생성자에 넘기는 콜백 함수는 관찰할 대상이 등록되거나 가시성에 변화가 생기면 동작할 함수이다.

 

useCallback으로 리렌더링 시 마다 새로 만들어 지는 것을 방지하기 위해 useCallback으로 함수를 감싸고 함수내에서 사용하는 getList 함수 또한 변경 가능성이 있으므로 최신 값을 참조하기 위해 deps 배열에 넣는다.

(useCallback을 사용하지 않으려면 useEffect 내부로 함수를 옮겨야한다.)

 

내부 로직을 간단히 설명하자면,

1. 관찰 대상이 등록되거나 가시성에 변화가 생기면 콜백을 호출한다.

2. 콜백 내부에서는 observe(관찰)를 멈추고 api 요청이 처리가 완료되면 다시 observe를 시작한다.


IntersectionObserver callback

 

IntersectionObserver의 callback에서는 entry 배열과 observer를 넘겨준다.

 

나는 한가지의 엘리먼트만 관찰하므로 비구조화 할당으로 첫번째 요소만 가져왔다.

 

observer의 unobserve를 이용하여 target 추적을 제어한다.


IntersectionObserver option

 

IntersectionObserver의 생성자 호출 시 넘기는 option은 다음과 같다.

 

예제에서는 default 값들로 option을 작성하였다.


구현 중 이슈 해결

백에서 가져오는 페이지의 인덱스를 state나 let으로 선언한 변수로 관리하였을 때, 최신 값이 제대로 반영되지 않는 이슈가 있었다.

 

(지금 생각해보니 페이지의 인덱스를 참조하는 함수를 useCallback으로 감쌌는데, deps에 넣어주지 않아 최신 값이 반영되지 않아 발생한 이슈인 것 같다..)

 

ref를 이용하여 페이지 인덱스를 관리하니 이슈가 해결되었다.

// let pageIndex = 0;
// const [pageIndex, setPageIndex] = useState<number>(0);
// 위 방식은 정상적으로 작동하지 않는 이슈가 있었다.

const pageIndexRef = useRef<number>(0);

console.log(pageIndexRef.current); // 0

pageIndexRef.current++;

console.log(pageIndexRef.current); // 1

관련글 더보기

댓글 영역