728x90
반응형

이번 포스트에서는 리액트에서 무한스크롤 이벤트를 구현해보겠다.

 

IntersectionObserver이란?
타겟엘리먼트와 타겟의 부모 혹은 상위엘리먼트의 뷰포트가 교차되는 부분을 비동기적으로 관찰하는 API

 

먼저 리액트 앱을 설치한다.

$ npx create-react-app [프로젝트 명]

 

 

1. App.js 파일을 수정해서 Item을 여러개와 Item 요소들이 들어갈 Itemlist를 만들어준다.

App.js

import "./App.css";
import styled from "styled-components";
import { useState } from "react";

const ItemWrap = styled.div`
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: center;
  align-items: center;

  .Item {
    width: 350px;
    height: 300px;
    display: flex;
    flex-direction: column;
    background-color: #ffffff;
    margin: 1rem;
    box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
    border-radius: 6px;
  }
`;

function App() {
  const [itemList, setItemList] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
  return (
    <div className="App">
      <ItemWrap>
        {itemList.map((item, index) => (
          <div className="Item" key={index}>{index+1}</div>
        ))}
      </ItemWrap>
    </div>
  );
}

export default App;

 

이런 화면이 될 것이다.

 

 

2. 데이터를 받아오는 중에 로딩컴포넌트를 보여주기위해 다음을 설치하고 코드에 추가해준다.

 

또한 target State와 observer의 관찰 대상이 될 target Element를 최하단에 생성해준다.

 

$ npm i react-loading

 

App.js

import ReactLoading from "react-loading";

...

const LoaderWrap = styled.div`
  width: 100%;
  height: 80%;
  display: flex;
  justify-content: center;
  text-align: center;
  align-items: center;
`;

...

function App() {
  const [itemList, setItemList] = useState([1, 2, 3, 4, 5]); // ItemList
  const [target, setTarget] = useState(""); // target
  const [isLoding, setIsLoding] = useState(false); // isloding

  return (
    <div className="App">
      <ItemWrap>
        {itemList.map((item, index) => (
          <div className="Item" key={index}>
            {index + 1}
          </div>
        ))}
      </ItemWrap>
      {isLoding ? (
        <LoaderWrap>
          <ReactLoading type="spin" color="#A593E0" />
        </LoaderWrap>
      ) : (
        ""
      )}
      <div ref={setTarget}></div>
    </div>
  );
}
export default App;

 

3. 이제 Intersection Observer를 사용하기위해 useEffect를 선언하고 Intersection Oberver의 인자로 쓰일 함수를 선언하며, option을 지정해준다.

 

App.js

function App() {
  const [itemList, setItemList] = useState([1, 2, 3, 4, 5]); // ItemList
  const [target, setTarget] = useState(""); // target
  const [isLoding, setIsLoding] = useState(false); // isloding

  const onIntersect = async ([entry], observer) => {
    if (entry.isIntersecting && !isLoding) {
      observer.unobserve(entry.target);
      setIsLoding(true);
      // 데이터를 가져오는 부분
      setIsLoding(false);
      observer.observe(entry.target);
    }
  };

  useEffect(() => {
    let observer;
    if (target) {
      // callback 함수, option
      observer = new IntersectionObserver(onIntersect, {
        threshold: 0.4,
      });
      observer.observe(target); // 타겟 엘리먼트 지정
    }
    return () => observer && observer.disconnect();
  }, [target]);

  return (
    <div className="App">
      <ItemWrap>
        {itemList.map((item, index) => (
          <div className="Item" key={index}>
            {index + 1}
          </div>
        ))}
      </ItemWrap>
      {isLoding ? (
        <LoaderWrap>
          <ReactLoading type="spin" color="#A593E0" />
        </LoaderWrap>
      ) : (
        ""
      )}
      <div ref={setTarget}></div>
    </div>
  );
}

 

useEffect 함수 안에 보면 위에 선언한 callback함수와 option을 deps로 넣어준 것을 볼 수 있는데,

조금 더 자세히 알아보면

 


※ Intersection Oberver 첫 번째 deps : callback()

- callback : 타겟 엘리먼트가 교차되었을 때 실행할 함수

 

※ Intersection Oberver 두 번째 deps : root, rootMargin, threshold

 - root 

  • default : null, 브라우저의 viewport
  • 교차영역의 기준이 될 root 엘리먼트. observer의 대상으로 등록할 엘리먼트는 반드시 root의 하위 엘리먼트여야 한다. 

 - rootMargin

  • default : '0px 0px 0px 0px'
  • root 엘리먼트의 margin. 

 - threshold

  • default : 0
  • 0.0부터 1.0 사이의 숫자 혹은 이 숫자들로 이루어진 배열로, 타겟 엘리먼트에 대한 교차 영역 비율을 의미한다. 
  • 0.0의 경우 타겟 엘리먼트가 교차영역에 진입했을 시점이고, 1.0의 경우 타겟 엘리먼트 전체가 교차영역에 들어왔을 때의 observer를 실행하는 것을 의미한다.


 

다시 한 번 코드를 살펴보면

  useEffect(() => {
    let observer;
    if (target) {
      // callback 함수, option
      observer = new IntersectionObserver(onIntersect, {
        threshold: 0.4,
      });
      observer.observe(target); // 타겟 엘리먼트 지정
    }
    return () => observer && observer.disconnect();
  }, [target]);

target 엘리먼트로 지정한 target State가 첫 렌더링 때 생성될 것이고, 첫 렌더링 때와 이 target의 변경이 감지될 때 useEffect가 실행된다. callback 함수로는 위에 선언한 onIntersect 함수이고, option으로 threshold : 0.4를 지정했다.

 

  const onIntersect = async ([entry], observer) => {
    if (entry.isIntersecting && !isLoding) {
      observer.unobserve(entry.target);
      setIsLoding(true);
      // 데이터를 가져오는 부분
      setIsLoding(false);
      observer.observe(entry.target);
    }
  };

useEffect가 실행되고 callback함수인 onIntersection이 실행된다. target 엘리먼트가 교차지점에 들어오고

isLodingfalse일 때 target 지정을 철회하고 데이터를 가져올 때 동안 isLoding Statetrue로 만들어준다.

 

데이터가 정상적으로 들어오면 isLoding State를 다시 false로 바꿔주고, targetElement를 재지정한다.

데이터를 임의로 가져왔다고 예상하고 itemList에 추가하는 로직을 구현해보자.

 

  const onIntersect = async ([entry], observer) => {
    if (entry.isIntersecting && !isLoding) {
      observer.unobserve(entry.target);
      setIsLoding(true);
      // 데이터를 가져오는 부분
      await new Promise((resolve) => setTimeout(resolve, 2000));
      let Items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
      setItemList((itemLists) => itemLists.concat(Items));
      setIsLoding(false);
      observer.observe(entry.target);
    }
  };

 

DB를 조회할 땐 그 만큼의 시간이 걸릴테지만 지금은 로딩컴포넌트를 보여주기위해 Timeout을 2초 설정했다.

  1. target Element가 첫 렌더링 될 때와 설정한 교차지점에 진입·들어왔을 때 observer 객체가 새로 생성된다.
  2. observer 객체가 callback함수option을 가진 채로 생성된다.
  3. observer 객체가 새로 생성될 때 callback 함수가 실행되어 데이터를 가져오거나 하는 로직이 실행된다.
  4. Itemlist가 수정되어 더 많아지게되고, target Element가 재지정되어 다시 최 하단으로 설정된다.

 

전체코드

App.js

import "./App.css";
import styled from "styled-components";
import { useState } from "react";
import { useEffect } from "react";
import ReactLoading from "react-loading";
import axios from "axios";

const LoaderWrap = styled.div`
  width: 100%;
  height: 80%;
  display: flex;
  justify-content: center;
  text-align: center;
  align-items: center;
`;

const ItemWrap = styled.div`
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: center;
  align-items: center;

  .Item {
    width: 350px;
    height: 300px;
    display: flex;
    flex-direction: column;
    background-color: #ffffff;
    margin: 1rem;
    box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
    border-radius: 6px;
  }
`;

function App() {
  const [itemList, setItemList] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); // ItemList
  const [target, setTarget] = useState(""); // target
  const [isLoding, setIsLoding] = useState(false); // isloding

  const onIntersect = async ([entry], observer) => {
    if (entry.isIntersecting && !isLoding) {
      observer.unobserve(entry.target);
      setIsLoding(true);
      // 데이터를 가져오는 부분
      await new Promise((resolve) => setTimeout(resolve, 2000));
      let Items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
      setItemList((itemLists) => itemLists.concat(Items));
      setIsLoding(false);
      observer.observe(entry.target);
    }
  };

  useEffect(() => {
    let observer;
    if (target) {
      // callback 함수, option
      observer = new IntersectionObserver(onIntersect, {
        threshold: 0.4,
      });
      observer.observe(target); // 타겟 엘리먼트 지정
    }
    return () => observer && observer.disconnect();
  }, [target]);

  return (
    <div className="App">
      <ItemWrap>
        {itemList.map((item, index) => (
          <div className="Item" key={index}>
            {index + 1}
          </div>
        ))}
      </ItemWrap>
      {isLoding ? (
        <LoaderWrap>
          <ReactLoading type="spin" color="#A593E0" />
        </LoaderWrap>
      ) : (
        ""
      )}
      <div ref={setTarget}></div>
    </div>
  );
}

export default App;

 

실행 화면

 

 


Next.js typescript 에서는 타겟 엘리먼트를 다음과 같이 선언해주자.

<div ref={(e: any) => { setTarget(e) }}></div>

 

728x90
반응형

+ Recent posts