사이드 프로젝트 개발 과정 - (무한 스크롤 구현)

knh6269·2024년 6월 15일
1

ootd.zip

목록 보기
12/16
post-thumbnail

도입

ootdzip는 이미지 위주의 서비스이다. 그만큼 이미지 로드 속도가 중요한데, 초기로드 속도를 늘리기 위한 방법중 하나인 인피니티 스크롤을 구현 해보았다. 무한 스크롤은 사용자가 페이지 하단에 도착했을 때, 콘텐츠를 더 불러와 기존의 정보에 덧붙이는 기술이다. 오늘은 스크롤 이벤트를 활용해 무한스크롤을 구현한 과정에 대해 작성해보겠다.


무한 스크롤을 선택한 이유

초기 로드 속도 향상

ootdzip의 ootd 탐색 컴포넌트는 현재 20개의 사진이 존재한다. 해당 사진들을 한번에 불러오는 api와 10개씩 끊어서 불러오는 api의 응답 속도를 보자.

사진을 10개 가져오는 경우

사진을 20개 가져오는 경우

당연하게도 적게 가져오는 경우의 응답 속도가 빨랐다. 어차피 휴대폰 크기는 제한되어있고, 유저는 처음부터 많은 정보를 받을 필요가 없다. 더 보고자 한다면 그때 더 주면된다.

모바일이라는 환경

ootdzip은 어플이다. 페이지네이션이라는 비슷한 기능을 제공하는 방법도 있지만 페이지네이션버튼 클릭 이벤트에 기반하고, 무한 스크롤스크롤 이벤트에 기반한다. 모바일에선 유저가 클릭하는 이벤트보다 스크롤을 하는게 유저 경험이 좋을 것 같아 무한 스크롤을 선택하게 되었다.


무한 스크롤 구현

요구사항

  • 무한 스크롤은 특정 컴포넌트의 맨 아래까지 스크롤 되면 발동한다.
  • 무한 스크롤은 다음 페이지의 값이 없다면 작동하지 않는다.
  • 무한 스크롤은 특정 상태의 변화에 따라 리셋될 수 있다.
  • 무한 스크롤은 초기 페이지, 가져올 데이터 크기, 초기 데이터를 정할 수 있다.

useInfiniteScroll 훅 구현

props

  • fetchDataFunction: 데이터 fetch 함수
  • initialData: 초기 데이터
  • size: 가져올 데이터 개수
  • initialPage: 초기 페이지 번호

state

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); // 해당 데이터의 총 개수

scroll event

//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]);

추가 데이터 로드

hasNextPageisLoading 모두 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>
)

완성 화면


정리

무한 스크롤을 구현해 보았는데, 유저 경험상 확실히 좋아보였다! 다만 리스트들을 보다가 상세 보기 화면으로 이동후 돌아오게 되면 다시 처음부터 로드 된다는게 불편했다. 조만간 고쳐야겠다는 생각이 들었다.

0개의 댓글