본문 바로가기
Q&A해결2026년 8월 14일1분 읽기

React.memo로 감쌌는데도 자식이 계속 리렌더돼요 (props는 안 바뀐 것 같은데)

m
memo_jiu
조회 883댓글 4

부모 컴포넌트에 상태가 하나 있는데, 그게 바뀔 때마다 자식들이 다 리렌더되길래 무거운 자식 하나를 React.memo로 감쌌습니다. 근데도 부모가 리렌더될 때마다 이 자식이 계속 다시 그려져요. React DevTools의 Highlight updates로 보면 매번 깜빡입니다.

자식에 넘기는 props는 이렇게 생겼어요.

<HeavyList
  data={items}
  style={{ marginTop: 8 }}
  onSelect={(id) => setSelected(id)}
/>

data는 그대로고 style이나 onSelect도 겉보기엔 안 바뀌는데 왜 memo가 안 먹는 걸까요? memo가 원래 이런 걸 막아주는 거 아니었나요?

댓글 4

김김영삼2개월 전
props 값은 그대로여도 참조가 매번 새로 만들어져서 그래요. React.memo는 이전 props와 새 props를 얕은 비교(shallow compare)하는데, 지금 넘기는 style={{ marginTop: 8 }}는 부모가 렌더될 때마다 새 객체 리터럴이라 이전 것과 다른 참조예요. onSelect의 인라인 화살표 함수도 마찬가지로 매 렌더마다 새 함수고요. 얕은 비교에서 이 둘이 계속 "바뀐 prop"으로 잡히니 memo가 무력화됩니다. data(items)가 안정적이어도 나머지 둘 때문에 다시 그려지는 거예요.

그래서 자식에 넘기는 참조들을 안정화해줘야 합니다. onSelect는 useCallback으로 감싸고, setSelected만 부르는 거면 의존성 배열은 비워도 돼요(setter는 안정적이라). style 객체는 컴포넌트 밖에 상수로 빼두거나 useMemo로 고정하면 됩니다. 아예 marginTop 정도는 CSS 클래스로 옮기는 게 제일 깔끔하고요. 이렇게 세 prop 다 참조가 유지되면 그때 memo가 제대로 작동합니다.
mmemo_jiu(guest)2개월 전
원인은 이해했어요. 근데 이걸 다 하려니까 onSelect마다 useCallback 붙이고 객체마다 useMemo 빼고... 컴포넌트가 좀 지저분해지네요 😅 자식에 뭐 넘길 때마다 이걸 다 해줘야 하는 건가요? 뭔가 배보다 배꼽이 큰 느낌이라서요.
김김영삼2개월 전
아니요, 다 붙일 필요 전혀 없습니다. 오히려 습관적으로 전부 감싸는 게 더 안 좋아요. useCallback/useMemo/memo 자체도 비교 비용과 의존성 관리 부담이 있어서, 실제로 리렌더가 성능 문제를 일으키는 무거운 자식에만 선택적으로 적용하는 게 맞습니다. 대부분의 컴포넌트는 리렌더가 좀 돼도 눈에 띄는 비용이 없어요.

그리고 memo에 기대기 전에 구조로 푸는 방법이 더 깔끔할 때가 많아요. 자주 바뀌는 상태를 그 상태가 실제로 필요한 하위 컴포넌트로 내려버리면(상태를 밑으로 밀기), 부모가 통째로 리렌더되는 일 자체가 줄어서 memo가 필요 없어집니다. 또 무거운 트리를 children으로 받는 패턴을 쓰면, 부모 상태가 바뀌어도 children으로 전달된 부분은 참조가 유지돼서 다시 안 그려져요.

정리하면 지금 HeavyList처럼 진짜 무겁고 자주 부모 때문에 덩달아 그려지는 놈만 memo + 참조 안정화 해주시고, 나머지는 그냥 두세요. DevTools Profiler로 실제 병목을 확인한 다음 그 지점에만 적용하는 게 조기 최적화를 피하는 길입니다.
mmemo_jiu(guest)2개월 전
아 무조건 다 감싸는 게 아니라 무거운 것만 골라서 하는 거군요. children 패턴이랑 상태 내리기 얘기가 특히 도움됐어요. Profiler로 짚어보고 HeavyList만 처리했더니 딱 필요한 만큼만 깔끔해졌습니다. 감사합니다!
Ctrl+Enter로 등록