상금 규모는 상승하는 느낌이 있어야 제맛!

이번에는 윤프로의 커리어 내역을 구현하고자 했다. 골프 프로의 커리어는 보통 우승과 상금으로 나타내는데, 아직 정규 투어 우승이 없어서 상금을 중점으로 구현했다. 상금은 그냥 띅 보여주기보다 주르륵 올라가는 이벤트를 주는 것이 더 다이나믹하고 실시간으로 업데이트 되는 느낌을 준다. 이렇게 말이다.

스크롤 이벤트 구현짤

움짤이여서 좀 끊기는데, 실제 구현은 매우 부드럽다. 이 컴포넌트를 구현하기 위해서는 scroll 이벤트가 필요했다. 당연히 window에 스크롤 이벤트가 발생할때마다 스크롤 좌표를 구해서 이벤트를 처리할 수도 있찌만, 이것은 트로트링도 해야하고 여간 귀찮지 않을 수 없다. 그리고 우리에겐 IntersectionObserver API가 있다.

사실 이 기술을 구글링하면 잘 구현된 커스텀 훅이 나온다.

  const useScrollCount = () => {
    const dom = useRef();
    
    let onlyonce = true;
    const handleScroll = useCallback(([entry]) => {
      if(entry.isIntersecting) {
        if (onlyonce) {
          countUP()
        }
        onlyonce = false;
      }
    }, []);
    
    useEffect(() => {
      let observer;
      const { current } = dom;
      console.log(current)
      if (current) {
        observer = new IntersectionObserver(handleScroll, { threshold: 0.7 });
        observer.observe(current);

        
        return () => observer && observer.disconnect();
      };
    }, [handleScroll]);
    
      return {
      refCount: dom,
    };
  }

물론 이 커스텀 훅을 사용하면 구현이 쉽다. 하지만 커스텀 훅을 사용하려면 훅을 따로 빼서 파일을 만들어야 하기도 하고, 또 무지성으로 이해하지 못하고 사용하는 것은 내 방법이 아니기에 커스텀 훅이 아닌 같은 컴포넌트에서 구현하고자 노력했다. 그리고 좀더 목적에 부합하는 직관적인 코드를 짜고 싶었다. useCallback() 훅도 굳이 필요가 없다고 생각했고… 그래서 IntersectionObserver()의 특성을 활용한 아주 직관적인 코드를 만들어 봤다.

결과물은 다음과 같다.

 function countUP () {
    setFade({opacity: 1})
    const targetMP = 230445714;
    const targetTP = 333855464;
    const MPJump =  Math.floor(targetMP/150) 
    const TPJump =  Math.floor(targetTP/150)

    let count = 0;
    if (onlyonce) {
      onlyonce = false
      const intervalPrice = setInterval(() => {
        count += 1;
        setMP(p => p + MPJump)
        setTP(p => p + TPJump)
  
        if (count >= 150) {
          clearInterval(intervalPrice)
          setMP(targetMP)
          setTP(targetTP)
        }
      }, 10)
    }
  }

  useEffect(() => {
  let observer;
  const { current } = ref;
  console.log(current)
  if (current) {
    observer = new IntersectionObserver(countUP, { threshold: 0.7 });
    observer.observe(current);
    
    return () => observer && observer.disconnect();
  };
}, []);

IntersectionObserver()는 스크롤 이벤트랑 다르게 비동기적으로 렌더링을 한다. 그래서 트로틀링이 필요가 없다. observer.observe(current); 이 줄에서 보면 알 수 있듯이 observer 객체가 ‘current’즉 ref객체의 current를 70프로 이상 관측하면, countUp 함수를 실행하고 랜더링이 일어난다. 그리고 ref 객체의 current값은 CarrerBox DOM이다.

앞서 가져온 커스텀 훅을 보면 관측이 되었을때 handleScroll함수를 실행한다. 그리고 handleScroll 함수는 IntersectionObserver에서 받은 entry 객체의 isIntersecting이 true 일때 countUP을 실행한다. (isIntersecting은 ref.current가 70프로 이상 관측되면 true가 된다.) 그런데 잘 생각해보면 나는 isIntersecting 값을 사용할 필요가 없다. 왜냐하면 onlyonce 변수 떄문인데, isIntersecting 값을 사용안해서 문제가 생기는 경우는 IntersectionObserver객체가 랜더링을 해서 countUP을 실행했지만, isIntersecting이 false인 경우이다.

이 경우는 언제 발생할 수 있는가? 기존 관측이 70프로가 이상인 상태에서 70이하로 빠져나올때만 가능하다. 하지만 애초에 관측이 70프로가 이상이면 countUP함수가 실행되고 onlyonce가 false가 되기 때문에 countUP은 실행되지만 어떠한 UX변화가 있지 않다. 그래서 나는 isIntersecting을 사용할 필요가 없다. 그리고 useCallback 훅을 써도 handleScroll함수에서 실행 시키는 countUP함수는 계속 랜더링이 되기 때문에 성능에서도 큰 차이가 없다.

#마치며

사실 내 코드가 더 좋은 코드라고 생각하지는 않는다. 사람들이 많이 쓰는 것에는 이유가 있을 것이다. 그래도 나만의 코드를 만들고자 노력해보면서 기존 커스텀 훅의 원리를 99프로는 이해할 수 있었던 것 같다. 다음에도 또 새로운 구현을 포스팅해보겠다!