[React] throttle 사용하여 scroll 이벤트 최적화

이수빈·2022년 4월 24일
3
post-thumbnail

💡 리액트에서 직접적인 DOM 조작


진행중인 프로젝트에서 스크롤을 내리면 최상단에 있는 네비게이션바를 숨기고, 올리면 나타나게 하는 기능을 구현중이였다.
그래서 네비게이션바를 getElementsByClassName 메소드를 사용해 불러오고 이전 스크롤 양과 비교해 방향을 감지하는 함수를 만든 뒤, 그 함수 안에서 classList.add 메소드로 css 속성을 바꾸는 로직으로 코드를 작성했다.
  let beforeScrollY = 0; //이전 스크롤 초기값
  let menu = document.getElementsByClassName("main__first");

  useEffect(() => {
    window.addEventListener("scroll", scrollDirection);
    if (token) {
      setIsLoggedIn(true);
    } else {
      setIsLoggedIn(false);
    }
  }, []);

 const scrollDirection = () => {
    //이전 스크롤된 양과 현재 스크롤된 양 비교하여 방향 감지
    if (document.documentElement.scrollTop > beforeScrollY) {
      //아래방향
      menu[0].classList.add("hidden");
    } else {
      menu[0].classList.remove("hidden"); //위방향
    }
    beforeScrollY = document.documentElement.scrollTop; //직전 스크롤양 저장
  }
return(
  <div className="main__first">

위와 같이 작성하게 되면 DOM 요소에 직접적으로 액세스해 클래스를 추가하기 때문에 코드가 커지고 복잡해지게 되면 DOM과 React의 상태를 구분하기 어렵고 테스트, 디버그하기 어려워져 좋은 코드가 될 수 없다.


💡 useState 변수 이용


참조한 글을 바탕으로 스크롤 방향을 useState 변수로 저장해 조건부로 css를 적용하는 방법으로 코드를 재작성해 보았다.
  const [isNavOn, setIsNavOn] = useState(true);
  let beforeScrollY = 0; //이전 스크롤 초기값

  useEffect(() => {
    window.addEventListener("scroll", scrollDirection);
    if (token) {
      setIsLoggedIn(true);
    } else {
      setIsLoggedIn(false);
    }
  }, []);

  const scrollDirection = () => {
      if (window.pageYOffset > beforeScrollY) {
          setIsNavOn(false);
          console.log("켜기");
        } else {
          setIsNavOn(true);
          console.log("끄기");
        }
        //이전 스크롤값 저장
        beforeScrollY = window.pageYOffset;
  }
return(
  <div className={isNavOn ? "main__first" : "main__first hidden"}>

정상적으로 작동하긴 하지만 스크롤을 살짝만 움직여도 콘솔에 찍히게 둔 스크롤 이벤트가 엄청나게 찍히는걸 볼 수 있었다. 수정한 코드도 비효율적인 코드라고 결론을 내렸고 이벤트를 줄일 수 있는 방법을 검색해보다 throttle을 이용하면 해결이 가능하다는 글을 보았다.

💡 throttle


throttle은 함수가 한 번 호출되면 지정된 시간 안에 여러번 실행되지 않도록 막아주는 것이다. 함수 이름 수정과 함께 코드를 수정해 보았다.

const [isNavOn, setIsNavOn] = useState(true);
  //이전 스크롤 초기값
  let beforeScrollY = 0;

  useEffect(() => {
    window.addEventListener("scroll", scrollEvent);
    if (token) {
      setIsLoggedIn(true);
    } else {
      setIsLoggedIn(false);
    }
  }, [isLoggedIn, setIsLoggedIn, token]);

  const scrollEvent = useMemo(
    () =>
      throttle(() => {
        if (window.pageYOffset > beforeScrollY) {
          setIsNavOn(false);
          console.log("켜기");
        } else {
          setIsNavOn(true);
          console.log("끄기");
        }
        //이전 스크롤값 저장
        beforeScrollY = window.pageYOffset;
      }, 300),
    [isNavOn]
  );
return(
  <div className={isNavOn ? "main__first" : "main__first hidden"}>

useMemo를 이용해 scrollEvent 함수를 메모이제이션 하고 원하는 throttle 함수 호출 시간을 설정해 준다. 그리고 useState로 선언한 변수를 의존 배열에 넣어준 뒤, 이벤트리스너의 콜백 함수로 넣어주었다. 콘솔에 찍히는 이벤트가 확연히 줄었음을 확인할 수 있다.

💡 useRef 사용


이전 스크롤값을 let 키워드를 사용해 전역변수로 선언하니 다음과 같은 오류가 콘솔에 나타났다.

Assignments to the 'beforeScrollY' variable from inside React Hook useMemo will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useMemo

검색해보니 useMemo 안에 들어가는 변수를 let이 아닌 다른 키워드로 선언을 해주어야 하는 것 같았다. 이 게시물에서는 해결법으로 let이 아닌 useState로 선언을 해주었는데, 나는 useRef를 이용해 스크롤 값이 변경될 때마다 current로 스크롤 값을 불러와 다뤄주었다.

const [isNavOn, setIsNavOn] = useState(true);
//이전 스크롤 초기값
const beforeScrollY = useRef(0);

  useEffect(() => {
    window.addEventListener("scroll", scrollEvent);
    if (token) {
      setIsLoggedIn(true);
    } else {
      setIsLoggedIn(false);
    }
  }, [isLoggedIn, setIsLoggedIn, token]);

  const scrollEvent = useMemo(
    () =>
      throttle(() => {
        const currentScrollY = window.scrollY;
        if (beforeScrollY.current < currentScrollY) {
          setIsNavOn(false);
          console.log("내림");
        } else {
          setIsNavOn(true);
          console.log("올림");
        }
        //이전 스크롤값 저장
        beforeScrollY.current = currentScrollY;
      }, 300),
    [beforeScrollY]
  );
return(
  <div className={isNavOn ? "main__first" : "main__first hidden"}>

의존성 배열 안에는 렌더링과 관련된 값을 넣어주어야 하기에 beforeScrollY를 넣어 주었다.
이렇게 코드를 수정하니 콘솔에 나타나던 오류도 없어졌고 실행도 정상적으로 되는 것을 확인 할 수 있었다.

profile
내가 나중에 보려고

1개의 댓글

comment-user-thumbnail
2022년 7월 29일

크 감사합니다!

답글 달기