[React] 리렌더링 성능 개선기
2026-02-17 20:25
시작하기
개인 프로젝트에서 발생한 리렌더링 문제와 해결과정에 대해 글을 쓰려고 한다. 요거트볼에 토핑을 올리는 인터렉션을 구현하고 있었는데, 버벅거리는 문제가 있었다.
🤨 문제 상황
토핑을 클릭하면 마우스에 클릭한 토핑이 따라 붙고, 요거트볼에 위치한 토핑은 그대로 유지되야 하는 기능이다.
이때 클릭을 한 토핑을 useState의 상태로 관리하고 있었는데, 마우스 이동마다 전체가 리렌더링 되는 문제가 있었다.
// useState로 마우스 위치 관리
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePos({ x: e.clientX, y: e.clientY }); // 매 이동마다 setState 호출
};
window.addEventListener("mousemove", handleMouseMove);
...
}, []);
// mousePos state가 바뀔 때마다 컴포넌트 전체가 리렌더링
style={{
left: `${mousePos.x - 10}px`,
top: `${mousePos.y - 10}px`,
}}
mousemove는 초당 수십~수백 회 발생한다고 한다. 매번 setMousePos를 호출하면 그때마다 컴포넌트 전체가 리렌더링되어, 배치된 토핑 이미지들까지 전부 불필요하게 다 렌더링되는 문제가 발생한다.
변경전 코드 성능 측정 결과
React DevTools Profiler를 사용해서 성능 측정을 진행했다.
참고로, React DevTools Profiler는
- 어떤 컴포넌트가 리렌더링되고
- 각 렌더링에 걸린 시간(ms)
- 왜 리렌더링이 되었는
비교할 수 있는 툴이다.
마우스가 움직임에 따라 잠깐 사용했지만, 526번이나 리렌더링이 되는 심각한 문제가 발생했다. 한 번에 0.9ms라도 526번이면 총 약 473ms, 거의 0.5초를 이 컴포넌트 렌더링에만 쓴 것이다.
😼 해결 방법
-
useState → useRef로 마우스 위치 관리
const cursorImageRef = useRef<HTMLImageElement>(null); // DOM 직접 참조 const mousePosRef = useRef({ x: 0, y: 0 }); // 위치 값 저장 // mousemove 마다 DOM 직접 조작 mousePosRef.current = { x: e.clientX, y: e.clientY }; cursorImageRef.current.style.left = `${e.clientX - 10}px`; cursorImageRef.current.style.top = `${e.clientY - 10}px`;이전에는 useState로 마우스 위치를 직접 저장했다면, mousemove마다 useRef로 DOM을 직접 조작하도록 변경했다.
React를 거치지 않으니 리렌더링이 일어나지 않는다.
-
mousePosRef 추가 — 토핑 전환 시 위치 보정
useRef로 바꾸니 새로운 문제가 생겼다. 토핑을 바꾸면 새로 마운트되는 커서 이미지가 초기 위치를 모르기 때문에 (0, 0)에 시작하게 됐다.
const cursorImageRef = useRef<HTMLImageElement>(null);
const mousePosRef = useRef({ x: 0, y: 0 });
useEffect(() => {
if (cursorImageRef.current) {
cursorImageRef.current.style.left = ${mousePosRef.current.x - 10}px;
cursorImageRef.current.style.top = ${mousePosRef.current.y - 10}px;
}
}, [selectedTopping]);
mousePosRef에 항상 최신 마우스 위치를 저장해두고, 토핑이 바뀔 때 그 위치로 즉시 배치하도록 했다.
전체 코드
const YogurtBowlComponent = ({ ref }: YogurtBowlProps) => {
const selectedTopping = useAtomValue(selectedToppingAtom);
// ✅ useState 대신 useRef로 마우스 위치 관리 → 리렌더링 방지
const cursorImageRef = useRef<HTMLImageElement>(null);
const mousePosRef = useRef({ x: 0, y: 0 });
// ✅ mousemove마다 DOM 직접 조작
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
mousePosRef.current = { x: e.clientX, y: e.clientY };
if (cursorImageRef.current) {
cursorImageRef.current.style.left = `${e.clientX - 10}px`;
cursorImageRef.current.style.top = `${e.clientY - 10}px`;
}
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// ✅ 토핑 전환 시 현재 마우스 위치로 즉시 배치
useEffect(() => {
if (cursorImageRef.current) {
cursorImageRef.current.style.left = `${mousePosRef.current.x - 10}px`;
cursorImageRef.current.style.top = `${mousePosRef.current.y - 10}px`;
}
}, [selectedTopping]);
return (
<>
...
{/* 커서를 따라다니는 토핑 */}
{selectedTopping && (
<img
src={selectedTopping.image}
alt={selectedTopping.name}
style={{ position: 'fixed', pointerEvents: 'none' }}
ref={cursorImageRef}
/>
)}
</>
);
};
export const YogurtBowl = memo(YogurtBowlComponent);
변경 후 성능 측정
변경 후 리렌더링 횟수가 24로 많이 줄어든 것을 볼 수 있다. 초당 60회 이상의 전체 리렌더가 0회로 줄었고, 토핑이 많아질수록 효과가 더 커진다.
이렇게 React DevTools Profiler을 통해 성능을 측정해보고, 리렌더링을 줄여서 성능 개선한 사례를 알아봤다. mousemove 처럼 빈번한 이벤트는 setState 사용을 줄이는 방법을 고려해야하고 React가 알아서 최적화해주는 것이 아니라, 개발자가 적절한 도구를 선택해야 한다.