React

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

swanzzz 2025. 1. 22. 22:07
반응형

 

 

무한스크롤이란?

  • 연속적인 스크롤을 제공하는 UI/UX 요소를 의미합니다.

웹이나 앱에서 스크롤이 페이지의 끝에 도달했을 때, 자동으로 다음 데이터를 요청하여 받아오는 방식 으로 별도의 페이지 이동없이 데이터를 계속 요청하기에 자주 사용되는 기능으로 유튜브에서 화면 가장 아래로 이동 하였을 경우 잠시간의 로딩효과와 함께 새로운 데이터를 받아오는 데 이게 무한스크롤 이벤트를 활용한 것 입니다.


구현

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

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


1. API 통신 설정

해당 url 주소에 GET 요청을 보내고 받아온 IMAGE를 저장할 DogImage 배열을 생성해 주겠습니다.

import "./App.css";
import React, {useState};
import axios from "axios";

function App () {
  const [dogImage, setDogImage] = useState([]);

  // url에 GET 요청을 보내는 함수
  const getDogImage = () => {
      axios.get('https://dog.ceo/api/breeds/image/random')

    // 요청이 성공한 경우
    .then(response => {
      console.log("요청성공")
      setDogImage((prev) => [...prev, response.data.message]);
    })

    // 요청이 실패한 경우
    .catch(error => {
      console.error(error);
    })
  }
  return (
    <div></div>
  )
}

위 코드에서 getDogImage 함수를 통해 axios 요청이 보내지고 보낸 axios 요청이 성공적으로 받아온 경우 기존의 배열에 새로운 이미지를 추가하는 형식으로 코드를 작성해 주었습니다.

이제 요청이 무한히 계속되기만 하면 dogImage 배열은 무한히 늘어날 것 입니다.


2. 받아온 이미지를 화면에 나타내기

출력을 위해선 dogImage배열에 저장된 순서대로 화면에 나타내 주어야 합니다.
그렇다면 배열을 탐색하며 각각의 이미지들을 출력해 주어야 하니 map 함수를 활용해 화면에 나타내 주겠습니다.

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

받아온 이미지가 있을 경우 화면에 표시해 주기 위해 조건으로 배열의 길이를 지정해 주었습니다.

받아온 이미지가 있다면 해당 배열을 탐색하며 img 요소를 화면에 나타내는 방식으로 작성해 주었습니다.


3. 무한스크롤을 이용해 API 요청 보내기

  • 무한 스크롤을 사용하기 위해선 useRef 와 IntersectionObserver 를 사용해 주어야 합니다.

무한 스크롤을 위해선 빈요소가 필요하고 해당 요소가 화면에 보이게 되면 API 통신을 통해 데이터를 받아오는 구조입니다.

따라서 useRef를 설정해 빈요소가 화면에 보이는지 판단해 주어야 하고 요소가 화면에 보인다면 데이터 요청을 보내기 위해 useEffect를 사용해 주어야 합니다.

또 IntersectionObserver를 이용하여 해당 빈 요소가 화면에 나타나는지 주시해 주어야 합니다.

그러기 위해 useRef를 선언 후 변수로 설정해 주고 useEffect 에 IntersectionObserver를 선언하여 해당 빈 요소를 주시해주어야 합니다.

import "./App.css";
import React, {useState, useRef, useEffect};    // useRef 와 useEffect 추가
import axios from "axios";

function App () {
  const [dogImage, setDogImage] = useState([]);
  const scrollRef = useRef(null);    // useRef 사용을 위해 변수 scrollRef 선언

  // url에 GET 요청을 보내는 함수
  const getDogImage = () => {
      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 함수 실행하기

  // 1. observer 생성 및 변수 선언 후 주시할 요소 설정
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {

        // 2. 매개변수를 이용해 주시할 요소 선택
        const first = entries[0];
        if (first.isIntersecting) {

          // 3. 함수 호출
          getDog(); 
        }
      },
      { threshold: 0.5 } // 4. 이미지가 절반 이상 보일 때 호출
    );

    // 5. useRef로 설정한 요소가 DOM에 보이기 시작하면 선언한 observer로 해당 요소를 주시 
    if (scrollRef.current) {
      observer.observe(scrollRef.current);
    }

    // 6. useEffect의 클린업 함수를 이용해 해당 이벤트를 제거합니다.
    return () => {
      if (scrollRef.current) {
        observer.unobserve(scrollRef.current);
      }
    };
  }, [dogImage]); // 7. useEffect의 의존생배열에 dogImage를 추가하여 dogImage 배열이 변경될 때마다 observer를 다시 설정하며 get요청 보내기
  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>
  )
}
  1. IntersectionObserver 를 사용하기 위해 선언 후 생성 해주었습니다.
  2. entries는 IntersectionObserver의 매개변수로 배열을 매개변수로 받습니다. 따라서 배열의 첫번째 요소를 지정하기 위해 entries[0] 을 새로운 first 변수로 선언해 주었습니다.
  3. 주시중인 요소가 화면에 보이게 되면 함수를 호출해 줍니다.
  4. 이때 주시중인 요소가 화면에 보일때 얼마나 보이는지 판단하는 요소로 threshold를 이용하여 주시중인 요소가 절반이상 보이는 경우 함수를 호출할 수 있도록 설정해 주었습니다.
  • 위 과정으로 무한 useEffect에 무한 스크롤이 동작가능하도록 설정해 주었는데 현재 dogImage를 의존성배열에 추가한 상태라 최초 1회 동작 후 dogImage가 변경되어야 useEffect가 동작합니다. 따라서 5번을 설정하여 useEffect 동작조건을 맞춰주었습니다.
  1. useRef를 설정한 요소가 화면에 보일경우 useEffect 동작을 위해 주시할 요소를 useRef를 설정한 요소로 바꿔주는 코드입니다.
  • 그렇다면 useRef를 설정한 요소를 IntersectionObserver가 주시하게 되고 해당 요소가 화면에 절반이상 보이는 경우 getDog 함수가 호출될 것 입니다.
  • 함수가 호출되면 새로운 이미지를 받아 배열에 저장하게 되고 이는 배열의 요소가 변경되어 다시 useEffect의 동작요건이 충족되는 것 입니다.
  1. useEffect가 계속 동작될 경우 브라우저에 무리가 가게 되고 성능 저하를 불러 일으킬 수 있으므로 useEffect의 클린업 함수인 return을 설정해 주어 동작하지 않을 때 이벤트를 제거해 메모리 누수를 방지해 주었습니다.

최종결과

 

 

간단한 예시를 이용해 무한스크롤을 구현해 보았습니다.

지금 코드는 페이지네이션 처리가 되어있지 않아 실제 프로젝트에서는 주어진 페이지네이션 조건에 맞춰 index 나 page 번호에 맞춰 API 요청을 보내도록 수정해 주면 됩니다.


참고사항

useEffect에 의존성배열을 추가하였는데 useEffect가 2번 동작하는 경우가 있습니다.

이는 index.js 에 설정된 React.StrictMode 때문에 2번씩 동작하는 것으로 해당 부분을 제거하면 1번씩 동작합니다.

 

 

그러나 React.StrictMode는 여러 개발 도구와 경고를 제공하는 기능이기 때문에 개발중에는 되도록 지우지 말고 개발이 완료된 시점에 지워주시면 됩니다.


더보기

전체코드

import "./App.css";
import React, {useState, useRef, useEffect};    // useRef 와 useEffect 추가
import axios from "axios";

function App () {
  const [dogImage, setDogImage] = useState([]);
  const scrollRef = useRef(null);    // useRef 사용을 위해 변수 scrollRef 선언

  // url에 GET 요청을 보내는 함수
  const getDogImage = () => {
      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 함수 실행하기

  // 1. observer 생성 및 변수 선언 후 주시할 요소 설정
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {

        // 2. 매개변수를 이용해 주시할 요소 선택
        const first = entries[0];
        if (first.isIntersecting) {

          // 3. 함수 호출
          getDog(); 
        }
      },
      { threshold: 0.5 } // 4. 이미지가 절반 이상 보일 때 호출
    );

    // 5. useRef로 설정한 요소가 DOM에 보이기 시작하면 선언한 observer로 해당 요소를 주시 
    if (scrollRef.current) {
      observer.observe(scrollRef.current);
    }

    // 6. useEffect의 클린업 함수를 이용해 해당 이벤트를 제거합니다.
    return () => {
      if (scrollRef.current) {
        observer.unobserve(scrollRef.current);
      }
    };
  }, [dogImage]); // 7. useEffect의 의존생배열에 dogImage를 추가하여 dogImage 배열이 변경될 때마다 observer를 다시 설정하며 get요청 보내기
  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>
  )
}

 

 

반응형