카테고리 없음

[엘리스 16주차] 무한 스크롤 구현

불곰자리 2023. 12. 28. 23:50

프로젝트도 마지막에 접어든다. 다행인지 아닌지 나 혼자 메인 기능이 아닌 자잘한 기능들을 맡게 되어서 생각보다 빨리 기능 구현이 끝나게 되었다. 빨리 끝내서 다른 분들도 도와주고 싶은데... 아무튼 어제~오늘은 무한 스크롤을 구현하게 되었는데, 무한 스크롤의 로직 자체는 대강 알고 있었는데 직접 구현해보는 것은 처음이었다.

 

내가 생각하는 로직

1. 스크롤을 맨 밑으로 내림

2. 스크롤 이벤트를 감지함

3. API 요청 전달

4. 받아온 데이터를 기존 데이터에 추가

 

구현하고 보니 그렇게 틀린 생각은 아니었다. 무한 스크롤은 대략 이런 방식으로 일어난다!

구현으로 얘기를 넘기자면, 원래는 React Query 에서 제공하는 useInfiniteQuery 훅을 사용할 생각이었는데, 코치님께서 리액트 쿼리를 사용하지 않는 상황에서도 사용할 수 있도록, IntersectionObserver를 통해 구현해보라고 하셔서 어찌 밤을 새서 구현을 완료했다. (사실 감이 안 와서 화면 바라보기만 3시간 정도 한 것 같다. 역시 이럴 때마다 항상 느낀다. 일단 코드 작성하는 것이 제일 중요하다는 사실을...)

 

IntersectionObserver는 뷰포트와 IntersectionObserver가 관찰하는 요소가 교차(intersect)하는지, 뷰포트에 요소가 보이는 지 아닌지를 알려주는 수단을 제공하는 인터페이스이다. 

IntersectionObserver가 실행되는 과정

데이터를 불러와서 보여주는 구역이 있고, 대체로 맨 밑에 스크롤 이벤트 여부를 결정하는 요소가 존재한다.

IntersectionObserver는 해당 관찰 요소가 뷰포트에 들어왔을 때 파라미터로 전달한 콜백 함수를 실행시킨다. 

export default function useIntersectionObserver({ root, rootMargin = '0px', threshold = 1, onIntersect }) {
  const [target, setTarget] = useState(null);

  useEffect(() => {
    if (!target) return;

    const observer = new IntersectionObserver(onIntersect, { root, rootMargin, threshold });
    observer.observe(target);

    return () => observer.unobserve(target);
  }, [onIntersect, root, rootMargin, target, threshold]);

  return { setTarget };
}

 

코드로 보면 다음과 같다. useIntersectionObserver 훅으로 따로 파일을 분리해주었다.

const observer = new IntersectionObserver(onIntersect, { root, rootMargin, threshold });

 

IntersectionObserver 인스턴스 생성 시 첫 번째 인자로 callback 함수를, 두 번째 인자로 옵션 값을 받는다.

callback의 경우는 아까도 말했지만 뷰포트에 관찰 요소가 들어왔을 때 실행할 함수이다.

두 번째 인자로는 옵션을 전달한다. 

{
	root: null, //뷰포트로 사용할 루트를 결정합니다. null일 경우 윈도우가 root가 됨
    rootMargin: '0px', //마진 값을 이용해 뷰포트의 범위를 결정할 수 있음
    threshold: 1.0 //콜백 함수가 실행되기 위해 관찰 요소가 얼마나 보여야하는지 결정 (0 ~ 1)
}

 

해당 훅에서 setTarget 함수를 실제 무한 스크롤 기능을 사용할 페이지로 가져온다. 

return (
    <div>
      <Header title={'알림'} />
      <div className="flex justify-end mr-[24px]">
        <button className="text-[12px] text-gray-1 mb-[14px]" onClick={deleteAllAlarm}>
          전체삭제
        </button>
      </div>
      <div className={`overflow-scroll h-[calc(100vh-160px)]`}>
        {notificationList?.map((notification) => (
          <NotificationItem
            key={notification._id}
            type={notification.alarmType}
            nickname={notification.senderId.nickname}
            message={''}
            createdAt={notification.createdAt}
            url={notification.postId}
            isRead={notification.isRead}
            alarmId={notification._id}
          />
        ))}
        {
          <div ref={setTarget} className={`flex justify-center w-full ${hasNextPage && 'h-4'}`}>
            {hasNextPage && <img src={spinner} alt="loading" />}
          </div>
        }
      </div>
    </div>
  );

 

관찰하고자 하는 요소의 ref 옵션에 setTarget 함수 값을 주어 target을 해당 요소(dom element)로 변경한다.

관찰 요소가 뷰포트 내에 들어왔을 때 실행되는 handleObserver 함수는 다음과 같다.

const handleObserver = useCallback(
    (entries) => {
      const [target] = entries;
      if (target.isIntersecting && hasNextPage) {
        callback();
      }
    },
    [callback, hasNextPage],
);

 

entries는 현재 관찰하고 있는 요소에 대한 정보를 담고 있다.

target은 entries 배열의 첫 번째 값을 받는다.

(IntersectionObserver는 현재 하나의 요소만을 관찰하고 있으니 해당 요소에 대한 정보 값을 받게 된다.)

entries 데이터는 다음 값을 담고 있다.

여러 값들이 존재하지만 우선 코드 내에서 사용하는 isIntersecting 값에 대해서만 얘기하려고 한다.

(사실 다른 값은 뭔지 모르겠다...) 

 

isIntersecting 값은 어떤 로직으로 작동하는지 모르지만, 스크롤을 밑으로 내려 관찰 요소가 뷰포트에 들어올 때는 true가 되고, 스크롤을 위로 올려 관찰 요소가 뷰포트에서 벗어나면 false 값을 가지는 것 같다. 그래서 스크롤을 밑으로 내렸다가 다시 위로 올릴 때 API 요청이 중복으로 일어나는 경우는 없다. (그러나 혹시 또 모른다... API 요청 후 응답이 빨라서 요청이 중복으로 일어나는 일은 없었는데, 만일 처리 시간이 오래 걸리는 API가 있다고 했을 때, 데이터를 불러오는 도중에 다시 스크롤 이벤트가 일어나면 중복으로 요청하게 될 지... 그건 그 때 고치면 되겠지 하고 나름 긍정적으로 생각해보고 있다.)

 

#엘리스트랙 #엘리스트랙후기 #리액트네이티브강좌 #온라인코딩부트캠프 #온라인코딩학원 #프론트엔드학원 #개발자국비지원 #개발자부트캠프 #국비지원부트캠프 #프론트엔드국비지원 #React #Styledcomponent #React Router Dom #Redux #Typescript #Javascript