웹 개발
리스트 렌더링이 느릴 때 확인할 것
큰 리스트를 표시하면 브라우저가 느려지는데, 원인은 렌더링, 네트워크, 또는 이벤트 핸들러일 수 있습니다.
화면에 1000개 항목을 렌더링하는데 죽어라 느리다. 스크롤도 끊긴다. 뭐가 문제일까.
먼저 병목 찾기
Chrome DevTools Performance 탭을 켜고 기록한다. 느린 부분은 어디인가?
- 렌더링이 느린가 (황색 그래프)
- 스크립트 실행이 느린가 (빨강)
- 페인팅이 느린가 (초록)
Performance 탭의 Flame Chart를 보면 각 작업이 얼마나 걸렸는지 나온다.
// Performance 측정
console.time('render-list');
// 리스트 렌더링 코드
console.timeEnd('render-list');
렌더링 최적화
1000개 항목을 DOM에 다 추가하면 브라우저가 이들을 모두 계산한다. 대신 화면에 보이는 것만 렌더링한다.
// 나쁜 예
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
// 낫다: 가상 스크롤 라이브러리 사용
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={35}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
가상 스크롤은 화면에 보이는 항목만 DOM에 유지한다. 스크롤할 때 항목을 교체한다.
이벤트 핸들러 최적화
각 리스트 항목에 이벤트 핸들러가 있고, 그 안에서 상태를 바꾸면 모든 항목이 리렌더링될 수 있다.
// 나쁜 예: 부모에서 상태를 관리하면 모든 자식이 리렌더링된다
function List() {
const [selected, setSelected] = useState(null);
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => setSelected(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
// 낫다: 각 항목에서 자신의 상태만 관리
function ListItem({ item }) {
const [isHovered, setIsHovered] = useState(false);
return (
<li
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{item.name}
</li>
);
}
데이터 요청 최적화
리스트 전체를 한 번에 가져오면 초기 로딩이 느리다.
// 나쁜 예: 10000개 항목을 다 가져온다
const [items, setItems] = useState([]);
useEffect(() => {
fetch('/api/items')
.then(res => res.json())
.then(data => setItems(data));
}, []);
// 낫다: 페이지네이션 또는 무한 스크롤
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const loadMore = () => {
fetch(`/api/items?page=${page}&limit=50`)
.then(res => res.json())
.then(data => setItems(prev => [...prev, ...data]))
.then(() => setPage(page + 1));
};
메모리 누수 확인
리스트를 많이 스크롤하다 보면 메모리가 계속 증가할 수 있다. DevTools Memory 탭에서 힙 스냅샷을 찍고 비교한다.
메모리가 줄어들지 않는다면 이벤트 리스너, 타이머, 또는 외부 구독이 정리되지 않은 것이다.
// 나쁜 예: useEffect에서 정리하지 않는다
useEffect(() => {
const handleScroll = () => { /* ... */ };
window.addEventListener('scroll', handleScroll);
// 정리 코드가 없다!
}, []);
// 낫다: 정리 함수로 리스너 제거
useEffect(() => {
const handleScroll = () => { /* ... */ };
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
최종 확인: 작은 리스트부터
100개 항목으로 테스트하고 부드러운가? 1000개로 올려보자. 어느 지점부터 느린가?
마지막으로, 가상 스크롤을 도입하기 전에 먼저 불필요한 리렌더링을 없애는 게 낫다. 최적화는 먼저 측정하고 진단한 후에 한다.