React 무한 스크롤 두가지 방법으로 구현

juno·2022년 9월 21일
0
post-thumbnail

React 무한 스크롤 두 가지 방법으로 구현해 보았다.

1. window.innerHeight + window.scrollY >= document.body.offsetHeight

2. IntersectionObserver

JSONPlaceholder 의 api로 테스트
https://jsonplaceholder.typicode.com/posts
100개의 데이터가 있고 쿼리스트링으로 접근하면 start = 몇번부터 limit= 몇개 나눠서 데이터를 가져올 수 있다.

window.innerHeight + window.scrollY >= document.body.offsetHeight

key point

  • useRef로 리렌더링을 일으키지않고 상태값 유지

  • 이벤트 발생 시점에 fetch가 데이터를 받아오는동안 onScroll 이벤트발생을 막을 toggle 사용

const InfiniteScroll = () => {
  const [feed, setFeed] = useState([]);
  const add = useRef(0);
  const [toggle, setToggle] = useState(true);

  const onScroll = () => {
    if (
      window.innerHeight + window.scrollY >= document.body.offsetHeight &&
      toggle
    ) {
      add.current += 10;
      setToggle(prev => !prev); --> 추가 이벤트가 발생되지 않도록 false 전환

      fetch(
        `https://jsonplaceholder.typicode.com/
					posts?_start=${add.current}&_limit=10`
      )
        .then(response => response.json())
        .then(data => {
          setFeed(prev => [...prev, ...data]);
          setToggle(prev => !prev);   
					 // fetch로 받아온 데이터 처리를 다 마치고 true 전환
        });
    }
  };

  useEffect(() => {
    fetch(`https://jsonplaceholder.typicode.com/posts?_start=0&_limit=10`)
      .then(response => response.json())
      .then(setFeed);
  }, []);


  useEffect(() => {
    window.addEventListener('scroll', onScroll);
    return () => window.removeEventListener('scroll', onScroll);
  });

  return feed?.map(({ id, title, body }, idx) => (
    <div key={idx}>
      <div>{id}</div>
      <h1> {title}</h1>
      <div>{body}</div>
    </div>
  ));
};

소스 코드입니다. 복사해서 확인해보시면 글로 설명하는것보다 더 쉽게 이해될 것입니다.

https://github.com/kimjuno97/React-Skill-Repository/blob/master/src/무한스크롤/InfiniteScroll.js

IntersectionObserver

참고 자료
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

https://flamingotiger.github.io/frontend/react/intersection-observer-api/
-> 자세한 설명은 여기서 보세요 잘 나와있습니다. 해당 자료를 참고하여 데이터를 fetch로 받아 올때로
적용해 보았습니다.

key point

  • map 함수를 실행하고 마지막 인덱스 데이터에 useRef로 타겟해야되는데,
    해당 동작은 뒤 늦게 작동된다. → toggle을 이용해 랜더링을 일으킨다.
  • intersection observer을 사용해도 fetch가 비동기 처리되는동안 추가 이벤트를 막아주는 toggle이 필요하다.
  • threshold 값을 조절하여 데이터 받아오는 시점을 제어 한다.

    export default function Observer() {
     const [datas, setData] = useState([]);
     const [toggle, setToggle] = useState(false);
    
     const add = useRef(0);
     const viewport = useRef(null); // section 그자체 가져옴
     const target = useRef(null); // 참조가 늦어짐. console 찍으면 null
    
     const loadItems = () => {
       console.log('한번만 발동');
       add.current += 10;
       setToggle(prev => !prev);
       fetch(
         `https://jsonplaceholder.typicode.com/posts?_start=${add.current}&_limit=10`
       )
         .then(response => response.json())
         .then(data => {
           setData(prev => [...prev, ...data]);
           setToggle(prev => !prev); // fetch로 받아온 데이터 처리를 다 마치고 true 전환
         });
     };
    
     useEffect(() => {
       console.log('이펙트발생');
       const options = {
         root: viewport.current,
         threshold: 0.5,
       };
    
       const handleIntersection = (entries, observer) => {
         entries.forEach(entry => {
           if (!entry.isIntersecting) {
             return;
           }
           if (toggle) loadItems();
           observer.unobserve(entry.target);
           observer.observe(target.current);
         });
       };
    
       const io = new IntersectionObserver(handleIntersection, options);
    
       if (target.current) io.observe(target.current);
    
       return () => io && io.disconnect();
     }, [viewport, target, toggle]);
    
     useEffect(() => {
       fetch(`https://jsonplaceholder.typicode.com/posts?_start=0&_limit=10`)
         .then(response => response.json())
         .then(data => {
           setData(data);
           setToggle(prev => !prev); 
    					// target 참조가 처음에 바로 안되서 위의 useEffect가 발생하지않는다.
    					// 리렌더링 시킨다.
         });
     }, []);
    
     return (
       <div className="wrapper">
         <section className="card-grid" id="target-root" ref={viewport}>
           {datas?.map(({ title, body, id }, index) => {
             const lastEl = index === datas.length - 1;
             return (
               <div
                 key={index}
                 className={`card ${lastEl && 'last'}`}
                 ref={lastEl ? target : null}>
                 <div>{id}</div>
                 <h1>{title}</h1>
                 <p>{body}</p>
               </div>
             );
           })}
         </section>
       </div>
     );
    }

css파일

.wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
}

.card-grid {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  height: 350px;
  border: 1px solid black;
  overflow: auto;
}

.card {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  border: 2px solid black;
  width: 50%;
  padding: 40px 20px;
  margin: 20px;
  font-weight: bold;
}

.last {
  background-color: purple;
  color: white;
}

p {
  margin: 5px;
}

소스코드입니다. 복사해서 확인해보시면 글로 설명하는것보다 더 쉽게 이해될 것입니다.
https://github.com/kimjuno97/React-Skill-Repository/blob/master/src/무한스크롤/Observer.js

참고용 글이라 설명이 없는 점 양해 바랍니다. 질문 주시면 답변 꼭 해드리겠습니다.

IntersectionObserver 2022 12/02 추가

IntersectionObserver를 많이 사용하게 되면서 웹에서 한번만 실행되면 된다는 것을 깨달았다.

방법은 2가지만 지키면된다.

  1. 데이터가 추가될때마다 Ref는 제일 아래로 밀려가는 위치에 넣는다.
  2. useEffect에서 실행되는 Observer는 의존성배열을 빈배열로 놓고, 딱 1번만 실행되게 한다.

위의 방법보다 아래의 방법을 쓰는 것이 훨씬 최적화에 좋은 코드 입니다.

export default function InfiniteScroll() {
	const [feed, setFeed] = useState<TypeFeed[]>([]);
	const target = useRef<HTMLDivElement | null>(null);
	console.log('feed 증가 확인', feed);
	useEffect(() => {
		let io: IntersectionObserver | null = null;
		if (target.current) {
			io = new IntersectionObserver(entries => {
				entries.forEach(entry => {
					if (entry.isIntersecting) {
						setTimeout(() => {
							setFeed((prev: TypeFeed[]) => [...prev, ...data]);
						}, 500);
					}
				});
			});
			io.observe(target.current);
		}
	}, []);

	if (feed.length === 0) {
		return (
			<Container>
				<Loading>Loading...</Loading>
				<div ref={target} />
			</Container>
		);
	}
	return (
		<div>
			{feed.map(({ img }, idx) => {
				return <ImgBox key={idx} img={img} />;
			})}
			<div ref={target} /> // Ref를 컨텐츠 제일 아래에
		</div>
	);
}
profile
안녕하세요 인터랙션한 웹 개발을 지향하는 프론트엔드 개발자 입니다. https://kimjunho97.tistory.com => 블로그 이전 중

0개의 댓글