[React] useRef를 이용한 무한스크롤 구현하기

정수완·2024년 3월 10일
0

React

목록 보기
2/8

무한스크롤이란?

  • 연속적인 스크롤을 제공하는 UI/UX 요소를 말한다.

웹이나 앱에서 스크롤이 페이지의 끝에 도달했을 때, 자동으로 다음 데이터를 요청하여 받아오는 방식 으로 별도의 페이지 이동없이 데이터를 계속 요청하기에 자주 사용되는 기능이다.

유튜브에서 화면 가장 아래로 이동 하였을 경우 잠시간의 로딩효과와 함께 새로운 데이터를 받아오는 데 이게 무한스크롤 이벤트를 활용한 것이다.


구현

무한스크롤을 구현하기 위해서는 api 통신이 필요하므로 간단한 예시를 위하여 "The Dog API" 를 이용하여 구현해 보겠습니다.

Dog API 사이트에 접속하면 하나의 url 주소가 보이는데 이 주소에 GET 요청을 보내면 IMAGE에 있는 귀여운 강아지 그림을 받아볼 수 있습니다.

useRef && IntersectionObserver

  • IntersectionObserver 란 특정 HTML 요소가 화면에 등장하는지 감시해주는 역할을 합니다
    말 그대로 옵저버 역할을 하는것 입니다.
  • useRef 는 특정 객체를 선택하여 DOM 조작이나 변수처럼 사용이 가능한 React Hooks 입니다.

이 두가지를 이용하여 무한스크롤을 구현해 보겠습니다.

1. 변수지정

useRef 와 IntersectionObserver 를 사용하기 위해 각각을 선언 해주겠습니다.

useRef 를 사용하기 위해 상단에 useRef 를 선언한 뒤 scrollRef 로 지정해 주었습니다.


2. IntersectionObserver 조건설정

IntersectionObserver 를 사용하기 위해 observer 로 새로이 생성해 주었습니다.

IntersectionObserver 의 콜백함수로 entries 를 매개변수로 선언해 주었습니다.
이 함수에서 entries는 배열을 매개변수로 받기에 첫번째 요소를 선택하기 위해 first 변수로
entries의 첫번째 요소를 선택해 주었습니다.
해당 요소가 화면에 보이게 되면 getDog 함수를 실행하여 이미지를 받아서 저장해 줍니다.

아래 if 문과 return문은 ref로 선택한 요소가 DOM 에 보이게 되면 observer로 해당 요소를 감시하고 useEffect의 클린업함수 return 으로 해당 이벤트를 제거하는 과정입니다.

마지막으로 useEffect의 의존성배열에 dogImage가 변경되는 경우 즉 url을 새로 받아오는 경우를 선택하여 useEffect의 동작을 제한해 주었습니다.


3. 이미지 받아오기

axios 통신으로 THE DOG API 사이트에 GET 요청을 보내 이미지를 받아온 후
받아온 이미지를 dogImage배열 에 저장해 주었습니다.


4. 화면에 출력하기


출력을 위해 map 함수를 이용하여 각각의 요소들을 img 태그로 출력해 주었고
ref로 감시할 빈 div 태그를 가장 아래쪽에 추가해 주었습니다.


결과

그럼 이렇게 무한스크롤이 작 작동하는걸 볼 수 있습니다.

간단한 예시로 이미지를 계속 받아오고 페이지네이션 처리가 안 되어있기 때문에 실제 프로젝트나 업무에서는 주어진 페이지네이션 조건에 맞춰서 index 나 page 번호를 증가시키는 방향으로 사용하시면 됩니다.

참고사항

React useEffect(() => {}, []) 형식으로 의존성 배열을 비워 1번만 동작하도록 작성했는데
2번 동작하는 경우 > React.StrictMode

src > index.js

폴더 내의 React.StrictMode를 지워주시면 useEffect가 두번씩 동작하는것 을 방지할 수 있습니다.


전체코드

App.jsx

import './App.css';
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';

function App() {
  const scrollRef = useRef(null); // 마지막 이미지를 참조할 ref
  const [dogImage, setDogImage] = useState([]);

  // 이미지를 불러오는 함수
  const getDog = () => {
    axios.get('https://dog.ceo/api/breeds/image/random')
      .then(response => {
        console.log('이미지 요청');
        setDogImage((prev) => [...prev, response.data.message]);
      })
      .catch(error => {
        console.error(error);
      });
  };

  useEffect(() => {
    getDog(); // 컴포넌트가 마운트될 때 초기 이미지 로드
  }, []);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        const first = entries[0];
        if (first.isIntersecting) {
          getDog(); // 새 이미지가 화면에 보일 때마다 getDog 함수 실행
        }
      },
      { threshold: 0.5 } // 이미지가 절반 이상 보일 때 호출
    );

    if (scrollRef.current) {
      observer.observe(scrollRef.current);
    }

    return () => {
      if (scrollRef.current) {
        observer.unobserve(scrollRef.current);
      }
    };
  }, [dogImage]); // dogImage가 변경될 때마다 observer를 다시 설정

  return (
    <div>
      {dogImage.length > 0 && dogImage.map((address, index) => {
          return (
            <img
              className="dog-image"
              src={address}
              alt={`dog-${index}`}
              key={index}
              />
          );
      })}
      <div ref={scrollRef}></div>
    </div>
  );
}

export default App;

App.css

.dog-image {
  width: 300px;
  height: 500px;
  margin: 1rem;
  border-radius: 10px;
}

0개의 댓글