infinity Scroll 적용기
2023-02-20
7 min read
DEVELOPMENT
이번 프로젝트에서 병원 검색이라는 서비스를 제공하기 위해서 약 7만 6천 개가량의 병원 데이터가 있었습니다. 검색어를 '병원'이라고 검색했 을 때는 모든 데이터를 보여주어야 하고 전체 데이터를 한 번에 넘겨줄 수 없어 분할 요청이 필요했습니다.
먼저 페이지별로 나눌까 무한 스크롤로 해야 할까 고민이 있었는데 서비스를 사용하는 데 있어 모바일 환경이 제일 잘 맞아 모바일 기기에서 사용하기 쉬운 스크롤을 적용하기로 했습니다
Intersection Observer도 사용해 본 적 없고 무한 스크롤을 적용해 본 적이 없어서 검색을 많이 해보았습니다.
해당 프로젝트에서는 백엔드도 같이 개발을 해야 했었기에 백엔드를 먼저 개발하였습니다
백엔드
백엔드는 Prisma와 PlanetScale을 사용하였습니다
필요한 데이터는 다음과 같았습니다
- 검색어
- 현재 페이지 수
현재 페이지 수에 * 10을 하여 10개씩만 보내 주었고 만약 10개의 데이터가 아니라면 마지막 페이지로 해두었습니다
프론트엔드
InterSection Observer을 사용하였습니다
간단한 사용방법을 알려드리자면
1
const options = {2
root: document.querySelector('#parent'),3
rootMargin: '0px'4
threshold: 1.05
}6
7
const IO = new IntersectionObserver(callback,options)
options를 먼저 알아보자면
root
의 경우 감시자를 설정한다고 보시면 됩니다 이는 대상 객체의 조상 요소여야만 합니다. 기본값은 브라우저 뷰포트입니다, root 값이 null이거나 지정되지 않았을 때 기본값으로 설정됩니다.
rootMargin
root가 가진 여백입니다 CSS의 Margin을 생각하시면 됩니다 root 요소의 bounding box를 축소, 증가를 하고, 교차성을 계산하기 전에 적용됩니다. 기본값은 0입니다
threshold
observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다
교차성이 될 요소가 50%만큼 보였을 때마다 콜백을 실행시키고 싶다면 0.5를 설정하면 됩니다 기본값은 0이고, 0은 1픽셀이라도 보이게 되면 실행시킨다는 것을 의미합니다 반대로 1은 전체가 보여야만 콜백을 실행한다는 뜻입니다
Callback
intersectionObserver 생성 시에 들어가는 콜백 함수에는 intersectionObserverEntry에 대한 참조를 입력으로 받습니다 자세히 보기 해당 정보를 모르고 있어서 고생했던 기억이 있네요
해당 링크에 intersectionObserverEntry에 대해서 자세히 나와있습니다 첫 번째 프로퍼티에는 intersectionObserverEntry 값이 들어오고 두 번째 프로퍼티에서는 생성한 observer 즉 io에 대한 값들이 들어옵니다
1
useEffect(() => {2
if (listRef.current === null) return;3
const io = new IntersectionObserver(4
async ([entry], observer) => {5
console.log(entry, observer);6
if (entry.isIntersecting) {7
observer.unobserve(entry.target);8
setPage(page + 1);9
observer.observe(entry.target);10
}11
},12
{ root: null, threshold: 1, rootMargin: "0px" },13
);14
io.observe(listRef.current as Element);15
return () => io.disconnect();16
}, [findState]);
처음 작성한 코드입니다
findState는 검색어로 input창에 입력시 값을 저장한 State입니다
하지만 이것은 검색어가 바뀔때마다 InterSectionObserver를 생성하고 타겟을 설정하는데 옳지 않다고 생각이 듭니다
이슈발생
1
const getTest = useCallback(async () => {2
setIsLoading(true);3
const result = await axios.get(`/api/users/my-hospitals/find?page=${page}&search=${search}`);4
if (result.data.status) return;5
setFindState((current: any) => {6
const array = [...current];7
if (array) {8
array.push(...result.data.foundHospital);9
}10
return [...new Set(array)];11
});12
setIsLoading(false);13
}, [search, page]);
해당 함수는 검색어와 페이지를 보내 값을 받아오는 함수입니다
IntersectionObserver의 콜백 함수로 지정하였는데 해당 함수로는 문제가 있었습니다
요청을 해도 현재 검색어와 페이지 넘버가 넘어가지 않았습니다. 확인해 보니 값들은 undefined로 들어가고 있었고 이는 State 문제라고 판단이 되었습니다 SetState는 동기적으로 작동하지 않고 비동기적으로 작동하기에 값이 바뀌기도 전에 요청을 보내기에 해당 오류가 발생한 것으로 보입니다
해결
State가 할당되기 전에 요청이 간다면 State 값이 바뀔 때 요청을 보내야 될 거 같아 useEffect를 이용하여 search, page가 바뀔 때마다 요청을 보내도록 하여 해결을 하였습니다.
refactor
해당 로직을 재사용성을 높이고 분리하기로 했습니다
custom hook으로 useIO를 만들게 되었습니다
ref로 하지 않고 왜 State로 했냐 궁금하시다면
useEffect에 dependency에 추가하기 위해서입니다 ref로 하게 되면 변경이 된 건지 알 수 없기 때문입니다.
Target할 요소를 만들기 위해 useState로 값을 만들어 주고
useEffect를 통해 IntersectionObserver를 생성해 주었습니다
그리고 Target 설정을 위해 SetTarget을 반환해 줍니다
만약 마지막 페이지라면 unObserve를 통해 더 이상 추적하지 않고 아니라면 callback을 실행시킵니다