[React] 무한 스크롤(in 오즈의_제작소)

Lee_Sangmin·2022년 5월 17일
9

business

목록 보기
1/2
post-thumbnail

구현 방법 설명

Scroll event감지, IntersectionObserver 직접 사용 등의 방법이 존재하지만, IntersectionObserver를 간편하게 사용할 수 있는 React Hook인 react-intersection-observer를 install하여 무한스크롤을 구현했다.

IntersectionObserver를 사용하는 방법이 일반적으로 Scroll event감지를 사용하는 방법보다 유리하다고 한다.

Scroll event를 사용해보신 분이라면 알겠지만, 스크롤 이벤트에 대한 실행 함수가 엄청난 횟수가 실행되기 때문에 일반적으로 debounce, throttle로 해당 호출 수를 제어하는 방법을 사용한다.
하지만, IntersectionObserver를 사용하는 방법은 해당 과정이 필요하지 않다.

또한 Scroll event에서는 현재 창의 높이값을 조사하기 위해 offsetTop을 사용하는데, 해당 값의 조사를 위해 새 layout을 매번 그리게 되는데...

layout을 새로 그린다는 것은 render tree를 재생성한다는 뜻이고, reflow라고 불리우는 해당 과정이 반복되면 웹 성능이 저하되고 화면의 버벅임이 유발한다.

따라서 일반적으로 IntersectionObserver을 사용한다.

IntersectionObserver mdn.

IntersectionObserver의 mdn에는 다음과 같이 정의되어 있다.

IntersectionObserver는 target element와 상위 element들의 viewport(현재 화면에 보이는 직사각형의 영역)가 교차되는 부분을 비동기적으로 관찰하는 API이다.

IntersectionObserver는 다음과같은 4가지 상황에서 보통 사용한다고 있다고 한다. (출처 mdn)

  1. 페이지 스크롤로 이미지에 대한 lazy-loading을 구현할 때

  2. Infinite scrolling을 통해 스크롤로 새로운 컨텐츠를 불러올 때

  3. 광고의 수익을 계산하기 위해 광고의 가시성을 참고할 때

  4. 사용자가 결과를 볼 것인지에 따라 애니메이션 동작 여부를 결정할 때

저희는 2번에 해당하는 무한 스크롤 구현을 react-intersection-ovserver을 통해 진행.

Install

npm install react-intersection-ovserver

라이브러리를 다운로드 합니다.


전체 코드

  • 무한 스크롤을 구현하는데에 있어서 필수적인 부분만 남겨두었습니다.
import {useInview} from 'react-intersection-observer';

// ... //

const MainMid = ({ step }) => {
  
  // ref가 viewport에 등장하면, inView는 true를 반환합니다. (그 외에는 false)
  const [ref, inView] = useInView();
  
  // useSWR을 통해 가져오는 주 데이터 값 
  const [honeytipList, setHoneytipList] = useState([]);
  const { honeytipList: honeytipListFromServer } = useGetHoneytipList({
    step: queryStep ?? '',
    page,
    sort,
    size,
  });
  
  useEffect(() => {
    if (!honeytipListFromServer) {
      return;
    }
    if (init) {
      return;
    }
    if (honeytipList.length === 0) {
      setHoneytipList([...honeytipListFromServer.content]);
      setInit(true);
    }
  }, [honeytipListFromServer, init]);

  useEffect(() => {
    if (!inView) {
      return;
    }
    setPage((prev) => prev + 1);
  }, [inView]);

  useEffect(() => {
    if (!honeytipListFromServer) {
      return;
    }
    if (page > 1) {
      setHoneytipList((prev) => [...prev, ...honeytipListFromServer.content]);
    }
  }, [page, honeytipListFromServer]);

  useEffect(() => {
    setHoneytipList([]);
    setPage(1);
    setInit(false);
    if (sort === 'id,desc') {
      setCurrentFilter('최신 순');
    }
  }, [step, sort]);

	return (
  	{/* ... */}
  	{honeytipList.length !== 0 && (
        <PostListWrapper>
          {honeytipList.map((post) => (
            <HoneytipCard key={post.id} {...post} />
          ))}
          {/* 스크롤을 감지할 하단부 컴포넌트를 만들어 ref삽입 */}
          <InfiniteScrollCheck ref={ref} />
        </PostListWrapper>
      )}
 	{/* ... */}
	)
}

관련되어 구현된 사이트의 링크를 첨부.

오즈의 제작소


문제 및 해결

1. 비동기 통신 useSWR과 useEffect의 충돌

페이지 구성 시, 초기 컨텐츠의 개수(최대 9개)만큼을 불러와야 했다.

useEffect를 통해 useSWR로 받아온 honeytipListFromServer의 data를 honeytipList 배열에 등록하는 과정이 필요했다.

그러나 비동기 통신인 useSWR과 useEffect의 순서가 보장되지 않았다.

contents가 전혀 없는 초기 화면에서, viewport에 ref가 등장하여 contents를 불러오는 함수를 무자비하게 호출하는 문제를 역이용하여 해결하였다.

    if (!honeytipListFromServer) {
      return;
    }
    if (init) {
      return;
    }
    if (honeytipList.length === 0) {
      setHoneytipList([...honeytipListFromServer.content]);
      setInit(true);
    }
  }, [honeytipListFromServer, init]);

반복되는 호출 중, honeytipListFromServer가 불러와진 때를 캐치하여 honeytipList 배열에 data값을 넣어준다.

이후 init이 true가 되면(초기 컨텐츠 불러와짐) 해당 useEffect는 동작하지 않는다.


2. 스크롤 초기화 문제

초기에는 honeytipList 배열 없이, useSWR의 data를 직접 사용하였는데, viewport에 ref가 진입할 때, 스크롤이 상단으로 땡겨졌다가 제자리로 가는 문제가 있었다.

현 honeytipList는 content가 추가되는 방식임과 달리, honeytipListFromServer는 key값들의 변경에 반응하여 재 호출될 때마다, 배열을 첫 번째 인자부터 재구성하기 때문에, 구현부(렌더링단)에서 부자연스러운 깜박임이 발생한다.

따라서 해당 문제를 해결하기 위해 content를 담을 임의의 배열을 사용하였다.


3. step과 sort에 대한 값 변경 시, 값 초기화

무한 스크롤이 구현된 해당 페이지에서는 card(step)의 변경과 contents에 대한 정렬이 존재하였다.

문제는 정렬이 변경되어 있는 상태에서 step을 변경할 경우, 해당 정렬방법으로 useSWR을 호출하기 위한 추가적인 로직이 필요했다.

이의 간소화를 위해 step변경 건에 대하여 모두 default로 조회수 순에 대한 정렬로 맞추기로 했다.

  useEffect(() => {
    setHoneytipList([]);
    setPage(1);
    setInit(false);
    if (sort === 'id,desc') {
      setCurrentFilter('최신 순');
    }
  }, [step, sort]);

step과 sort의 변경건에 대한 모든 작업이 이루어지는 부분이다.


참조

https://velog.io/@yejinh/Intersection-Observer%EB%A1%9C-%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

https://3000.tistory.com/32

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API

https://gist.github.com/wseungjin/6d19cdcdc8736c911198769ce853ae27

profile
FE developer

2개의 댓글

comment-user-thumbnail
2022년 5월 19일

글에 들어갔다가 뒤로가기 했을 때 보던 위치가 아닌 초기화가 되면 나중에 글이 많아 졌을 때 보던 위치를 찾기 어렵진않나요?

답글 달기
comment-user-thumbnail
2022년 5월 26일

스크롤 위치 기억하는 법을 구현하는 것도 좋겠네요

답글 달기