13. 컴포넌트 최적화 - 리렌더링 제어
12편에서 useSelector가 리렌더를 유발하는 조건을 봤습니다. 이 편은 실제 앱에서 “왜 이 컴포넌트가 필요 이상으로 자주 그려지는가"를 진단하고, 구체적인 세 가지 기법으로 줄이는 방법을 다룹니다.
학습 목표
useSelector의 구독 범위를 좁혀 불필요한 리렌더를 줄일 수 있다.React.memo로 props가 바뀌지 않은 컴포넌트의 리렌더를 건너뛸 수 있다.- 리스트를 렌더링할 때 항목 단위로 컴포넌트를 분리해야 하는 이유를 설명할 수 있다.
문제 진단: React DevTools Profiler로 확인한다
최적화를 하기 전에 실제로 무엇이 얼마나 자주 리렌더되는지 측정해야 합니다. React DevTools의 Profiler 탭에서 “Highlight updates when components render” 옵션을 켜면, 화면에서 리렌더되는 컴포넌트가 실시간으로 표시됩니다. 추측으로 최적화를 시작하면 실제로 문제가 아닌 곳에 시간을 쓰기 쉬우므로, 이 관찰 단계를 건너뛰지 않는 것이 중요합니다.
기법 1: 구독 범위 좁히기
가장 먼저 확인할 것은 컴포넌트가 필요한 것보다 넓은 범위를 구독하고 있지 않은가입니다.
| |
CartBadgeBad는 cart.total이나 cart.discountCode처럼 화면에 표시하지 않는 필드가 바뀌어도 리렌더됩니다. CartBadgeGood은 items.length라는 숫자(원시값)만 구독하므로, 그 값이 실제로 바뀔 때만 리렌더됩니다.
기법 2: React.memo로 props가 안 바뀐 컴포넌트를 건너뛴다
부모 컴포넌트가 리렌더되면 기본적으로 모든 자식도 함께 리렌더됩니다. 자식이 받는 props가 바뀌지 않았다면 React.memo로 이 리렌더를 건너뛸 수 있습니다.
| |
React.memo는 이전 props와 새 props를 얕은 비교로 확인해, 같으면 리렌더를 건너뜁니다. 하지만 함정이 있습니다. 부모가 onToggle 같은 콜백을 매 렌더링마다 새로 만든 함수로 전달하면, 함수의 참조가 매번 달라지므로 React.memo가 무력화됩니다.
| |
이 문제는 useCallback으로 함수 자체를 메모이제이션하거나, 항목별 id를 클릭 핸들러 안에서 클로저로 캡처하지 않고 이벤트 위임으로 처리하는 방식으로 해결합니다.
| |
주의할 점은 onToggle={() => handleToggle(todo.id)}도 여전히 매 렌더링마다 새 화살표 함수를 만든다는 것입니다. 완전히 최적화하려면 id를 TodoItem에 prop으로 넘기고 TodoItem 내부에서 onToggle(id)를 호출하는 형태로 바꿔야 합니다. 이런 세부 조정은 “측정해서 실제로 병목일 때만” 적용하는 편이 좋습니다.
기법 3: 리스트는 항목 단위 컴포넌트로 분리한다
리스트를 렌더링할 때, 각 항목을 별도 컴포넌트로 분리하지 않으면 목록의 상태 하나만 바뀌어도 전체 목록이 다시 그려집니다.
| |
todos 배열에서 항목 하나의 done만 바뀌어도(08편에서 본 것처럼 map()이 새 배열을 반환하므로) todos 배열 참조 전체가 바뀌고, 이 컴포넌트 전체가 리렌더되며 모든 <li>가 다시 그려집니다. 항목을 별도 컴포넌트로 분리하고, 각 컴포넌트가 자신의 id로 필요한 값만 구독하게 하면 이 문제가 해결됩니다.
| |
이 패턴이 14편(Selector 패턴)의 메모이제이션된 selector와 결합되면, 대규모 목록에서도 변경된 항목만 정확히 리렌더되는 구조를 만들 수 있습니다.
최적화의 순서: 측정 → 진단 → 적용
성능 최적화는 다음 순서를 지키는 것이 중요합니다.
- 측정: Profiler로 실제로 리렌더가 과도한 컴포넌트를 찾는다.
- 진단: 구독 범위가 너무 넓은지, props 참조가 매번 바뀌는지, 리스트가 통째로 렌더링되는지 원인을 특정한다.
- 적용: 원인에 맞는 기법(구독 좁히기,
React.memo+useCallback, 항목 분리)만 적용한다.
모든 컴포넌트에 React.memo를 습관적으로 붙이는 것은 오히려 비교 비용만 늘릴 수 있습니다. 측정 없이 최적화부터 하지 않는 것이 원칙입니다.
실무 체크리스트
useSelector가 실제로 화면에 쓰는 값보다 넓은 범위(전체 슬라이스)를 구독하고 있지 않은가?React.memo로 감싼 컴포넌트에 콜백을 전달할 때useCallback으로 참조를 안정시켰는가?- 배열을 렌더링할 때 항목 하나의 변경이 전체 목록의 리렌더로 이어지고 있지 않은가?
- 최적화를 적용하기 전에 Profiler로 실제 병목을 확인했는가?
연습 과제
기초(★☆☆)
CartBadgeBad를 React DevTools Profiler로 관찰해,cart.total만 바뀌어도 리렌더되는지 확인해보세요.
중급(★★☆)
TodoListInline을TodoListSeparated구조로 리팩터링하고, 항목 하나를 토글했을 때 다른 항목들이 리렌더되지 않는지 Profiler로 확인해보세요.
고급(★★★)
- 100개 이상의 항목을 가진 목록에서 항목 분리 전/후의 리렌더 횟수와 소요 시간을 Profiler로 비교하는 실험을 설계해보세요.
요약
- 최적화는 측정으로 시작한다. React DevTools Profiler로 실제 병목을 먼저 찾는다.
useSelector의 구독 범위를 필요한 만큼만 좁히면 대부분의 불필요한 리렌더가 사라진다.React.memo는useCallback과 함께 써야 효과가 있고, 리스트는 항목 단위 컴포넌트로 분리해야 변경이 국소화된다.
참고 문헌 및 출처(추천)
- React 공식 문서, “React.memo” API 레퍼런스
- React 공식 문서, “useCallback” API 레퍼런스
- Redux 공식 문서, “Using Redux with React, Performance”
![Featured image of post [Redux] 13. 컴포넌트 최적화 - 리렌더링 제어](/post/redux/component-optimization/wordcloud_hu_dd21f004729cf228.webp)
![[Redux] 11. React-Redux 시작하기 - Provider와 connect](/post/redux/react-redux-basics/wordcloud_hu_ff09d62dce5f2caf.webp)
![[Redux] 12. React-Redux Hooks - useSelector와 useDispatch](/post/redux/react-redux-hooks/wordcloud_hu_71b8875b416f1345.webp)
![[Redux] 13. 컴포넌트 최적화 - 리렌더링 제어](/post/redux/component-optimization/wordcloud_hu_6a65d0a31ce36910.webp)
![[Redux] 14. 데이터 선택자 - Selector 패턴](/post/redux/selector-patterns/wordcloud_hu_e73d33d3c19de96.webp)
![[Redux] 15. 실습: Counter와 Todo 앱 만들기](/post/redux/practice-counter-todo/wordcloud_hu_5c499868ea4452c8.webp)
![[Redux] 29. Redux 성능 최적화와 디버깅 심화](/post/redux/redux-performance-debugging/wordcloud_hu_d21c809d96be34b8.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[Redux] 08. 불변성의 중요성 - Immutability in Redux](/post/redux/immutability-in-redux/wordcloud_hu_3a5feb2a8eb157f4.webp)