기록을 불러오는 중입니다...
map 을 사용해 카드 UI를 렌더링했었는데요, 성능 최적화를 위해 가상 스크롤을 구현하고 싶었습니다. import { useEffect, useState, useCallback } from 'react'
interface UseVirtualGridProps {
itemCount: number
containerRef: React.RefObject<HTMLElement>
itemHeight: number
getColumnCount: () => number
overscan?: number
}
export function useVirtualGrid({
itemCount,
containerRef,
itemHeight,
getColumnCount,
overscan = 1,
}: UseVirtualGridProps) {
const [scrollTop, setScrollTop] = useState(0)
const [containerHeight, setContainerHeight] = useState(0)
// 스크롤 이벤트 핸들러
const onScroll = useCallback(() => {
if (containerRef.current) {
setScrollTop(containerRef.current.scrollTop)
}
}, [containerRef])
// 스크롤 이벤트 핸들러 등록
useEffect(() => {
const el = containerRef.current
if (!el) return
const handleResize = () => {
setContainerHeight(el.clientHeight)
}
handleResize()
el.addEventListener('scroll', onScroll)
window.addEventListener('resize', handleResize)
return () => {
el.removeEventListener('scroll', onScroll)
window.removeEventListener('resize', handleResize)
}
}, [containerRef, onScroll])
// 레이아웃
const columnCount = getColumnCount()
const rowCount = Math.ceil(itemCount / columnCount)
const totalHeight = rowCount * itemHeight
const startRow = Math.floor(scrollTop / itemHeight)
const visibleRowCount = Math.ceil(containerHeight / itemHeight)
const start = Math.max(0, (startRow - overscan) * columnCount)
const end = Math.min(itemCount, (startRow + visibleRowCount + overscan) * columnCount)
return {
visibleRange: { start, end },
totalHeight,
scrollTop,
}
}const gridRef = useRef<HTMLDivElement>(null)
const itemHeight = 200
const getColumnCount = () => Math.floor(window.innerWidth / 300) || 1
const { visibleRange, totalHeight } = useVirtualGrid({
itemCount: items.length,
containerRef: gridRef,
itemHeight,
getColumnCount,
})
return (
<div ref={gridRef} style={{ overflowY: 'auto' }}>
<div style={{ height: totalHeight, position: 'relative' }}>
{items.slice(visibleRange.start, visibleRange.end).map((item, idx) => {
const index = visibleRange.start + idx
const row = Math.floor(index / getColumnCount())
const col = index % getColumnCount()
return (
<div
key={item.id}
style={{
position: 'absolute',
top: row * itemHeight,
left: `${(100 / getColumnCount()) * col}%`,
width: `${100 / getColumnCount()}%`,
height: itemHeight,
}}
>
{item.content}
</div>
)
})}
</div>
</div>
)import { useEffect, useState, useCallback } from 'react'
interface UseVirtualGridProps {
itemCount: number
containerRef: React.RefObject<HTMLElement>
itemWidth: number
itemHeight: number
gap: number
overscan?: number
}
export function useVirtualGrid({
itemCount,
containerRef,
itemWidth,
itemHeight,
gap,
overscan = 1,
}: UseVirtualGridProps) {
// ...
// column 개수 계산
const columnCount = Math.max(
1,
Math.floor((containerSize.width + gap) / (itemWidth + gap)),
)
const rowCount = Math.ceil(itemCount / columnCount)
const totalHeight = rowCount * itemHeight + (rowCount - 1) * gap
const startRow = Math.floor(scrollTop / (itemHeight + gap))
const visibleRowCount = Math.ceil(containerSize.height / (itemHeight + gap))
const start = Math.max(0, (startRow - overscan) * columnCount)
const end = Math.min(itemCount, (startRow + visibleRowCount + overscan) * columnCount)
return {
visibleRange: { start, end },
totalHeight,
columnCount,
scrollTop,
}
}getColumnCount 를 인자로 받아서 호출하던 기존 방식에서, 레이아웃을 기반으로 열의 수를 직접 계산하도록 바꿨습니다.items.slice(visibleRange.start, visibleRange.end).map((item, idx) => {
const index = visibleRange.start + idx;
const row = Math.floor(index / columnCount);
const col = index % columnCount;
return (
<div
key={item.id}
style={{
position: 'absolute',
top: row * (itemHeight + gap),
left: col * (itemWidth + gap),
width: itemWidth,
height: itemHeight,
}}
>
{item.content}
</div>
)
});top, left 를 계산헤서 렌더링을 하면 됩니다.top 과 left 대신 transform 을 이용해서 위치를 조정해줘야 합니다.style={{
position: "absolute",
top: 0,
left: 0,
width: itemWidth,
height: itemHeight,
transform: `translate(${col * (itemWidth + gap)}px, ${row * (itemHeight + gap)}px)`,
}}top , left 속성을 transform 으로 옮겨줬습니다.const [mounted, setMounted] = useState(false);
const intervalIdRef = useRef<number>(0);
useEffect(() => {
intervalIdRef.current = setInterval(() => {
if (containerRef.current) {
setMounted(true);
clearInterval(intervalIdRef.current);
}
}, 0);
return () => {
clearInterval(intervalIdRef.current);
};
}, [containerRef]);grid 스타일을 적용한 “진짜 그리드 스타일” 가상 스크롤이였거든요.