Virtual scroll
가상 스크롤은 화면에서 보이지 않는 부분의 내용을 출력하지 않고, 화면에 보여질 때만 출력하는 방식의 스크롤을 말한다.
많은 양의 리스트 데이터를 화면에 그리는 경우, 모든 항목을 그리면 성능 상 문제를 초래한다.
예를 들자면 10만개의 데이터를 그리기 위해 10만개의 DOM노드를 그리려고 하면 Call Stack size error가 발생할 것이다.
그렇기에 화면에 직접적으로 보여지는 부분만 그리고, 나머지 부분은 가상으로 그려내는 것이 Virtual Scroll의 목표이다.
이때 새로운 요소들이 렌더링되는 동안 어색하지 않도록, Node padding이라는 여분의 공간을 둔다.
가상 스크롤은 SNS나 커뮤니티 등 무한 스크롤에서 많은 내용 출력할 때 사용하면 유리하다.
구현
우선 가상 스크롤이 구현되지 않은 간단한 포스팅 리스트이다.
import { useEffect, useRef, useState } from 'react';
import './App.style.css'
import randomImage from './radomImage';
function App() {
const randomCards = useRef(Array.from({length : 200}, ()=>parseInt(Math.random() * 100) % randomImage.length)).current;
return (
<div className="layout">
<div className='container FC'>
{randomCards.map((v,i)=>(
<div key={i} className='cardContainer FC'>
<div className='cardImageContainer FC'>
<img className='cardImage' src={randomImage[v]}></img>
</div>
<div className='textContainer'>
display
</div>
</div>
))}
</div>
</div>
);
}
export default App;


개발자 도구에서 확인하면 모든 게시물이 렌더링이 된 것을 확인할 수 있다.
이제 차근차근 가상 스크롤을 구현해보자
1. 스크롤 위치 확인
가상 스크롤을 구현하기 위해서 가장 먼저 해야 할 부분은 현재 스크롤이 어디에 위치하고 있는지 확인하는 것이다.
현재 스크롤 위치를 확인하기 위해서 scroll EventListener를 추가해보자.
const [scrollPos,setScrollPos] = useState(0);
...
function onScroll() {
setScrollPos(window.scrollY);
}
useEffect(() => {
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []);
console.log(scrollPos);
...

콘솔 창에 현재 스크롤 위치를 출력하는 것을 확인할 수 있다.
2. 현재 창의 크기 확인하기
스크롤 위치를 확인하였으면 현재 창의 크기를 확인해야한다.
현재 창의 크기를 확인해야 한 화면에 몇 개의 포스팅이 들어가는지 확인할 수 있기 때문이다.
현재 창 크기를 확인하는 방법은 window.innerHeight로 쉽게 확인할 수 있다.
const windowHeight = window.innerHeight;
3. 보여지는 부분만 실제 데이터 넣기.
현재 보여지는 부분만 실제 데이터를 넣기 위해 조건문을 추가해준다.
...
return (
<div className="layout">
<div className='container FC'>
{randomCards.map((v,i)=>(
<div key={i} className='cardContainer FC'>
<div className='cardImageContainer FC'>
//해당 부분의 420은 포스팅 하나의 height이다.
{scrollPos < 420 * (i + 1) && (scrollPos + windowHeight) > 420 * (i)
? <img className='cardImage' src={randomImage[v]}></img>
:<div className='skeleton'></div>}
</div>
<div className='textContainer'>
{scrollPos < 420 * (i + 1) && (scrollPos + windowHeight) > 420 * (i)
? 'display'
: 'hide'}
</div>
</div>
))}
</div>
</div>
);
...

현재 보여지는 3개의 포스팅만 display로 표기가 되어 있고, 다른 포스팅은 모두 hide처리가 된 것을 확인할 수 있다.
4. NodePadding 추가하기
가상 스크롤을 위와 같이 화면에 보여지는 것만 출력하면 성능적으로 좋기는 하지만 빠르게 스크롤 할 경우 데이터 로드가 느려 데이터가 안 보일 수 있다.
그렇기 때문에 보여지는 부분에 더해 여유분을 미리 로드해두는 것이 nodePadding이다.
nodePadding을 추가하는 것은 간단하다.
...
const NodePadding = 2;
...
{randomCards.map((v,i)=>(
<div key={i} className='cardContainer FC'>
<div className='cardImageContainer FC'>
{scrollPos < 420 * (i + 1 + NodePadding) && (scrollPos + windowHeight) > 420 * (i - 1 - NodePadding) ? <img className='cardImage' src={randomImage[v]}></img> :<div className='skeleton'></div>}
</div>
<div className='textContainer'>
{scrollPos < 420 * (i + 1 + NodePadding) && (scrollPos + windowHeight) > 420 * (i - 1 - NodePadding)? 'display' : 'hide'}
</div>
</div>
))}
...

화면에 출력된 3개의 포스트 위, 아래로 2개씩 추가적으로 display가 된 것을 확인할 수 있다.
마치며
최근 프로젝트 성능을 높이기 위해 다양한 방법을 찾아보며 알게 된 가상 스크롤을 직접 구현해보았는데 구현은 매우 간단하지만 성능 향상은 매우 좋다는 것을 알았다.
이후에는 디바운스 추가를 통한 성능 추가 개선, 동적 크기의 포스팅 관리 등 다양한 개선을 추가적으로 할 수 있을 것 같다.