본문 바로가기

개발

useScrollAutoMove - 실시간 로그 화면 스크롤 자동 이동

회사 서비스 중에 실시간으로 추가되는 로그를 계속해서 보여줘야 하는 화면이 있었다.

 

배열에 로그 데이터가 계속해서 추가되며 전체 로그의 height가 길어지면서 당연히 화면에 세로 스크롤이 생겼다. 이런 상황에서 리액트는 스크롤이 상단에 멈춰있는 상태로 스크롤 전체 길이만 길어져갔다.

스크롤이 상단에서 멈춰있다

 

그런데 이렇게 되니 새로 쌓이는 로그들은 스크롤을 직접 내려 확인해야 하는 불편함이 생겼다. 마우스로 스크롤을 내리지 않는 이상 초반에 생성된 로그만 볼 수 밖에 없었고, 자동으로 가장 최신 스크롤을 계속 볼 수 있게 개발해보기로 했다.


 

export const useScrollAutoMove = (data: any[]) => {
  const scrollRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    }
  }, [data]);
  
  return { scrollRef };
};

 

 

useScrollAutoMove이라는 이름의 커스텀 훅을 만들어보았다. div element를 위한 scrollRef라는 Ref 객체를 만들었다. 스크롤을 원하는 div에 해당 Ref를 설정해놓으면 자동으로 스크롤이 내려가도록 동작한다.

 

코드에 대한 개략적인 설명은 아래와 같다.

- 데이터가 추가될 때마다 scrollRef의 scrollTop을 scrollHeight로 변경한다.
- clientHeight: 현재 보이는 영역의 높이
- scrollHeight: 전체 컨텐츠의 높이
- scrollTop: 현재 스크롤된 위치

 

예를 들어, 화면의 최상단 높이는 0px. clientHeight를 200px, scrollHeight를 500px이라고 가정해보자.


scrollTop의 가능한 범위는 0 ~ 300(전체 컨텐츠 높이 - 보이는 영역의 높이)px이다. 이 상황에서 스크롤을 가장 위로 올리는건 element.scrollTop = 0; 으로 설정하면 된다. 반대로 스크롤을 가장 아래로 내리는건 element.scrollTop = element.scrollHeight; 으로 설정하면 된다.


* scrollTop의 최대 범위가 300px까지지만 브라우저가 최대 가능한 값으로 자동 조정하게 된다.

 

스크롤이 추가되는 데이터를 따라 자동으로 내려간다


 

여기까지 개발하고 실행시켜 보면 잘 동작하는 것 같다. 그런데 그 상황에서 위쪽 데이터를 보기 위해 스크롤을 올리는 순간 버그가 생긴다. 스크롤을 올려도, scrollTop이 계속해서 화면의 가장 아래로 설정되기 때문에 다시 스크롤이 아래로 이동하게 되는 것이다.

 

자동 스크롤이 실행되던 도중 스크롤 변경이 발생할 수 있는 경우는 총 2가지가 있었다.

1) 유저가 직접 스크롤바를 끌면서 스크롤 이동

2) 마우스 스크롤을 이용해 스크롤 이동

자꾸 하단으로 되돌아가는 스크롤

두 경우를 모두 대응할 수 있는 코드를 추가해줬다.

export const useScrollAutoMove = (data: any[]) => {
  const scrollRef = useRef<HTMLDivElement>(null);
  const [isAutoMove, setIsAutoMove] = useState(true);

  useEffect(() => {
    if (scrollRef.current && isAutoMove) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    }
  }, [data, isAutoMove]);

  useEffect(() => {
    const element = scrollRef.current;
    if (!element) return;

    const handleWheel = () => {
      setIsAutoMove(false);
    };

    const handleMouseDown = (e: MouseEvent) => {
      const isScrollbarClick = e.offsetX > element.clientWidth;

      if (isScrollbarClick) {
        setIsAutoMove(false);
      }
    };

    element.addEventListener('wheel', handleWheel);
    element.addEventListener('mousedown', handleMouseDown);

    return () => {
      element.removeEventListener('wheel', handleWheel);
      element.removeEventListener('mousedown', handleMouseDown);
    };
  }, []);

  return { scrollRef };
};

 

이제 자동 스크롤 이동도 잘 동작하면서, 유저의 스크롤 변경에도 대응하는 커스텀 훅이 만들어졌다. 다만, 유저가 스크롤을 건드리는 즉시 isAutoMove은 false가 되어 그 자리에서 스크롤은 고정된다. 유저가 그 부분을 멈춰 볼 필요가 있다고 여길 때 스크롤을 직접 변경한다고 생각해 그렇게 동작하도록 했다.

원했던 대로 이제 잘 동작한다!