원티드 프리온보딩 프론트엔드에서 기업 과제를 수행하면서 모바일에 대해 렌더링 최적화를 하기 위해 Virtual Scroll을 적용해보았다.
Virtual scroll은 viewport에 보여져야 할 요소만 DOM Tree에 추가하여 렌더링하는 기법이다. viewport에 따라 DOM Tree를 갱신함으로써 렌더링 최적화를 할 수 있다(특히 모바일에서의 퍼포먼스 이슈를 해결해주게 된다).
위의 영상을 보면 현재 viewport에 보여지는 카드들만을 dom tree에 렌더링하고 있음을 확인할 수 있다. 이번에 내가 만든 Virtual scroll은 Card의 높이가 고정되어있음을 가정한다.
Virtual scroll에 대해서는 구현하면서 참고한 해당 레퍼런스에 자세한 설명이 있으므로 참고하길 바란다.
참고로 필자는 virtual scroll을 구현하기 위해 필요한 "브라우저 윈도우 사이즈"와 "scroll 위치"를 알기 위해 react use 라이브러리를 이용했으므로 참고하길 바란다.
Virtual scroll을 구현하기 위해서는 아래 차례를 따라가면 된다.
참고한 레퍼런스의 사진들을 이용해 각 차례를 하나하나 설명하도록 하겠다.
container의 height를 card list의 높이로 지정해야 한다.
예컨대 card height가 300px이고 card 개수가 10개일 시 container height는 3000px이다.
dom tree에 렌더링할 요소들을 구하기 위해서는 index range를 구해야한다.
이때 index range는 시작과 마지막인 start index와 end index이다.
start index와 end index를 구하는 방법을 차례로 설명하겠다.
start index를 구하기 위해서는 container의 offsetY를 먼저 구한다.
start index는 현재 offsetY와 itemHeight를 나눈 값이다.
이 때 내가 만든 card는 상하좌우에 margin이 걸려있으므로 itemHeight + margin으로 나누어주어야 한다.
또한 사용자가 끊김없이 card list를 볼 수 있도록 node padding을 걸어주어야 하므로 start index에 node padding값을 빼준다.
코드로 설명하면 다음과 같다. renderAhead는 node padding을 의미한다.
const startIndex = Math.max(
Math.floor(offsetY / (itemHeight + columnGap)) - renderAhead,
0
);
end index는 현재 viewport의 height를 itemHeight로 나눈값에 start index를 더한 값이다.
코드로 설명하면 다음과 같다.
const { height } = useWindowSize();
const endIndex = Math.min(
Math.ceil(height / (itemHeight + columnGap) + startIndex) + renderAhead,
children.length
);
모든 card에 대해 사용할 수 있도록 visual scroll의 children으로 card list를 전달해준다.
(대부분의 virtual scroll 예제의 경우 card를 props로 전달한 후 virtual scroll에서 map을 돌리는 식으로 되어있다. 이 경우 해당 card에 대해 virtual scroll이 고정되어 재사용성이 떨어지게 된다. 따라서 children으로 전달해줌으로써 응집성을 제거해준 것이다.)
그 중 dom tree에 붙여야 하는 index range안의 item들을 slice하여 dom tree에 렌더링한다.
코드로 설명하면 다음과 같다.
const visibleItem = children.slice(
Math.max(startIndex, 0),
Math.min(endIndex + 1, children.length)
);
viewport에 따라 다른 card list를 보여주기 위해 translateY를 적용한다.
container div 안에 div 태그를 추가해 해당 translateY를 적용시킨다.
dom tree의 첫 번째 렌더링 요소인 start index를 기준으로 하며, 해당 translateY의 값을 이용해 y축을 shift하여 현재 viewport의 item list들만 보여주게끔 한다.
이를 코드로 설명하면 다음과 같다.
const translateY = Math.max((itemHeight + columnGap) * startIndex, columnGap);
전체 코드는 다음과 같으며 위의 설명을 참고하면서 이해해주길 바란다.
리액트에서는 react-virtualized 라이브러리가 있어 손쉽게 virtual scroll을 구현할 수 있다.
하지만 모든 라이브러리가 그러하듯, 이 라이브러리 또한 몇 가지 케이스에 대해서는 기능을 제공하지 못하는것으로 알고 있다. 사례로 오늘의 집이 있다.
따라서 virtual scroll을 만들고자 하는 독자분들은 해당 라이브러리에서 제공하지 않는 케이스에 대해 구현을 해보는 것을 추천드린다.
virtual scroll을 구현하시는 분들에게 이 글이 조금이라도 도움이 되길 바란다.
(ref)
멋져요!