[리액트] 무한스크롤 Intersection Observer

임승민·2022년 11월 3일
0

React 기능구현

목록 보기
4/6
post-thumbnail

왜 Intersection Observer인가?

무한 스크롤 기능을 구현하기 위해 scroll 이벤트를 사용하려고 했지만 scroll 이벤트의 몇가지 문제점이 있다.

  • 1px당 하나의 스크롤 이벤트가 발생해서 스크롤 한번에 수많은 이벤트가 발생한다.
  • clientHeight, scrollTop, scrollHeight을 사용해 무한 스크롤 기능을 구현한다.
    따라서 정확한 값을 가져오기 위해 브라우저는 웹 페이지의 일부 혹은 전체를 다시 그린다.

의미없는 이벤트 발생을 막기위해 setTimeout을 이용해서 스크롤 시 설정한 시간마다 이벤트를 발생시키게 한다. 하지만 이 방법도 콜스택이 비워지지 않아 설정한 시간보다 늦게 동작할 수도 있다.

그럼 어떻게 해야 효율적으로 무한 스크롤을 구현할 수 있을까?

바로! Intersection Observer를 이용해 선택한 요소에 특정한 변화가 발생할 때를 알 수 있다.

Intersection Observer

우선 관촬할 요소를 설정해 줘야 한다. 목록의 제일 아래에 위치해야 하기 때문에 목록 아래에 div 하나를 두었다. ref를 통해 해당 div의 dom에 접근하고 setState로 값에 변화가 발생하면 바로바로 알 수 있게했다.

// list.tsx
		<MainLayout>
			{issueList?.map((issueItem: any, index: number) => {	
				<Issue key={index} issueItem={issueItem} userItem={newUserList[index]} />
			})}
		<div ref={setTarget}>{loadingMessage}</div>
		</MainLayout>

전체 코드

// InfiniteScroll.tsx
import { useEffect, useState } from 'react';
import { getIssues } from '../../apis/Issue';

export default function useInfinityScroll() {
	const [issueList, setIssueList] = useState([]);
	const [target, setTarget] = useState('');
	const [page, setPage] = useState(0);
	const defaultPage = 8;
	const isLastIssue = issueList.length < page - defaultPage;
	const loadingMessage = isLastIssue ? '' : '로딩 중∙∙∙';

	const getList = () => {
		getIssues(page).then((data) => {
			setIssueList(data);
		});
	};

	const onIntersect = ([entry]) => {
		if (entry.isIntersecting) {
			setPage((prev => prev + defaultPage);
		}
	};

	useEffect(() => {
		let observer;
		if (target) {
			observer = new IntersectionObserver(onIntersect, { threshold: 1 });
			observer.observe(target);
		}
		return () => observer?.disconnect();
	}, [target]);

	useEffect(() => {
		!isLastIssue && getList();
	}, [page]);

	return { issueList, setTarget, loadingMessage };
}

전체 코드는 이러하고 하나씩 천천히 설명하겠다.


변수 설명

const [issueList, setIssueList] = useState([]);
const [target, setTarget] = useState('');
const [page, setPage] = useState(0);
const defaultPage = 8;
const isLastIssue = issueList.length < page - defaultPage;
const loadingMessage = isLastIssue ? '' : '로딩 중∙∙∙';

issueList: 받은 data를 관리하는 state

target: 목록 마지막 div state

page: 요청할 목록 수

defaultPage: 한번 요청시 추가할 목록 개수

isLastIssue: 실제 목록 갯수와 요청한 목록 갯수가 맞지 않을때

  • page의 경우 issueList보다 먼저 발생하기 때문에 data를 받기 전까지 defaultPage만큼 더 많다.
  • 더이상 불러올 data가 없는데 마지막에 닿게하면 또 감지하여 요청을 보내기 때문에 불 필요한 요청이 계속된다.
  • 따라서 현재 목록 보다 요청하는 목록수가 더 많다면 요청을 중지한다.

loadingMessage: 마지막 단에서 보여줄 text


IntersectionObserver 생성

useEffect(() => {
  let observer;
  if (target) {
    observer = new IntersectionObserver(onIntersect, { threshold: 1 });
    observer.observe(target);
  }
  return () => observer?.disconnect();
}, [target]);
  • 마지막 div 즉 target이 바닥에 닿으면 해당 코드를 실행한다.
  • target이 true면 IntersectionObserver를 선언한다. .observe(target) 으로 인자속 대상의 관찰을 시작한다. 여기서의 true는 관찰 대상의 교차 상태가 보이는 경우를 말한다.
  • IntersectionObserver의 첫번째 인자는 관찰 시작시 실행할 함수, 두번째 인자는 관찰이 시작될 때의 옵션인데 기본값이 있어 넣지 않아도 된다.
  • if문 안의 함수가 실행되고 바로 .disconnect()로 관찰을 종료한다.

option 기본값

const option = {
	root: null,
  rootMargin: '0px',
  threshold: 0
}

교차시 함수 실행

괄찰이 시작되면 onIntersect 함수가 실행된다. IntersectionObserver로 부터 받은 인자 entry객체의 isIntersecting에 접근하면 관찰 대상의 교차 상태를 boolean 값으로 반환해 준다.

const onIntersect = ([entry]) => {
	if (entry.isIntersecting) {
		setPage((prev) => prev + defaultPage);
	}
};

교차가 되었다면 page 값에 defaultPage를 더해서 업데이트 해준다.

page값이 바뀌면 useEffect안의 코드가 실행된다.

isLastIssue가 false면 즉, 마지막 페이지가 아니면 getList함수를 실행한다.

useEffect(() => {
	!isLastIssue && getList();
}, [page]);

data 요청

마지막으로 원하는 목록 수인 page를 쿼리스트링에 넣고 응답값으로 IssueList를 업데이트 해준다.

const getList = () => {
	getIssues(page).then((data) => {
		setIssueList(data);
	});
};

마치며

처음 구현해 보기도 하고 짧은 시간안에 만들어야 했다보니 정확한 이해가 뒷받침 되어 주지 못해서 좋은 코드는 아니라고 생각된다. 하지만 다음에 또 구현해 볼 기회가 생긴다면 정말 잘 구현해 보고싶다. 몇시간 밖에 없어서 너무 아쉬운 마음 뿐이다...

0개의 댓글