Kim-DaHam / Portfolly

Portfolly 프로젝트 리팩토링
1 stars 0 forks source link

useIntersectionObserver 초기 props로 상태값을 다루는 callback을 넘겨줬을 때, 상태가 업데이트 되지 않는 문제 #86

Closed Kim-DaHam closed 7 months ago

Kim-DaHam commented 7 months ago

Bug Report

개요

PortfolioList.tsx에서 무한스크롤을 구현하는 중에 발생한 문제입니다.

useIntersectionObserver 커스텀훅을 만들어서 초기값으로

가 observe 되었을 때 발생시킬 callback을 전달합니다.

그런데 callback 안에 부모 컴포넌트에서 관리하는 state를 다루는 로직이 포함됩니다.

부모 컴포넌트에서는 상태값이 변하지만, 초기값으로 전달된 callback 함수는 전달된 그 순간에서부터 내부 변수가 더이상 변하지 않습니다. 전달 받은 함수를 그대로 유지하고 다시 새롭게 생성되지 않기 때문입니다.


📸 Screenshots

https://github.com/Kim-DaHam/Portfolly/assets/81691456/32fdfeaa-f22e-4dcb-b00f-8c5fe5d5f0bf


💻 Code

const [showedPortfolioNum, setShowedPortfolioNum] = useState<number>(ITEMS_PER_SHOW);

const loadNextPart = () => {
  const allPortfoliosNum = portfolios ? portfolios.length : 0;
  const isOnePageLoaded = (showedPortfolioNum === allPortfoliosNum) ? true : false;
  const isLastPortfoliosLessThanPerShows = showedPortfolioNum + ITEMS_PER_SHOW > portfolios.length;

// 콘솔 테스트
  console.log('초기값으로 전달된 callback 함수: ', showedPortfolioNum)

  if(isOnePageLoaded && !hasNextPage) {
      setLoadData(false);
      return;
  }
  if(isOnePageLoaded && hasNextPage) {
      fetchNextPage();
      return;
  }
  if(isLastPortfoliosLessThanPerShows) {
      setShowedPortfolioNum(prev => prev + (portfolios.length - showedPortfolioNum));
      return;
  }
  setShowedPortfolioNum(prev => prev + ITEMS_PER_SHOW);
  };

  const setObservationTarget = useIntersectionObserver(loadNextPart);

// 콘솔 테스트
  useEffect(() => {
  console.log('부모 컴포넌트: ', showedPortfolioNum)
  }, [showedPortfolioNum]);

부모 컴포넌트에서 관리하는 showedPortfolioNum라는 state가 있습니다.

서버에서 데이터를 100개정도 받아온 다음 30개씩 끊어 보여주는 방식으로 무한스크롤을 만들 계획인데(한 페이지=100개, 한 파트=30개), showedPortfolioNum가 지금까지 보여준 데이터 개수를 의미합니다.

그래서 observer가 발동했을 때 바로 데이터 패칭을 하는 게 아닌, showedPortfolioNum 상태값을 조정해야 합니다.


🙁 Actual behavior

useIntersectionObserver 초기 props로 상태값을 다루는 callback을 넘겨줬을 때, 상태가 업데이트 되지 않는다.

🙂 Expected behavior

useIntersectionObserver 훅을 재사용 가능하게 최대한 더럽히지 않고, showedPortfolioNum state를 조작하고 싶습니다.


추가 사항

-

Kim-DaHam commented 7 months ago

해결 방법

new IntersectionObserver 객체에 들어가는 콜백 함수는 단순 동작만 하도록 만들었습니다.

const loadNextPage = () => {
  if(hasNextPage) {
    fetchNextPage();
  }
};

그리고 useSuspenseInfiniteQuerygetNextPageParam이 다음 조건에 따라 다음 페이지를 불러올 건지 말 건지 결정하기 때문에,

getNextPageParam: (lastPage: any, allPages: any) => {
  const nextPageNum = allPages.length + 1;
  return lastPage?.length < PAGE_PER_DATA ? null : nextPageNum;
},

아래 조건문은 중복 검사가 되기 때문에 제거하고,

if(isOnePageLoaded && !hasNextPage) {
  setLoadData(false);
  return;
  }

아래와 같이 useInfiniteQuery가 반환하는 hasNextPage boolean 값으로 observer

를 조건부 렌더링 했습니다.

// 원래 방식
{ loadData &&
  <S.ObserveDiv ref={setObservationTarget}></S.ObserveDiv>
}
// 바꾼 방식
{ hasNextPage &&
  <S.ObserveDiv ref={setObservationTarget}></S.ObserveDiv>
}

그리고 다음 페이지를 불러와서 portfolios가 바뀔 경우 useEffect를 발동시켜 showedPortfolioNum 을 조작해 화면에 보여주는 아이템 수를 업데이트 했습니다. showedPortfolioNum 라는 변수 네이밍이 너무 길고 별로라서 간단하게 showsNum 라고 바꿨습니다.

useEffect(() => {
  const isLastPortfoliosLessThanPerShows = showsNum + ITEMS_PER_SHOW > portfolios.length;

  if(isLastPortfoliosLessThanPerShows) {
      setShowsNum(prev => prev + (portfolios.length - showsNum));
      return;
  }
  setShowsNum(prev => prev + ITEMS_PER_SHOW);
}, [portfolios]);

그리고 반환되는 데이터는 pages: Array[] 형태이기 때문에, pages를 flat() 메서드로 1차원 배열로 풀어줬습니다.

return useSuspenseInfiniteQuery({
  queryKey: portfolioKeys.list(section, {
      type: filter.filterKey,
      value: filter.filterValue
  }),
  queryFn: getPortfolios,
  select: data => data.pages.flat(),
  initialPageParam: 1,
  getNextPageParam: (lastPage: any, allPages: any) => {
      const nextPageNum = allPages.length + 1;
      return lastPage?.length < PAGE_PER_DATA ? null : nextPageNum;
  },
  staleTime: Infinity,
  gcTime: Infinity,
});

아래 링크에선 flat() 메서드 보다 concat으로 잘라 붙이는 게 속도가 더 빠르다고 하는데.. 코드가 간단해 보이길 원하는데다 데이터 수가 그렇게 많지 않기 때문에 그냥 flat을 사용합니다.

중첩 배열을 1차원 배열로 푸는 방법

기타 궁금한 점

new IntersectionObserversetIsObserve(true) 를 넣어useEffect(void, [isObserve]) 조건부 실행을 시킨 다음 그 안에서 showedPortfolioNum를 조작하려고 했습니다.

그런데 초기 렌더링 2번 + new IntersectionObserver 객체가 생성된 직후 첫 콜백 실행을 제외하고는 setIsObserve(true)에 따라 useEffect가 발동하지 않았습니다.

그 원인은 추후 자세하게 알아봐야합니다.