ootdzip
는 이미지 위주의 서비스이다. 그만큼 이미지 로드 속도가 중요한데, 초기로드 속도를 늘리기 위한 방법중 하나인 인피니티 스크롤을 구현 해보았다. 무한 스크롤
은 사용자가 페이지 하단에 도착했을 때, 콘텐츠를 더 불러와 기존의 정보에 덧붙이는 기술이다. 오늘은 스크롤 이벤트를 활용해 무한스크롤을 구현한 과정에 대해 작성해보겠다.
ootdzip
의 ootd 탐색 컴포넌트는 현재 20개의 사진이 존재한다. 해당 사진들을 한번에 불러오는 api와 10개씩 끊어서 불러오는 api의 응답 속도를 보자.
사진을 10개 가져오는 경우
사진을 20개 가져오는 경우
당연하게도 적게 가져오는 경우의 응답 속도가 빨랐다. 어차피 휴대폰 크기는 제한되어있고, 유저는 처음부터 많은 정보를 받을 필요가 없다. 더 보고자 한다면 그때 더 주면된다.
ootdzip
은 어플이다. 페이지네이션
이라는 비슷한 기능을 제공하는 방법도 있지만 페이지네이션
은 버튼 클릭 이벤트에 기반하고, 무한 스크롤
은 스크롤 이벤트에 기반한다. 모바일에선 유저가 클릭하는 이벤트보다 스크롤을 하는게 유저 경험이 좋을 것 같아 무한 스크롤을 선택하게 되었다.
const [page, setPage] = useState<number>(initialPage ? initialPage : 0); // 페이지 번호
const [data, setData] = useState(initialData); // 데이터
const [hasNextPage, setHasNextPage] = useState<Boolean>(false); // 마지막 페이지 여부
const [isLoading, setIsLoading] = useState<Boolean>(false); // 데이터 패칭 중 여부
const [total, setTotal] = useState<number>(0); // 해당 데이터의 총 개수
//containerRef를 통한 특정 컴포넌트의 스크롤 감지를
const containerRef = useRef<any>(null);
const handleScroll = () => {
const container = containerRef.current;
if (!container) return;
const { scrollTop, clientHeight, scrollHeight } = container;
// 스크롤이 맨 아래에 닿았으면 isLoading을 true로 변경
if (scrollTop + clientHeight >= scrollHeight && !isLoading) {
setIsLoading(true);
}
};
useEffect(() => {
const container = containerRef.current;
if (!container) return;
container.addEventListener('scroll', handleScroll);
return () => {
container.removeEventListener('scroll', handleScroll);
};
}, [containerRef.current]);
useEffect(() => {
if (!router.isReady) return;
fetchDataFunction(page, size).then((result: any) => {
if (!result) return;
// 초기 페이지가 있다면, 기존 데이터에 덧 붙이기
if (initialPage) {
setData(() => [...initialData, ...result.content]);
} else {
setData(result.content);
}
setHasNextPage(!result.isLast);
// 초기 페이지가 있다면 initial page + 1로 변경
if (initialPage) {
if (result.content.length > 0) {
setPage(initialPage + 1);
}
} else { // 그렇지 않다면 1로 변경
setPage(1);
}
setIsLoading(false);
//toatl 개수가 있다면 업데이트
if (result.total) setTotal(result.total);
});
}, [router.isReady]);
hasNextPage
와 isLoading
모두 true를 만족할 때, 추가 데이터를 가져온다.
useEffect(() => {
if (!hasNextPage || !isLoading) return;
fetchDataFunction(page, size).then((result: any) => {
setHasNextPage(!result.isLast);
setData((prevData: any) => [...prevData, ...result.content]);
setPage((prevPage) => prevPage + 1);
setIsLoading(false);
});
}, [isLoading]);
정렬 순서가 변하는 등의 조건이 발생했을 때, 데이터를 모두 지우고 0번 페이지의 정보만 가져온채로 초기화한다.
const reset = async () => {
fetchDataFunction(0, size).then((result: any) => {
setData(result.content);
setHasNextPage(!result.isLast);
setPage(1);
setIsLoading(false);
if (result.total) setTotal(result.total);
});
};
ClothList 컴포넌트의 스크롤을 감지해, 스크롤이 맨 아래 닿을 때 마다 12개의 데이터를 추가로 가져온다. 데이터를 가져오는 동안엔 Spinner 컴포넌트가 동작한다.
const {
data: OOTDData,
isLoading: OOTDIsLoading,
containerRef: OOTDRef,
hasNextPage: OOTDHasNextPage,
reset: ootdReset,
} = useInfiniteScroll({
fetchDataFunction: fetchOOTDDataFunction,
size: 12,
initialData: [],
initialPage: 0,
});
return (
<S.ClothList ref={OOTDRef}>
<ImageList
onClick={onClickImageList}
data={OOTDList.map((item) => {
return { ootdId: item.id, ootdImage: item.imageUrl };
})}
type={'column'}
/>
{OOTDIsLoading && OOTDHasNextPage && <Spinner />}
</S.ClothList>
)
무한 스크롤을 구현해 보았는데, 유저 경험상 확실히 좋아보였다! 다만 리스트들을 보다가 상세 보기 화면으로 이동후 돌아오게 되면 다시 처음부터 로드 된다는게 불편했다. 조만간 고쳐야겠다는 생각이 들었다.