react · high priority
React 최적화 원리 — 리렌더 규칙, 메모이즈의 비용, React Compiler
`memo`/`useMemo`/`useCallback` 의 진짜 비용과 "언제 안 쓰는가"
학습 개요
탄생 배경
쉬운 설명
복잡한 개념을 실생활 비유로 설명합니다.
“단체 사진 다시 찍기”
한 명이 자세를 바꿀 때마다 모두 다시 찍어 두는 것이 React 의 기본 동작입니다. `React.memo` 는 "내 옷이 그대로면 다시 찍지 마세요" 라는 표지판이고, `useMemo`/`useCallback` 은 "내가 들고 있는 손가방·미소 의 *모양과 동일성* 을 유지" 하는 약속입니다. Compiler 는 사진사가 자동으로 그런 표지판을 들어주는 도구이고, 우선순위(`useTransition`) 는 "VIP 가 들어오면 내 사진은 미루고 그쪽부터" 라는 다른 축의 도구입니다.
핵심 개념
| 원인 | 발동 조건 | 메모이즈와의 관계 |
|---|---|---|
| 자기 state 변경 | setState 가 이전과 다른 값을 푸시 | memo 로도 못 막는다 — 본인 렌더는 본인 책임 |
| 받은 props 변경 | React.memo 적용 시에만 비교가 의미 있음 | 얕은 비교 — 객체/배열/함수는 참조 동일성이 핵심 |
| 부모 렌더 | *기본값* — props 가 같아도 자식 렌더 | React.memo 가 필요한 유일한 이유 |
| Context value 변경 | 구독한 모든 소비자 | memo 로는 못 막는다 — Context 분리 또는 selector 패턴 |
"props 가 안 바뀌면 자식이 안 그려진다" 는 잘못된 모델
React 의 *기본 동작* 은 "부모 렌더 → 자식도 렌더" 입니다. props 가 같아도, 함수가 같아도, 자식은 다시 렌더 함수가 호출됩니다. *이 기본을 깨려면* React.memo 가 필요하고, memo 가 의미 있으려면 props 의 *참조 동일성* 까지 챙겨야 합니다.
1function Parent() {2 const [n, setN] = useState(0);3 return (4 <>5 <button onClick={() => setN(n + 1)}>{n}</button>6 <Child name="알리스" /> {/* 같은 문자열 props */}7 </>8 );9}10const Child = ({ name }: { name: string }) => {11 console.log('Child render');12 return <div>{name}</div>;13};14// 버튼을 5번 누르면 'Child render' 가 몇 번 찍히는가?
정답
6 번 (초기 마운트 1 + 클릭 5).
해설
props 가 같아도 부모가 렌더되면 자식도 렌더 함수가 호출됩니다. Child 를 React.memo(Child) 로 감싸면 props 가 동일하므로 5 번의 클릭은 자식 렌더를 건너뛰고 콘솔에는 1 번만 찍힙니다.
흔한 오답
- "문자열 같은 원시값 props 면 자동으로 안 그려진다" — 아니다. 부모 렌더는 자식 렌더를 *기본적으로* 끌고 온다.
- "
memo안 붙여도 React 가 알아서 비교한다" — 아니다. 비교 자체가 비용이라 React 는 명시 옵트인이다 (Compiler 가 자동화하기 전까지는).
실무 적용
어떤 상황에서 사용하는가
대시보드 페이지에서 토글 한 번에 200 개 이상의 자식 컴포넌트가 렌더되어 INP 가 300 ms 를 넘었다는 RUM 경고가 들어왔다.
어떻게 적용하는가
(1) Profiler 로 토글 → 어떤 트리가 리렌더되는지 캡처. (2) 광범위 Context 가 원인이면 *영향 범위가 작은 슬라이스* 로 Context 분리 또는 외부 스토어 + selector 도입. (3) memo 된 자식인데도 깨지는 노드는 props 의 인라인 객체/함수를 useMemo/useCallback 으로 안정화. (4) 정렬·필터 같은 큰 계산은 `useMemo` 로 메모이즈하거나 Web Worker 로 이동. (5) 같은 토글 흐름을 React Compiler 가 적용된 빌드에서 다시 측정해 손으로 쓴 메모이즈가 여전히 가치가 있는지 검증.
흔한 실수와 안티패턴
- 모든 컴포넌트에 `React.memo` 를 일괄 적용 — 비교 비용만 늘리고 이득은 핫패스에만 있다.
- deps 배열을 빈 배열로 두고 안에서 최신 state 를 참조 — stale closure 버그.
- `useCallback` 의 함수가 큰 객체를 캡처해 메모리 누수.
- Context value 를 매 렌더 새 객체로 만들면서 Provider 위치만 옮김.
- Compiler 적용 후 손 메모이즈를 *통째로* 지우는 광범위 리팩터링 — 회귀 시 원인 추적이 어려워진다.
흔한 오해
"`React.memo` 는 항상 좋다."
교정비교 비용 + 메모리 보존 비용이 따라온다. 핫패스에만.
왜 중요props 가 자주 바뀌는 컴포넌트에는 비교만 더 늘어 손해다. 측정 후 적용해야 한다.
"`useCallback` 은 함수 컴포넌트의 모든 함수에 붙여야 한다."
교정*memo 된 자식의 props* 로 들어가거나 *외부 시스템에 등록* 되는 함수에만 의미가 있다.
왜 중요단순한 onClick 핸들러를 useCallback 으로 감싸도 자식이 memo 가 아니면 어차피 자식은 매번 렌더된다. 비용만 늘 뿐.
"React Compiler 가 나오면 메모이즈는 다 사라진다."
교정Compiler 가 못 보는 경계(외부 라이브러리, Worker, Context value 의도 표현 등) 에 사람이 쓰는 자리가 남는다.
왜 중요Compiler 는 사용자 코드의 정적 패턴만 메모이즈한다. React 트리 바깥과의 참조 동일성, 명시적 의도 표현은 여전히 사람의 몫이다.
면접 질문
답변 방향 힌트
"부모가 그려지면 자식도 그려진다" 가 *기본* 임을 깔고 시작하세요.
반드시 언급할 키워드
- 자기 state 변경 — 본인 책임, memo 로 못 막음
- 받은 props 변경 — `React.memo` 의 얕은 비교 대상
- 부모 렌더 — `React.memo` 가 *유일한* 차단 도구
- Context value 변경 — Context 분리 / selector / 외부 스토어
- 얕은 비교를 깨는 원흉은 인라인 객체/배열/함수
예상 꼬리 질문
- Context value 객체를 useMemo 로 감싸면 광범위 리렌더가 해결되나요?
- children prop 이 매 렌더 새 React 엘리먼트라는 사실은 어떻게 활용할 수 있나요?
자기 점검
`React.memo` 가 *유일하게* 막을 수 있는 리렌더 원인은 무엇인가?
기대 키워드
자주 하는 오해
"props 변경 자체를 막는다" 는 틀렸다 — props 가 진짜로 바뀌면 당연히 다시 그려진다.
`useCallback` 이 무용지물인 대표 케이스 한 가지를 한 문장으로 답하라.
기대 키워드
자주 하는 오해
"모든 함수는 useCallback 에 감싸야 한다" 는 잘못된 일반화.
React Compiler 가 자동 메모이즈해도 *사람이 직접* 써야 하는 상황 한 가지는?
기대 키워드
자주 하는 오해
"Compiler 가 나오면 메모이즈가 0 으로 줄어든다" 는 과장.
학습 자료
- `React.memo` — Reference얕은 비교의 의미론, areEqual 의 시그니처, 흔한 안티 패턴을 공식 문서에서 정리.Docreact.dev
- `useMemo` — Reference"값을 캐시하고 싶을 때" 라는 정확한 용처와, *언제 사용하지 말아야 하는지* 까지 다룬 문서.Docreact.dev
- React Compiler — IntroductionCompiler 가 자동화하는 메모이즈의 종류와 도입 가이드.Docreact.dev
- React Compiler v1.0 Release1.0 안정화 발표글 — 자동 메모이즈의 적용 범위와 escape hatch 설명.Blogreact.dev · 2025
- React Compiler & React 19 — forget about memoization soon?Compiler 가 흡수하는 패턴과 *남는* 손 메모이즈 사례를 코드로 비교.Blogdeveloperway.com