[React] 채팅창 자동스크롤을 구현해보자
때는 바야흐로.. HourGoods 프로젝트에서 채팅창을 구현하던 중…
“새로운 메세지가 왔을 때 자동으로 스크롤이 내려가면 좋겠어요!” 와 같은 불편이 접수됐다.
기억을 정리하고자 블로그 작성을 해봤습니다!
아래는 결과화면입니다. (최종 완성본과는 다르지만.... 자동스크롤 녹화해둔게 없기에..)
1. 스크롤바를 항상 아래로...
일반적인 채팅창에서는
- 내가 채팅을 친 경우
- 상대방이 채팅을 친 경우
즉, 새로운 메세지가 올 때마다 스크롤이 항상 맨 아래로 향합니다.
자동 스크롤을 구현함으로써 채팅을 새로 칠 때마다 스크롤을 아래로 일일이 내릴 필요가 없으니, 사용자에게 보다 좋은 UX를 제공할 수 있게 됩니다.
2. 하단 영역을 useRef로 설정하기
잠깐 useRef에 대해 짚고 넘어가자면..
💡 useRef란?
특정 가상DOM 요소에 접근하기 위해 사용되는 React Hooks이다.
⚡️ useRef 사용 방법
1. 원하는 변수명으로 useRef() 선언
const chatBottomRef = useRef<HTMLDivElement | null>(null);
2. 접근하고자 하는 DOM node에 ref 속성 추가
<div ref={chatBottomRef}></div>
3. 필요한 곳에서 변수명.current로 꺼내서 사용
useEffect(() => { if (isScrollAtBottom) { scrollToBottom(chatBottomRef.current); // Bottom 영역 current 속성 할당 } ... });
HourGoods 코드 발췌...
useRef 함수는 current 속성을 가지고 있는 객체를 반환합니다. 그렇기에 저희가 필요한 곳에서 .current로 꺼내서 사용할 수 있는 것이죠!
3. 지정한 영역으로 스크롤 끌어내리기
똑똑한 JS에는 내장함수 scrollIntoView 가 있습니다.
💡 element.scrollInToView 란?
scrollIntoView()가 호출된 요소가 사용자에게 표시되도록 요소의 상위 컨테이너를 스크롤한다.
⚡️ scrollInToView 사용 방법
element.scrollIntoView();
또한, 메서드의 파라미터값을 지정할 수 있습니다.
1. alignToTop (Boolean Parameter)
- true : 해당 요소의 상단으로 스크롤 이동. {block: "start", inline: "nearest"}와 일치
- false : 해당 요소의 하단으로 스크롤 이동. {block: "end", inline: "nearest"}와 일치
2. behavior (스크롤 애니메이션 지정 Parameter)
- smooth : 스크롤 부드럽게 이동
- instant : 스크롤이 단번에 즉시 이동
3. block (수직 정렬 Parameter)
- start(위), center(중앙), end(아래), nearest 중 하나
4. inline (수평 정렬 Parameter)
- start(왼쪽), center(중앙), end(오른쪽), nearest 중 하나
⚡️ scrollInToView 사용 예제
저의 경우에는 스크롤을 아래로 이동시키는 기능을 따로 함수로 빼서 구현하였습니다.
export const scrollToBottom = (element: HTMLDivElement | null) => {
if (element) {
element.scrollIntoView({
behavior: "smooth", // 부드럽게 이동
block: "end", // 아래로 이동(수직)
inline: "nearest", // 그대로(수평)
});
}
};
즉, useRef + scrollInToview 를 사용하게 되면,
지정한 영역이 사용자에게 표시되도록 스크롤을 이동시킬 수 있게 되는 것!!!!!!!!!
4. 추후에 보완하고 싶은 점...
새로운 채팅이 올라올 때마다 자동으로 스크롤이 내려가는 것까진 좋다! (그것이 UX니까)
하지만, 치명적인 단점을 발견했다..
위에 작성된 코드는.. 지정된 useRef에 새로운 이벤트가 발생했을 때 scrollToBottom() 함수가 실행되는 코드다.
그러다 보니, 내가 이전에 온 채팅들을 보려고 채팅창 스크롤을 올렸으나, 그 사이에 새로운 채팅이 입력되면 강제로 아래로 끌려가게 된다.
코드 전문을 보면 알 수도 있듯.. 사실 보완을 해보려 노력을 해보았다..ㅎㅎ..ㅠㅠ..
채팅창 전체를 useRef로 설정하여, 스크롤 이벤트가 감지가 됐을 때는 scrollToBottom() 함수가 실행이 되지 않도록하였다.
// scroll 이벤트 감지하기
useEffect(() => {
if (chatMsgListRef.current) {
chatMsgListRef.current.addEventListener("scroll", handleScroll);
}
return () => {
if (chatMsgListRef.current) {
chatMsgListRef.current.removeEventListener("scroll", handleScroll);
}
};
}, [chatMsgListRef.current]);
...
// Bottom 영역으로부터 얼마나 떨어진 곳에서부터 scroll을 감지할 것인지
const handleScroll = () => {
const element = chatMsgListRef?.current;
if (element) {
const { scrollTop, scrollHeight, clientHeight } = element;
const isScrolledToBottom =
Math.abs(scrollTop + clientHeight - scrollHeight) <= 100;
setIsScrollAtBottom(isScrolledToBottom);
}
};
또, 사용자의 스크롤이 Bottom 영역에 있지 않은 채 새로운 메세지가 오면
"이름 : 새로운 메세지"
와 같은 버튼을 만들어 하단으로 이동할 수 있도록 만들었다.
const handleButtonClick = () => {
// setIsNewMessage(false);
setLatestMessage(null);
scrollToBottom(chatBottomRef.current);
};
...
{!isScrollAtBottom && isLatestMessage && (
// 새메시지 하단 이동 버튼
// 스크롤이 하단에 있지 않고 최신메세지가 있다면
<button
type="button"
className="new-message-button"
onClick={handleButtonClick}
>
<img
src={`https://d2uxndkqa5kutx.cloudfront.net/${message.imageUrl}`}
alt="프로필사진"
/>
{message.nickname}: {message.content}
</button>
)}
하지만, 어째서인지 100% 정확하게 동작하지는 않는다..😅
코드 리팩토링 1순위이기에, 이번 여행이 끝난다면 반드시 블로그 포스팅을 해서 아래에 링크를 달아 새로운 주제로 업로드하겠다!!!!!