웹 개발 프로젝트를 하던 중 채팅 기능을 구현해야한적이 있었다. 채팅창에 대화 내용들을 렌더링하는것은 성공했지만 문제가 발생했다. 스크롤에 대한 이슈였다.
보통 최근 대화일수록 아래쪽에 위치하기 때문에 채팅창 스크롤을 맨 아래쪽에서 시작하도록 만들어야했다. 그게 일반적인 유저들이 기대하는 UI이기 때문이다. 또한 새로운 메세지가 실시간으로 생성될 때마다 스크롤을 다시 맨 아래로 위치시켜주어야했다.
이와 같은 이슈는 정리해두면 좋을것 같아서 알아보았던 자료들을 정리해보고자 한다.
이 문제는 바닐라 자바스크립트를 사용할 경우 오히려 더 쉽게 해결할 수 있을것 같다고 생각했다. 직접적으로 DOM을 조작하면되기 때문이다. 컨트롤하고 싶은 영역(div)을 선택자로 지정하여 스크롤을 제어하면 될 문제지만 리액트에서는 조금 다르다.
우선 해결해야할 문제들을 정리해보자면 다음과 같다.
- 스크롤을 조작할 타겟 지정
- 채팅창의 스크롤 위치 제어
- 스크롤 제어 이벤트 시점 설정
리액트는 Virtual DOM을 이용한다. 만약 데이터가 업데이트되어 변동이 생긴다면 Virtual DOM과 Real DOM을 비교하여 바뀐 부분만 Real DOM에 적용되는 방식이다. 따라서 순수 자바스크립트처럼 getElementById나 querySelector를 이용하여 요소에 접근하는것은 지양되는 방식이다.
리액트 라이프사이클과 함께 동작하며 스크롤을 제어하고 싶은 요소를 타겟으로 지정하려면 어떻게 해야할까? 그것은 useRef
hook을 이용하면 가능하다.
useRef
는 상태가 바뀌어도 UI의 변경과 관계없을때 사용하는 hook으로 주로 리액트에서 DOM Element에 접근할 때 사용한다. 컴포넌트 생애 주기 내에서 유지할 ref
객체를 반환한다. ref
객체는 current라는 프로퍼티를 가지고 있으며 자유롭게 변경이 가능하다. 중요한것은 useRef에 의해 반환된 ref
객체가 변경되어도 컴포넌트가 리렌더링되지 않는다.
이를 다음과 같이 적용할 수 있다.
// ChatRoom.js
import { useRef } from "react";
function ChatRoom () {
const scrollRef = useRef(); // ChatWrapper Element
return (
<ChatWrapper ref={scrollRef}>
{/* Chats */}
</ChatWrapper>
);
};
export default ChatRoom;
이처럼 스크롤 제어를 하고 싶은 element에 ref
속성을 부여하면 해당 요소에 접근할 수 있으며, 임의로 조작하여도 컴포넌트 리렌더링이 발생되지 않는다.
앞서 scrolRef라는 변수에 스크롤 제어를 하고 싶은 요소를 타겟으로 지정하였다. 다음 자바스크립트 프로퍼티 속성을 이용하면 스크롤 제어를 할 수 있다.
scrollTop: 현재 스크롤 위치
scrollHeight: 스크롤 영역 전체 높이
ref객체는 자유롭게 수정해도 된다. 그래서 현재 스크롤 위치에 스크롤 영역 전체 높이값을 주면 스크롤 위치를 맨 아래쪽으로 위치시킬 수 있을 것이다.
코드로 표현하면 다음과 같다.
// 스크롤 제어
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
지금 구현하고 있는 스크롤 위치 제어 이벤트가 발생되는 시점은 두 종류가 있다. 채팅창을 처음 열었을 때와 새로운 메세지가 생성되었을때이다. 이를 다르게 말하자면 채팅창 컴포넌트가 렌더링 될 때와 채팅 메세지 데이터가 업데이트 될 때라고 할 수 있다.
- 채팅창 최초 렌더링 = 컴포넌트 마운트
- 새로운 채팅 메세지 발생 = 컴포넌트 업데이트
즉 컴포넌트의 생애 주기에 따른 동작을 처리해주어야한다. 클래스형 컴포넌트인지 함수형 컴포넌트인지에 따라 방법이 다른데, 이 프로젝트는 함수형을 채택하였으므로 특정 hook을 사용하여 구현해야한다.
함수형 리액트에서 컴포넌트가 마운트 되었을때와 업데이트 될 때 특정 작업을 처리하려면 useEffect
라는 hook을 사용하면 된다.
컴포넌트 생애 주기에 따른 동작 처리는 클래스형 컴포넌트에서만 가능했다. 지금은 hook을 이용하여 함수형 컴포넌트에서도 구현할 수 있다.
ChatRoom 컴포넌트에 useEffect
를 적용하면,
// ChatRoom.js
import { useRef, useEffect } from "react";
function ChatRoom () {
const scrollRef = useRef();
useEffect(() => {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, [chatData])
return (
<ChatWrapper ref={scrollRef}>
{/* Chats */}
</ChatWrapper>
);
};
export default ChatRoom;
useEffect
의 두번째 parameter는 의존성 배열(deps)이다. 해당 배열에 채팅 내용과 관련된 데이터를 넣어주었으므로 채팅 데이터가 업데이트된다면 해당 기능이 동작할 것이다. 따라서 새 메세지가 발생되면 스크롤은 다시 맨 아래로 위치할 것이다.
리액트에서 DOM에 접근하여 특정 시점에 스크롤 위치를 제어하는 방법을 정리해보았다. 이번 이슈를 통해 Virtual DOM에 대해 좀 더 깊이 공부할 수 있었다. 만약 해당 내용을 잘 몰랐다면 Error가 발생하는 이유를 찾기 어려웠을 것이다.
이 포스팅은 리액트 기초와 hook에 대해서 공부하고 다시 업데이트하였다. 내가 이 문제를 풀어나갔던 방법이 같은 문제로 고민하는 사람들에게 명료하게 전달되었으면 좋겠다.