[React]Throttle로 Sticky Header 구현하기

16
post-thumbnail
post-custom-banner

사이드 프로젝트를 진행하며 유저가 스크롤을 내릴 때도 상단에 유지되는 header를 간단하게 만들어 보았다..이름하여 sticky header..

🐯 Sticky Header


Sticky Header는 위 이미지처럼 대부분의 웹 서비스에서 적용되고 있는 방식으로 유저가 스크롤을 할 때 header가 상단에 유지시켜 스크롤 상태와 무관하게 유저가 navigator 나 검색창을 사용할 수 있도록 하는 기능이다. 개인적으로 Excel에서 셀 고정이 필수인 것처럼.. 웹 사이트에도 Sticky header도 필수라고 생각한다. 없으면 너무 불편하다!!

👀 Scroll event 사용시 주의할 점

사실 처음에는 window의 scroll가 발생할 때마다 현재 Y축의 위치를 찾아 이를 state로 업데이트 해주면 되겠지 싶었다. 하지만 scroll 이벤트는 말 그대로 유저가 scroll을 조금만 내려 Y의 위치가 아주 조금만 변경되어도 이벤트가 핸들러가 실행되기 때문에.. 또 유저의 scroll 이벤트가 빠른 속도로 실행될 수 있기 때문에.. scroll을 할 때마다 state를 하겠다는 생각은!! 멈춰~!!!야 한다.

아래는 scroll 이벤트에 따른 state값을 console에 찍어본 결과인데, 보이는 것과 같이 scroll 이벤트에 따라 state를 변경하면 한 번의 스크롤로 인해 state가 수 십번 바뀌게 되니 해당 컴포넌트가 수 십번 불필요하게 리렌더링 될 수 밖에 없다!!

🛠 Scroll event를 제어해보자!(debounce vs throttle)

scroll 이벤트가 빠른 속도로 실행될 수 있기 때문에, 이벤트 핸들러는 DOM 수정과 같이 느린 작업을 실행하지 말아야 합니다. 대신, requestAnimationFrame(), setTimeout(), 혹은, CustomEvent을 사용하여 이벤트를 제한하는 것을 권장합니다.
MDN-scroll

MDN에서 말하는 것처럼 빠른 속도로 실행되는 scroll 이벤트를 제어할 방식이 필요하다. DOM event를 제어하는 대표적인 방법으로는 debounce와 throttle이 있는데, 간단하게 말하면 1초짜리 debounce와 throttle을 걸었다고 했을 때 debounce는 유저의 마지막 scroll event가 일어난 시점에서 1초 후에 event를 1번 실행시키고, throttle은 1초 동안 딱 1번의 event만 실행시킨다.

이번에는 유저가 스크롤을 한 즉시!! 표준 header에서 Sticky Header로 변환시키는 작업이 필요하기 때문에 이벤트 발생 시점 부터 정해진 시간이 지난 후 이벤트를 발생시키는 debounce 보다는 여러 번 발생하는 이벤트를 일정 시간 동안 한번만 실행시키는 throttle이 적합하다는 생각이 들어 throttle 통해 scroll event를 제어했다.(참고로 throttle에 사용할 setTimeout() Web API는 호출 시점을 millisecond 단위로 설정할 수 있기 빠른 스크롤 이벤트도 충분히 잡을 수 있다!)

👩‍💻 Scroll event를 제어할 함수 만들기(throttle)

  1. timer를 담은 변수를 정의한다.
  2. timer 존재 여부에 따라 setTimeout()을 실행시킨다.
    2-1. timer가 없다면 setTimeout이 실행되지 않은 상태이므로 setTimeout을 실행시키며 정해진 시간 후에 callback 함수를 실행시키도 timer를 다시 비워준다.
    2-2 timer가 있다면 early return 한다.(if(!timer) 일때 아래 코드를 실행시키는 방법으로 해도 괜찮다.)

*참고로 setTimeout의 리턴값을 받는 timer 변수의 type을 number로 주고 싶다면 아래처럼 setTimeout 앞에 window를 명시에 주면 된다.

window.setTimeout(() => {...}, delay);

Sticky Header Component에 throttle 함수 적용하기

  1. 우선 scroll 여부를 담을 상태를 만들어준다.
  const [scrollFlag, setScrollFlag] = useState(false); 
  1. seEffect에는 window의 scroll 이벤트 리스너를 추가하고 clean up도 진행해준다.
  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);
  1. scroll 이벤트 리스너의 callback 함수는 위에서 만든 throttle 함수로 만들어진 함수를 이용한다.
const handleScroll = throttle(updateScroll, 100);
  1. updateScroll 함수에서는 Window.scrollY를 통해 문서를 스크롤한 픽셀의 수가 0이 아니면 스크롤 한 것으로 판단한다.
 const updateScroll = () => {
    const { scrollY } = window;
    const isScrolled = scrollY !== 0;
    setScrollFlag(isScrolled);
  };

🌷 완성

참고
MDN scrollY

post-custom-banner

14개의 댓글

comment-user-thumbnail
2021년 8월 8일

잇님 글 잘봤습니다~ 좋아요 꾸욱!~
스티키 헤더? 이름 완전 귀여운데용?

1개의 답글
comment-user-thumbnail
2021년 8월 9일

잘 봤어요!! 궁금한 점이 있습니다 !
스크롤 이벤트에 따라서 다른 컴포넌트가 렌더되는 건가요?? 쓰로틀로 어떻게 컴포넌트에 작용하는지 궁금해요 !
그리고 만약 저라면,,그냥 특정 scrollY값에 도달하면 header를 없애거나 할 것 같은데..쓰로틀로 구현하신 이유가 궁금해요 !

1개의 답글
comment-user-thumbnail
2021년 8월 9일

스로틀로 렌더링 최적화를 하셨군요!!! 한번 만들어두면 다른 프로젝트에서도 두고두고 쓰일 듯 합니다람쥐

1개의 답글
comment-user-thumbnail
2021년 8월 11일

https://developer.mozilla.org/ko/docs/Web/CSS/position
position에 sticky라는 속성을 사용하면 css만으로도 구현이 가능합니다!

1개의 답글
comment-user-thumbnail
2021년 8월 12일

쓰로틀에 대해 명확히 알 수 있게 되었어요. 감사합니다

1개의 답글
comment-user-thumbnail
2022년 9월 19일

const isScrolled = scrollY !== 0; 으로 스크롤 여부 판별하면 만약 스크롤 상태일 때 스크롤용 해더로 변경됐는데 컨텐츠 양이 적어서 스크롤이 안생기는 height로 되면 다시 non 스크롤용 해더로 변경될 거고 그러면 스크롤 생기고 그러면 다시 스크롤용 해더로 변경되고...? 무한 루프가 될것 같아요~

답글 달기