채팅 구현 직후
첫번째 최적화 (자동으로 스크롤을 가장 아래로)
문제
- 채팅방을 잠시 나갔다 들어오면 가장 마지막에 온 메세지가 보이지 않고 가장 오래된 메세지가 먼저 보였습니다.
- 그리고 새로운 메세지가 와도 스크롤이 자동으로 내려가지 않고, 사용자가 직접 내려야만 메세지를 볼수 있습니다.
- 채팅방이 이런식으로 작동하면 안된다고 생각했습니다.
해결
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>
);
};
useRef
를 생성하고, 채팅 내용이 담긴 배열 아래에 빈 div
를 만들어줍니다.
- 그리고 빈
div
에 만든 ref
를 달아줍니다.
useEffect
에 설정한 요소 (여기선 ref를 달아둔 div입니다) 로 화면 스크롤을 이동하는 scrollIntoView()
로직을 추가합니다.
- 그리고 의존성 배열에
chatArr
를 넣어 새로운 채팅이 올때마다 scrollIntoView()
함수가 작동하게 합니다.
- 이제 새로운 메세지가 오면 자동으로 스크롤이 내려가게 됩니다.
두번째 최적화 (조건부로 스크롤 자동 이동)
문제
- 이제 채팅방에 입장하거나 새로운 메세지가 온다면 스크롤이 자동으로 최신 메세지를 보여주게 됩니다.
- 하지만 유저A가 이전의 채팅들을 읽어보던 중, 유저B가 새로운 메세지를 보내자 유저A의 스크롤도 자동으로 내려가게 됩니다.
- 이전의 채팅을 읽던 유저A는 자동으로 내려가는 스크롤이 답답한 나머지, 사이트를 꺼버리게 됩니다....흑흑
해결방안
- 스크롤이 가장 아래에 위치했을 때만, 스크롤이 자동으로 내려가고 이전의 채팅을 읽고 있다면 스크롤이 자동으로 움직이지 않게 해야합니다.
해결
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 (
<div>
{chatArr.map((chat) => (
<div key={chat.key}>{chat.message}</div>
))}
;
<div ref={scrollRef} />
</div>
);
};
scrollState
라는 state
를 생성합니다. scrollState
는 유저의 스크롤이 가장 아래에 있다면 true
를, 스크롤이 가장 아래가 아니라면 false
를 값으로 갖게 됩니다.
scrollEvent
함수를 생성합니다.
- 세가지 변수를 만듭니다.
totalHeight
에는 html 전체의 높이 값을 할당합니다. innerHeight
에는 현재 창의 높이 값을 할당합니다. 마지막으로 myHeight
는 현재 스크롤의 높이 값을 할당합니다. 스크롤이 가장 아래에 있을 때 totalHeight
== innerHeight
+ myHeight
가 됩니다. 그 외에는 total Height
가 항상 큽니다. 따라서 스크롤이 가장 아래에 내려가있다면 scrollState
는 true
가 되고, 자동 스크롤이 활성화 됩니다.
3-1. 스크롤 이벤트가 너무 자주 일어나면 과부하가 일어나므로, lodash의 debounce로 함수를 감싸줍니다.
3-2. 이부분은 콘솔로 직접 찍어보시면 어떤 값들인지 빠르게 알 수 있습니다.
- 컴포넌트가 렌더링 될때 scroll 이벤트를 추가해주고,
scrollEvent
함수를 불러옵니다.
scrollState
가 false
일 땐 scrollIntoView()
함수가 작동하지 않도록 useEffect
에 return
문을 작성해줍니다.
마지막 최적화 (마지막 메세지로 가는 버튼)
문제
- 이제 조건부로 스크롤이 잘 작동합니다. 마지막으로 하나만 더 추가하고 싶은 욕심이 납니다. 유저가 긴 채팅들을 다 읽고 난 후에 한 번에 가장 아래로 내려가고 싶어할것을 알고 있습니다.
해결
- 마지막 메세지로 가는 버튼을 생성합니다.
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>
</>
);
};
scrollState
가 false
일 때, 즉 스크롤이 가장 아래가 아닐때 마지막 메세지로 가는 버튼을 렌더링합니다.
- 버튼에 onClick 이벤트를 설정하고 설정한
ref
로 이동하게 합니다. 추가로 { behavior: smooth }
를 적용하면 스크롤이 부드럽게 내려가게 됩니다.