본문 바로가기

Computer Programming/React

리액트 기능구현 스터디- 3. Infinite Scroll 리팩토링 회고 (vs react-virtualized)☀️

1. 주제 소개

1) 완성 조건

STEP 1 : 리스트 구현 +  리스트 아래에 목록 추가

STEP 2 : 스크롤이 맨 바닥을 바라보면 로드

STEP 3 : 확대 축소를 해도 문제없이 되는지

2) 시간

- 07.08 토요일 오후 3시~ 오후 8시

3) 사용기술과 라이브러리

- React, styled-components, font awesome icons, react-loader-spinners

 

4) 폴더 구조

이번에는 간단하게 필요한 부분만 넣었다.

 

5) 전체 회고

인피니티 스크롤을 구현하는데에는 크게 두가지 방법이 있다. 1) Scroll Event로 핸들링하기 2) Intersection Observer API를 이용하기

 

 다음 프로젝트 때에도 인피니티 스크롤을 구현하게 될텐데 이때는 이벤트를 핸들링하는 방법도 해봐야겠다.

 

그리고 리펙토링에 대해서는 옵저버 사용에서 크게 문제될 것은 없어보였다. 물론 리액트쿼리나 미들웨어 등 다양하게 구현하셔서 이래저래 다 해보고싶긴 하지만... 원하는 특정 기능이 완성됬으니 우선은 미뤄뒀다.

 

그 와중에 다른 팀원 중에 한 분이 Skeleton UI를 적용하셨는데, 나는발표를 들으면서도 그런 UI를 사용한줄도 모르고 무의식적으로 사용자의 입장에서 느리다는 생각이 드는 틈도 없이 그냥 기다렸다.

 

 

 

실제로 Skeleton UI를 적용하면 사용자의 이탈률도 적어지고 UX 개선에 큰 도움이 된다고 한다. 그래서 이번 리펙토링에서는 skeleton ui를 추가하고 로딩 속도를 조금 늘려서 어떻게 작동하는지 살펴보려고 한다.

 

 

 


 

 

 

2. 기능 구현 코드 

 

https://velog.io/@jce1407

 

List.js 전체 코드

const List = () => {
  const [data, setData] = useState([]);
  const [page, setPage] = useState(1);
  const obsRef = useRef(null); //observer Element (현재 참조되는)

  useEffect(() => {
    fetchDogs();
    const observer = new IntersectionObserver(obsHandler, {
      threshold: 0,
      rootMargin: "150% 0px",
    }); //element가 root 화면에 나타났을 때 obsHandler 실행
    if (obsRef.current) {
      observer.observe(obsRef.current); //처음엔 null 이라 실행 안되지만 return 문에서 참조한 뒤 실행됨
    }
  }, []);

  useEffect(() => {
    fetchDogs();
  }, [page]);

  //obs element가 화면에 진입했을 때 실행됨
  const obsHandler = (entries) => {
    const target = entries[0]; //boundingClientRect: 관찰 대상의 사각형 정보 (타겟 설정)
    if (target.isIntersecting) {
      console.log("intersected");
      setPage((prev) => prev + 1);
    }
  };

  const fetchDogs = async () => {
    const response = await getDogs();
    if (response) {
      console.log(response);
      setData((prev) => prev.concat(response));
    }
  };

  return (
    <ListBox>
      {data.map((item, idx) => {
        return <ListItem key={idx} idx={idx} url={item.url} id={item.id} />;
      })}
      <div ref={obsRef} className="ref">
        <Progress />
      </div>
    </ListBox>
  );
};

export default List;

const ListBox = styled.div`
  margin: 100px 100px 0px 100px;
  width: 70%;
  .ref {
    padding-top: 50px;
  }
`;

 

 

Intersection Observer API는 웹 개발에서 사용되는 JavaScript API 중 하나로, 브라우저의 뷰포트와 요소 사이의 교차 상태를 감지할 수 있다. 이 기능을 활용해 infinite scroll을 구현할 수 있다. 옵저버는 요소가 뷰포트에 진입했는지, 벗어났는지, 얼마나 교차했는지 (크기와 위치 정보) 그리고 뷰포트와 교차하는 시점을 감지할 수 있다.

 

 

인피니트 스크롤 뿐만 아니라 이미지 지연로딩, 광고 로드 제어 등에 사용되기도 한다.

 

 

 

useEffect(() => {
    fetchDogs();
    const observer = new IntersectionObserver(obsHandler, {
      threshold: 0,
      rootMargin: "150% 0px",
    }); //element가 root 화면에 나타났을 때 obsHandler 실행
    if (obsRef.current) {
      observer.observe(obsRef.current); //처음엔 null 이라 실행 안되지만 return 문에서 참조한 뒤 실행됨
    }
  }, []);

 

 

첫 렌더링 시 옵저버 생성 전 10개의 데이터를 먼저 뿌려준다. 그 다음부터는 IntersectionObserver의 instance가 생성되면서 threshold과 rootMargin 속성에 따라 조건을 만족하면 obsHandler을 실행시킨다. 나는 이 프로젝트에서 이미지들을 불러오고 리사이징을 하지 않기 때문에 로딩 속도가 꽤 걸리는 편이었다. 

 

 

  return (
    <ListBox>
      {data.map((item, idx) => {
        return <ListItem key={idx} idx={idx} url={item.url} id={item.id} />;
      })}
      <div ref={obsRef} className="ref">
        <Progress />
      </div>
    </ListBox>
  );
};

 

그래서 threshold를 통해 뷰포트에 element인 div가 진입하자마자 obsHandler가 실행하도록 했다. 또한 rootMargin 속성을 줌으로서 뷰포트가 마지 아래로 늘어나 영역을 차지한 것 처럼 보이게 해 조금 더 빨리 진입하도록 했다. 그래서 화면에서 보이는 view height 보다 길이가 길기 때문에 유저가 맨 아래로 화면을 내리지 않아도 loading이 미리 실행되는 것이다.

 

 

 

그래서 프로그램 실행 시 첫 렌더링부터 obsHandler의 console을 찍은 부분이 나타난다.

 

 

 

 

 

 

이제 obsHandler을 살펴보자

 

  //obs element가 화면에 진입했을 때 실행됨
  const obsHandler = (entries) => {
    const target = entries[0]; //boundingClientRect: 관찰 대상의 사각형 정보 (타겟 설정)
    if (target.isIntersecting) {
      console.log("intersected");
      setPage((prev) => prev + 1);
    }
  };

 

여기서 매개변수인 entries는 Intersection Observer의 콜백함수에 전달되는 매개변수이다. 

 

entries는 배열 형태로, 관찰 대상 요소 ref의 정보를 담고 있다. 

  • boundingClientRect: 관찰 대상의 사각형 정보(DOMRectReadOnly)
  • intersectionRect: 관찰 대상의 교차한 영역 정보(DOMRectReadOnly)
  • intersectionRatio: 관찰 대상의 교차한 영역 백분율(intersectionRect 영역에서 boundingClientRect 영역까지 비율, Number)
  • isIntersecting: 관찰 대상의 교차 상태(Boolean)
  • rootBounds: 지정한 루트 요소의 사각형 정보(DOMRectReadOnly)
  • target: 관찰 대상 요소(Element)
  • time: 변경이 발생한 시간 정보(DOMHighResTimeStamp)

이 중에서는 isInteracting 요소를 이용해 무한 스크롤에 이용할 수 있다. 정한 속성에 따라 Intersecting이 일어나면 console을 출력하고 page + 1 을 해준다. 

 

그리고 이 page가 변경되면 useEffect를 통해 변화를 감지하고 데이터를 더 가지고 온다.

 

 

useEffect(() => {
	fetchDogs();
}, [page]);

 

page 변경시에만 useEffect가 실행되도록 했다. 그리고 그 안의 fetchDogs()는

 

  const fetchDogs = async () => {
    const response = await getDogs();
    if (response) {
      console.log(response);
      setData((prev) => prev.concat(response));
    }
  };

 

비동기 함수 구조이고 api/api.js에서 프로미스 객체를 반환하는데, 이에 따라 data를 변경하면 된다.

 

api/api.js

const API_KEY = import.meta.env.VITE_API_KEY;
const URL = import.meta.env.VITE_DOG_URL;

export const getDogs = async () => {
  try {
    const response = await axios.get(URL, {
      headers: {
        "x-api-key": API_KEY,
      },
      params: {
        limit: 10,
      },
    });
    return response.data;
  } catch (e) {
    console.log(e);
  }
};

 

 

옵저버 사용 인피니트 스크롤 로직은 여기서 마무리 👻

 

 

 


3. 코드 리팩토링

 

- Skeleton UI를 적용해보자

 

 

1) skeleton ui를 적용하기 위해 현재 loading 중인지 체크하는 boolean 상태를 관리해야한다.

 

  const [isLoading, setIsLoading] = useState(false)

 

데이터 fetch 로직에도 상태를 추가해준다.

 

  const fetchDogs = async () => {
    setIsLoading(true)
    const response = await getDogs();
    if (response) {
      console.log(response);
      setData((prev) => prev.concat(response));
    }
    setIsLoading(false)
  };

 

 

 

 

그리고 기존의 Progress  대신 Skeleton.js 로 대체한다.

 

 

 

Skeleton.js

import { keyframes, styled } from "styled-components";
import { ItemBox } from "../ListItem";

const Skeleton = () => {
  return (
    <StyledItemBox>
      <div className="idx"></div>
      <div className="imgCircle"></div>
      <p className="id"></p>
    </StyledItemBox>
  );
};

export default Skeleton;

const blinkAnimation = keyframes`
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0.3;
  }
  100% {
    opacity: 1;
  }
`;


//Itembox styling 재사용
const StyledItemBox = styled(ItemBox)`
  border: 1.5px solid #ccc;
  .idx {
    background: #ccc;
  }
  .imgCircle {
    background-color: #ccc;
    animation: ${blinkAnimation} 1.5s linear infinite; // 회전 애니메이션 적용
  }

  .id {
    width: 25rem;
    background-color: #ccc;
    animation: ${blinkAnimation} 1.5s linear infinite; // 회전 애니메이션 적용
  }
`;

 

스타일 재사용을 위해 styled(Itembox)로 호출한다. 이 방법을 몰라서 꽤나 애먹었다 ,,,,,ㅠ

 

 

 

 

 

👻

 

 

그러면 react-virtualize vs Interseption Observer api ?

두개 다 무한스크롤을 구현할 수 있지만, 각각 장단점이 있다.

 

우선 라이브러리를 사용하면 react-virtualized를 사용하게 되는데, 이는 아이템의 크기나 스크롤 위치 변경 시에 자동으로 최적화를 수행해준다. 스크롤 이벤트에 따라 작동하며 렌더링을 최소화해 부드럽게 스크롤할 수 있다.

 

그리고 오늘 구현한 것 처럼 Observer api를 통해 순수 자바스크립트로 사용하면, 외부 라이브러리에 의존하지 않고 간단한 기능이라면 스타일링 등의 사항을 더 유연하게 처리할 수 있다.

그러나 Intersection Observer API의 사용법을 잘... 이해해야하며 성능 최적화에 대한 부분이 개발자에게 달려있어 구현이 복잡해질 수도 있다.

 

 

react-virtalized 사용법은 요기

 

https://nayoungkim00.tistory.com/64

 

react-virtualized를 사용한 렌더링 최적화

올해 초 캐나다에서 코딩 공부한지 정말 얼마 안 됬을 때.... 백엔드 친구와 주식 관련 앱을 만들어보려고 했던 적이 있는데 주식 데이터가 실시간으로 너무 많다보니 렌더링이 느리고 성능도 매

nayoungkim00.tistory.com

 

 

 


Reference: 

https://velog.io/@jce1407/React-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-Intersection-Observer

https://heropy.blog/2019/10/27/intersection-observer/