무한 스크롤 기능을 구현하기 위해 scroll 이벤트를 사용하려고 했지만 scroll 이벤트의 몇가지 문제점이 있다.
의미없는 이벤트 발생을 막기위해 setTimeout을 이용해서 스크롤 시 설정한 시간마다 이벤트를 발생시키게 한다. 하지만 이 방법도 콜스택이 비워지지 않아 설정한 시간보다 늦게 동작할 수도 있다.
그럼 어떻게 해야 효율적으로 무한 스크롤을 구현할 수 있을까?
바로! 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
: 실제 목록 갯수와 요청한 목록 갯수가 맞지 않을때
loadingMessage
: 마지막 단에서 보여줄 text
useEffect(() => {
let observer;
if (target) {
observer = new IntersectionObserver(onIntersect, { threshold: 1 });
observer.observe(target);
}
return () => observer?.disconnect();
}, [target]);
.observe(target)
으로 인자속 대상의 관찰을 시작한다. 여기서의 true는 관찰 대상의 교차 상태가 보이는 경우를 말한다..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]);
마지막으로 원하는 목록 수인 page를 쿼리스트링에 넣고 응답값으로 IssueList를 업데이트 해준다.
const getList = () => {
getIssues(page).then((data) => {
setIssueList(data);
});
};
처음 구현해 보기도 하고 짧은 시간안에 만들어야 했다보니 정확한 이해가 뒷받침 되어 주지 못해서 좋은 코드는 아니라고 생각된다. 하지만 다음에 또 구현해 볼 기회가 생긴다면 정말 잘 구현해 보고싶다. 몇시간 밖에 없어서 너무 아쉬운 마음 뿐이다...