Skip to main content

Command Palette

Search for a command to run...

[React] 무한 스크롤 구현

Updated
2 min read

React로 개발하는 웹 페이지에서 매 렌더링마다 데이터를 가져오게 된다면 불필요하게 많은 데이터 읽기가 발생할 수 있다. 이를 방지하기 위해 Pinterest 무한스크롤처럼 스크롤이 하단에 닿을 때마다 일정 개수만큼 데이터를 가져오도록 구현한 과정이다.

Scroll Sensor 구현

<div className={styles.pageEnd} ref={pageEnd}/>

보이기 쉽게 빨간색으로 표현한 부분이 sensor div이다.

화면에서 sensor div가 보이는지는 IntersectionObserver api을 사용하여 관찰한다.

IntersectionObserver 객체와 콜백함수 생성

const pageEnd = useRef(null);
const observer = new IntersectionObserver(onIntersect, { threshold: 0 });

useEffect(() => {
    if (pageEnd.current) observer.observe(pageEnd.current);
  }, []);

페이지가 완전히 렌더링된 후 useEffect 안에서 observer 객체가 pageEnd.current를 대상으로 관찰할 수 있도록 렌더링한다. threshold는 대상 div의 어느 정도만큼 보일 때 함수를 호출할지를 결정하는 인자이다. (0.5면 절반이 보일 때 함수 호출)

const onIntersect = async ([entry], observer) => {
    if (entry.isIntersecting) {
      observer.unobserve(entry.target);
      await getMorePhotos();
      setTimeout(() => {
        observer.observe(entry.target);
      }, 800);
    }
  };

observer 객체에게 전달하는 콜백함수는 화면과 sensor div가 intersect할 때 호출되는 함수이다.

우리는 다음 데이터를 더 로드하도록 하는 getMorePhotos()를 호출한다.

이 때, 순간적으로 콜백함수가 여러 번 호출되는 것을 막기 위해 onIntersect 안에서 잠시 observe를 해지하고, 일정시간 setTimeout 후 다시 observe를 걸어주었다.

getMorePhotos 구현

let timeStamp = useRef(null); 

const getMorePhotos = async () => {
    console.log("photo request");

    let queryTemp;
    if (!timeStamp) {
      //first query
      queryTemp = query(
        collection(db, "Photos"),
        orderBy("timestamp", "desc"),
        limit(10)
      );
    } else {
      queryTemp = query(
        collection(db, "Photos"),
        orderBy("timestamp", "desc"),
        startAfter(timeStamp),
        limit(10)
      );
    }
    setIsLoading(true);

    let dataSnapShot;
    try {
      dataSnapShot = await getDocs(queryTemp);
    } catch (error) {
      console.log(error);
    }

    const dataList = dataSnapShot.docs.map((doc) => doc.data());

    const length = dataList.length;
    if (length) {
      timeStamp = dataSnapShot.docs[length - 1];
      setPhotos((prev) => [...prev, ...dataList]);
    } else {
      setEndOfData(true);
    }
    setIsLoading(false);
  };

가져오는 데이터에는 timestamp field가 존재한다. 데이터를 시간순으로 display하기 위해 timestamp를 기준으로 쿼리를 작성한다.

if (!timeStamp) {
      //first query
      queryTemp = query(
        collection(db, "Photos"),
        orderBy("timestamp", "desc"),
        limit(10)
      );

가장 처음 데이터를 읽을 때는 시간순으로 정렬된 데이터를 처음부터 (최대)10개를 읽어온다.

timestamp는 useState로 상태관리를 하려했지만, 미상의 이유로 잘 작동하지 않아 useRef로 저장하며 사용하였다.

else {
      queryTemp = query(
        collection(db, "Photos"),
        orderBy("timestamp", "desc"),
        startAfter(timeStamp), //timestamp 뒤에 있는 데이터들
        limit(10)
      );
    }

이후 데이터를 읽을 때는 timeStamp를 기준으로 (최대)10개의 데이터를 읽어온다.

const length = dataList.length;
    if (length) {
      timeStamp = dataSnapShot.docs[length - 1];
      setPhotos((prev) => [...prev, ...dataList]);
    else {
      setEndOfData(true);
    }

그 이후 가져온 데이터의 길이를 받아 timeStamp를 업데이트한다.

만약 받아온 데이터가 10개보다 적다면 endOfData flag를 업데이트한다.

모든 데이터 로드가 끝났을 때

{!endOfData && <div className={styles.pageEnd} ref={pageEnd} />}
{endOfData && (<div
          className={styles.footer}>

모든 데이터가 로드됐을 때는 sensor div의 관찰이 중지되어야 한다.

useEffect를 사용하여 unobserve 처리를 해주어도 좋고, 나는 단순히 요소를 제거해버렸다.

그리고 최종적으로 스크롤이 하단에 닿았으므로 그 자리에 footer를 생성한다.

edited by 정정환

L
lva_jiho2y ago

어디서 많이 본 코드인데요?

More from this blog

[ZSH] tree 사용하기

들어가며 큰 규모의 프로젝트를 출시한 뒤, 후일을 위해서 더 늦기 전에 파일 정리 및 문서화를 진행해야했다. 문서화 작업을 하는 중에 기왕 정리하는 거 파일 구조를 이쁘게 트리 구조로 나열하여 코멘트를 달면 나중에 보더라도 이해하기 더 쉬울 것 같았다. 어떻게 해야 간지나는 트리 구조를 만들 수 있을까 방법을 찾다보니 역시나 파일 구조를 트리로 이쁘게 출력해주는 커맨드 툴이 존재했다. tree 커맨드에 대해서 알아보고 알짜배기 내용만 정리했다....

Feb 21, 20242 min read

[Next.js] parallel routes & intercepting routes

트위터 로그인 모달창을 만들어보며 넥스트의 parallel routes 와 intercepting routes 을 학습한 내용을 정리해보았습니다. 트위터 로그인 창을 확인해봅시다. 루트 디렉토리 화면을 배경으로 i/flow/login 페이지가 동시에 표시되고 있습니다. 저는 app router 를 학습하기 전까지는 createPortal 을 사용하여 포탈 영역에 로그인 컴포넌트를 띄우는 방식을 사용했었습니다. const NoLogin =()=...

Feb 1, 20244 min read

C/C++ 이진 트리(binary tree) 개요 및 구현(1)

개요 트리는 노드들이 나무 가지처럼 연결된 비선형 계층적 자료구조이다. 하위 트리가 존재하고, 그 노드에 또 하위 트리가 존재하는 자료구조 이다. 트리의 맨 위에 있는 루트 노드가 존재한다. 우리가 알아볼 트리는 이진 트리이다. 이진 트리는 자식 노드(부모로부터 아래로 이어진 노드)가 2개 이하인 구조를 말한다. 트리의 사용 사례로는 다음과 같다 계층 적 데이터 저장(파일,폴더) 효율적인 검색 속도 힙 데이터 베이스의 인덱싱 트리에 ...

Jan 31, 20244 min read

[React] Server component (RSC)

React.js 18 에 도입된 리액트 서버 컴포넌트는 서버에서 동작하는 리액트 컴포넌트를 의미합니다. Next가 권장하는 라우팅 방식인 app router의 기반이 되는 컴포넌트이기 때문에 app router 를 이해하기 위해서는 server component 에 대한 이해가 필요합니다. server component 리액트는 클라이언트단만을 컴포넌트화하는 대신, server component라는 개념을 통해 서버 영역을 컴포넌트화합니다. ...

Jan 29, 20243 min read

Flutter, JavaScript

42 posts