티스토리 뷰

미니쇼핑몰🎁 프로젝트에서 상품리스트/북마크 리스트 컴포넌트를 만드는데
무한스크롤과 카테고리 필터 기능을 함께 구현하면서 useEffect사용이 많아졌습니다.

useSelector로 전역상태인 상품리스트를 가져오고 이를 의존하여 다른 상태들을 useState로 사용하는데,
useSelector는 렌더링 후에 실행되고, useState는 렌더링 전에 실행되기 때문에 useEffect로 상태를 동기적으로 업데이트 해주어야 했습니다.

그런데 이는 불필요하게 렌더링이 많아지는 문제가 있었습니다.
필터 또는 리스트 상태가 바뀌면 3번씩 렌더링되고, 무한스크롤시 2번씩, 새로고침을 하면 아래 사진과 같이 9번씩 리렌더링됐습니다.

새로고침했을 때 컴포넌트가 9번 렌더링된다.

 

초기 코드는 아래와 같습니다.

function List() {
  const DATA_PER_PAGE = 30;

  const productList = useSelector(state => state.productList);
  
  const [currentFilter, setCurrentFilter] = useState('All');
  const [filteredList, setFilteredList] = useState([]);
  const [currentIndex, setCurrentIndex] = useState(DATA_PER_PAGE);
  const [currentList, setCurrentList] = useState([]);
  const [isEnd, setIsEnd] = useState(false);
  
  const handleScroll = () => {
    const scrollHeight = document.documentElement.scrollHeight;
    const scrollTop = document.documentElement.scrollTop;
    const clientHeight = document.documentElement.clientHeight;
    
    if (scrollTop + clientHeight >= scrollHeight) {
      setIsEnd(true);
    } // 스크롤 끝점 감지시 isEnd 상태 업데이트
  }; // 스크롤 이벤트 핸들러 함수
  
  const addNextData = () => {
    if(isEnd) {
      setCurrentList([...currentList, ...filteredList.slice(currentIndex, currentIndex + DATA_PER_PAGE)]);
      setCurrentIndex(currentIndex + DATA_PER_PAGE);
      setIsEnd(false)
    }
  } // 무한스크롤시 데이터 추가하는 함수
  
  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
  }, []); // 컴포넌트 초기렌더링시 스크롤이벤트 등록 및 언마운트시 해제
  
  useEffect(() => {
    setFilteredList(productList.filter(product => product.type === currentFilter))
  }, [productList, currentFilter]); // productList 업데이트시에도 현재 필터 유지, 필터 바뀌면 filteredList 업데이트

  useEffect(() => {
    setCurrentList(filteredList.slice(0, DATA_PER_PAGE))
  }, [filteredList]); // // 필터 바뀌면 무한스크롤 안된 상태 데이터 30개로 리셋
  
  useEffect(() => {
    addNextData()
  }, [isEnd]) // 무한스크롤 감지시 addNextData함수 호출
  
  const handleFilterClick = (type) => {
    setCurrentFilter(type);
    setCurrentIndex(DATA_PER_PAGE);
  }; // 필터 클릭시 필터 상태 변경, 인덱스 상태 초기화
  
  return ( ... )
}

 

코드를 읽어보며 잘 생각해보았습니다.
'데이터'라면 유지되어야 하니, 또는 변동되면 리렌더링시키기 위해서 무조건 useState를 사용했는데, 진짜 유지할 필요가 있는 데이터인가? 유지를 시키기 위해 useEffect를 일부러 사용해주어야 하는데 다른 방법은 없을까?

useEffect를 줄이기 위해 상태를 없애기로 했습니다.
실제로 currentList는 필터가 변동되거나, 무한스크롤이 발생하거나, 상품리스트가 업데이트될 때 등 항상 변경되는 값이라서 유지시킬 필요가 없습니다. 
렌더링마다 변수에 할당되도록 변경했습니다.

filteredList는 조금 다릅니다.
무한스크롤시에는 같은 필터에 머물러있기 때문에 유지할 필요도 있었습니다.
그러나 useEffect를 사용하며 리렌더링을 발생시키기보다
currentList와 같은 방식으로 변수에 할당을 하지만 useMemo를 사용하여 메모리에 저장되어있는 값과 비교하고 의존하는 값이 변동이 되었을 때에만 filteredList를 업데이트하도록 했습니다.
productList와 currentFilter가 변동되었을 때에만 filteredList가 업데이트되어 무한스크롤시에는 실행되지 않습니다.

다음은 리팩토링한 코드입니다.

function List() {
  const DATA_PER_PAGE = 30;

  const [currentFilter, setCurrentFilter] = useState(Types.ALL);
  const [currentIndex, setCurrentIndex] = useState(DATA_PER_PAGE);

  const getFilteredList = () => {
    if (currentFilter === Types.ALL) return baseList;
    else return baseList.filter((product) => product.type === currentFilter);
  };

  const filteredList = useMemo(() => getFilteredList(), [baseList, currentFilter]); // 스크롤 시 memo 필요

  let currentList = filteredList.slice(0, currentIndex); // 필터 또는 스크롤 시 항상 업데이트 되어 memo 불필요

  const handleScroll = () => {
    const scrollHeight = document.documentElement.scrollHeight;
    const scrollTop = document.documentElement.scrollTop;
    const clientHeight = document.documentElement.clientHeight;

    if (scrollTop + clientHeight >= scrollHeight) {
      setCurrentIndex((prev) => prev + DATA_PER_PAGE);
    }
  };

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);

  const handleFilterClick = (type) => {
    setCurrentFilter(type);
    setCurrentIndex(DATA_PER_PAGE);
  };

  return (...)
  }

 

리팩토링 후, useEffect는 4개에서 1개로 줄었습니다.
렌더링은 필터 또는 리스트 상태 업데이트시 1번씩, 무한스크롤시 1번씩, 새로고침시 3번씩(useEffect(()=>{}, [])으로 인해) 됩니다. 👍

 

다음은 매우 도움이 되었던 참고자료입니다. 

https://velog.io/@jay/you-might-need-useEffect-diet

https://velog.io/@jun17114/useEffect-%EC%9E%98-%EC%95%8C%EA%B3%A0-%EC%93%B0%EA%B3%A0-%EC%9E%88%EC%9D%84%EA%B9%8C

 

 

반응형
댓글