<dev.log/>;

실시간 채팅 스크롤 UX 최적화

채팅 구현 직후

  • 우선 채팅 내용이 담긴 배열을 map 메서드를 사용하여 렌더링했습니다.
    //chatBox.js
    
    const chatBox = ({ chatArr }) => {
      return (
        <div>
          {chatArr.map((chat) => (
            <div key={chat.key}>{chat.message}</div>
          ))}
          ;
        </div>
      );
    };
    

첫번째 최적화 (자동으로 스크롤을 가장 아래로)

문제

  • 채팅방을 잠시 나갔다 들어오면 가장 마지막에 온 메세지가 보이지 않고 가장 오래된 메세지가 먼저 보였습니다.
  • 그리고 새로운 메세지가 와도 스크롤이 자동으로 내려가지 않고, 사용자가 직접 내려야만 메세지를 볼수 있습니다.
  • 채팅방이 이런식으로 작동하면 안된다고 생각했습니다.

해결

//chatBox.js

const chatBox = ({ chatArr }) => {
  const scrollRef = useRef();

  useEffect(() => {
    scrollRef.current.scrollIntoView();
  }, [chatArr]);

  return (
    <div>
      {chatArr.map((chat) => (
        <div key={chat.key}>{chat.message}</div>
      ))}
      ;
      <div ref={scrollRef} />
    </div>
  );
};
  1. useRef 를 생성하고, 채팅 내용이 담긴 배열 아래에 빈 div를 만들어줍니다.
  2. 그리고 빈 div에 만든 ref를 달아줍니다.
  3. useEffect 에 설정한 요소 (여기선 ref를 달아둔 div입니다) 로 화면 스크롤을 이동하는 scrollIntoView() 로직을 추가합니다.
  4. 그리고 의존성 배열에 chatArr 를 넣어 새로운 채팅이 올때마다 scrollIntoView() 함수가 작동하게 합니다.
  5. 이제 새로운 메세지가 오면 자동으로 스크롤이 내려가게 됩니다.

두번째 최적화 (조건부로 스크롤 자동 이동)

문제

  • 이제 채팅방에 입장하거나 새로운 메세지가 온다면 스크롤이 자동으로 최신 메세지를 보여주게 됩니다.
  • 하지만 유저A가 이전의 채팅들을 읽어보던 중, 유저B가 새로운 메세지를 보내자 유저A의 스크롤도 자동으로 내려가게 됩니다.
  • 이전의 채팅을 읽던 유저A는 자동으로 내려가는 스크롤이 답답한 나머지, 사이트를 꺼버리게 됩니다....흑흑

해결방안

  • 스크롤이 가장 아래에 위치했을 때만, 스크롤이 자동으로 내려가고 이전의 채팅을 읽고 있다면 스크롤이 자동으로 움직이지 않게 해야합니다.

해결

//chatBox.js
import _ from 'lodash';

const chatBox = ({ chatArr }) => {
  const scrollRef = useRef();

  //1번 내용
  const [scrollState, setScrollState] = useState(true);

  //2번 내용
  const scrollEvent = _.debounce((event) => {
    //3번 내용
    const totalHeight = document.documentElement.scrollHeight;
    const innerHeight = window.innerHeight;
    const myHeight = event.srcElement.scrollingElement.scrollTop;

    setScrollState(totalHeight <= innerHeight + myHeight + 500);
  }, 200);

  //4번 내용
  useEffect(() => {
    window.addEventListener('scroll', scrollEvent);
  }, [scrollEvent]);

  //5번 내용
  useEffect(() => {
    if (!scrollState) return;
    scrollRef.current.scrollIntoView();
  }, [chatArr, scrollState]);

  return (
    <div>
      {chatArr.map((chat) => (
        <div key={chat.key}>{chat.message}</div>
      ))}
      ;
      <div ref={scrollRef} />
    </div>
  );
};
  1. scrollState라는 state를 생성합니다. scrollState는 유저의 스크롤이 가장 아래에 있다면 true를, 스크롤이 가장 아래가 아니라면 false를 값으로 갖게 됩니다.
  2. scrollEvent 함수를 생성합니다.
  3. 세가지 변수를 만듭니다. totalHeight에는 html 전체의 높이 값을 할당합니다. innerHeight에는 현재 창의 높이 값을 할당합니다. 마지막으로 myHeight는 현재 스크롤의 높이 값을 할당합니다. 스크롤이 가장 아래에 있을 때 totalHeight == innerHeight + myHeight 가 됩니다. 그 외에는 total Height가 항상 큽니다. 따라서 스크롤이 가장 아래에 내려가있다면 scrollStatetrue 가 되고, 자동 스크롤이 활성화 됩니다. 3-1. 스크롤 이벤트가 너무 자주 일어나면 과부하가 일어나므로, lodash의 debounce로 함수를 감싸줍니다. 3-2. 이부분은 콘솔로 직접 찍어보시면 어떤 값들인지 빠르게 알 수 있습니다.
  4. 컴포넌트가 렌더링 될때 scroll 이벤트를 추가해주고, scrollEvent 함수를 불러옵니다.
  5. scrollStatefalse일 땐 scrollIntoView() 함수가 작동하지 않도록 useEffectreturn문을 작성해줍니다.

마지막 최적화 (마지막 메세지로 가는 버튼)

문제

  • 이제 조건부로 스크롤이 잘 작동합니다. 마지막으로 하나만 더 추가하고 싶은 욕심이 납니다. 유저가 긴 채팅들을 다 읽고 난 후에 한 번에 가장 아래로 내려가고 싶어할것을 알고 있습니다.

해결

  • 마지막 메세지로 가는 버튼을 생성합니다.
    //chatBox.js
    import _ from 'lodash';
    
    const chatBox = ({ chatArr }) => {
      const scrollRef = useRef();
    
      const [scrollState, setScrollState] = useState(true);
    
      const scrollEvent = _.debounce((event) => {
        const totalHeight = document.documentElement.scrollHeight;
        const innerHeight = window.innerHeight;
        const myHeight = event.srcElement.scrollingElement.scrollTop;
    
        setScrollState(totalHeight <= innerHeight + myHeight + 500);
      }, 200);
    
      useEffect(() => {
        window.addEventListener('scroll', scrollEvent);
      }, [scrollEvent]);
    
      useEffect(() => {
        if (!scrollState) return;
        scrollRef.current.scrollIntoView();
      }, [chatArr, scrollState]);
    
      return (
        <>
          {!scrollState && (
            <div onClick={() => scrollRef.current.scrollIntoView({ behavior: 'smooth' })}>
              마지막 메세지 보기
            </div>
          )}
    
          <div>
            {chatArr.map((chat) => (
              <div key={chat.key}>{chat.message}</div>
            ))}
            ;
            <div ref={scrollRef} />
          </div>
        </>
      );
    };
    
  1. scrollStatefalse일 때, 즉 스크롤이 가장 아래가 아닐때 마지막 메세지로 가는 버튼을 렌더링합니다.
  2. 버튼에 onClick 이벤트를 설정하고 설정한 ref로 이동하게 합니다. 추가로 { behavior: smooth } 를 적용하면 스크롤이 부드럽게 내려가게 됩니다.