React를 조금이라도 써봤다면 “리렌더링”이라는 단어를 수도 없이 들었을 겁니다.
리렌더링은 React의 핵심 동작이자, 동시에 가장 큰 성능 병목이 되기도 합니다.
이번 글에서는 실제 프로젝트에서 리렌더링 때문에 성능이 떨어졌던 경험을 바탕으로,
그 원인을 어떻게 파악하고 최적화했는지 정리했습니다.
성능이 떨어지는 진짜 이유
React는 상태(state)나 props가 변경되면 컴포넌트를 다시 렌더링합니다.
이건 의도된 동작이지만, 규모가 커지면 불필요한 렌더링이 눈덩이처럼 불어납니다.
예를 들어 이런 상황을 본 적 있을 겁니다.
하나의 상위 컴포넌트가 있고, 그 안에 수십 개의 하위 컴포넌트가 있는 구조에서
상태가 변경되면, 전혀 관련 없는 하위 컴포넌트들까지 모두 다시 그려지는 현상 말이죠.
제가 실제로 작업하던 프로젝트에서는
“알림창 열림 여부” 하나 바뀌었을 뿐인데,
대시보드 전체가 리렌더링되는 문제가 있었습니다.
React.memo로 불필요한 렌더링 막기
이럴 때 가장 먼저 적용할 수 있는 게 **React.memo**입니다.
React.memo는 props가 바뀌지 않는 한, 컴포넌트를 다시 렌더링하지 않습니다.
const UserCard = React.memo(function UserCard({ name, age }) {
console.log('렌더링됨:', name);
return (
<div>
<strong>{name}</strong> ({age})
</div>
);
});
이제 상위 컴포넌트의 다른 상태가 바뀌더라도,
UserCard의 props가 그대로라면 다시 렌더링되지 않습니다.
단, React.memo는 얕은 비교(shallow compare)만 수행합니다.
즉, 객체나 배열을 props로 넘길 때는 매번 새로운 참조값이 만들어져
렌더링이 다시 일어날 수 있습니다.
이 문제는 다음에서 해결합니다.
useCallback과 useMemo로 참조 안정화
React에서 함수나 배열, 객체를 props로 넘길 때
매 렌더링마다 새로운 참조값이 만들어집니다.
예를 들어 아래 코드를 보세요.
function Parent() {
const handleClick = () => console.log('click');
return <Child onClick={handleClick} />;
}
겉보기엔 같은 함수지만,
렌더링될 때마다 handleClick이 새로 만들어져 Child 컴포넌트는 매번 리렌더링됩니다.
이럴 땐 useCallback을 사용해야 합니다.
function Parent() {
const handleClick = useCallback(() => console.log('click'), []);
return <Child onClick={handleClick} />;
}
useCallback은 의존성 배열이 바뀌지 않는 한
같은 함수 참조를 유지해줍니다.
비슷하게, 배열이나 객체도 useMemo로 감싸면 동일한 효과를 얻을 수 있습니다.
const columns = useMemo(() => [
{ key: 'name', label: '이름' },
{ key: 'age', label: '나이' },
], []);
이렇게 하면 렌더링이 일어날 때마다 새로운 객체가 생성되지 않습니다.
최적화 적용 전후 비교
한 프로젝트에서 React.memo + useCallback + useMemo를 적용하기 전후를 비교해본 적이 있습니다.
데이터 테이블을 렌더링할 때, 100개의 행이 동시에 리렌더링되던 부분이
적용 후에는 4~5개의 셀만 다시 렌더링되었고, FPS가 30 → 58로 올랐습니다.
이건 눈으로도 느껴질 정도였습니다.
스크롤이 부드러워지고, 필터를 변경할 때 딜레이가 사라졌죠.
특히 리스트나 대시보드 같은 UI에서는 이런 미세한 최적화가 체감이 큽니다.
불필요한 리렌더링을 탐지하는 법
그럼 어디서 리렌더링이 많이 일어나는지 어떻게 알 수 있을까요?
크롬 개발자 도구의 “React Developer Tools”를 쓰면 됩니다.
- React DevTools → Profiler 탭으로 이동
- “Record” 버튼 클릭 후 앱을 조작
- 리렌더링된 컴포넌트들이 색으로 표시됨
색이 자주 바뀌는 컴포넌트일수록
불필요한 리렌더링이 발생하고 있을 가능성이 높습니다.
저는 보통 Profiler로 확인 후,
해당 부분에 React.memo를 적용하거나
useCallback으로 함수 참조를 안정화하는 식으로 해결합니다.
가상화(Virtualization)로 대용량 데이터 최적화
만약 데이터가 수천 개 이상이라면
렌더링 자체를 줄이는 것도 방법입니다.
이럴 땐 react-window나 react-virtualized 같은 라이브러리를 사용합니다.
이 라이브러리들은 실제로 보이는 영역만 렌더링하고,
스크롤할 때 필요한 부분만 동적으로 교체합니다.
npm install react-window
import { FixedSizeList as List } from 'react-window';
function BigList({ items }) {
return (
<List
height={400}
itemCount={items.length}
itemSize={35}
width={300}
>
{({ index, style }) => (
<div style={style}>{items[index]}</div>
)}
</List>
);
}
이 방식은 브라우저 DOM 부하를 크게 줄여줍니다.
1000개 이상의 데이터도 부드럽게 스크롤되죠.
실무에서 느낀 점
성능 최적화는 “한 번에 끝내는 작업”이 아닙니다.
어느 시점엔 최적화가 필요 없던 코드도,
기능이 늘어나면 병목 구간이 생기기 마련입니다.
제가 최근에 겪은 사례로,
Zustand와 React Query로 구성한 대시보드에서
API 데이터가 갱신될 때 리렌더링이 동시에 여러 곳에서 일어나던 문제가 있었습니다.
Profiler로 추적해보니,
props로 내려주는 콜백 함수들이 매번 새로 만들어지는 게 원인이었죠.
useCallback과 useMemo를 적용한 후,
렌더링 횟수가 절반 이하로 줄었습니다.
React 성능 최적화의 핵심은
“무엇을 렌더링할지”보다 “언제 렌더링할지”를 제어하는 것입니다.
React.memo, useCallback, useMemo는 그 제어권을 개발자에게 돌려줍니다.
다음 글에서는 이 최적화 개념을 기반으로,
**실제 렌더링 흐름(렌더 → 커밋 → 업데이트)**을 이해하고
React가 내부적으로 상태를 어떻게 처리하는지 깊게 살펴보겠습니다.
React의 렌더링 구조를 이해하면,
왜 이런 최적화가 필요한지도 훨씬 명확해집니다.
'frontend > react' 카테고리의 다른 글
| [React] React.js 강좌 30. useEffect 완전 정복 — 렌더링 이후에 일어나는 일들 (0) | 2025.11.11 |
|---|---|
| [React] React.js 강좌 29. React의 렌더링 흐름 깊이 이해하기 (0) | 2025.11.11 |
| [React] React.js 강좌 27. React Query와 Zustand를 함께 사용하는 구조 설계 (0) | 2025.11.11 |
| [React] React.js 강좌 26. Zustand로 가벼운 전역 상태 관리하기 (0) | 2025.11.11 |
| [React] React.js 강좌 24. Suspense와 Error Boundary로 비동기 렌더링 다루기 (0) | 2025.11.11 |