React 렌더링 최적화 훅 정리: useMemo, useCallback, React.memo, useTransition
메모이제이션은 "이전 결과를 기억해 두고, 입력이 같으면 다시 계산하지 않는 것"이다. React의 최적화 훅은 계산을 아끼거나(useMemo), 참조를 유지하거나(useCallback), 렌더링을 건너뛰거나(memo), 우선순위를 나누는(useTransition) 도구다.
1. 먼저 알아야 할 것: React는 언제 리렌더링하나
| 원인 | 설명 |
| 자신의 state 변경 | setState로 값이 바뀌면 다시 렌더 |
| 부모의 리렌더 | 부모가 렌더되면 자식도 전부 렌더 (props가 같아도) |
| 구독한 Context 변경 | Provider의 value가 바뀌면 그 Context를 쓰는 컴포넌트 렌더 |
여기서 "렌더"는 컴포넌트 함수를 다시 호출해 가상 DOM을 만드는 것이고, 실제 DOM 변경은 달라진 부분만 일어난다. 대부분의 리렌더는 충분히 싸다. 최적화가 필요한 건 계산이 무겁거나 큰 트리가 통째로 다시 렌더될 때다.
2. 참조 동등성 (Referential Equality)
최적화 훅을 이해하는 핵심 개념이다. React는 이전 값과 새 값을 Object.is로 비교한다.
| 값 | 렌더마다 새로 만들면 | 비교 결과 |
| 숫자, 문자열, 불리언 | 값이 같으면 같음 | 같음 |
| 객체 { } | 내용이 같아도 새 객체 | 다름 |
| 배열 [ ] | 내용이 같아도 새 배열 | 다름 |
| 함수 () => { } | 코드가 같아도 새 함수 | 다름 |
그래서 객체나 함수를 props로 넘기면 매 렌더마다 "바뀐 props"가 된다. useMemo와 useCallback은 이 참조를 유지하는 도구다.
3. 한눈에 비교
| 구분 | useMemo | useCallback | React.memo |
| 종류 | 훅 | 훅 | 고차 컴포넌트 (HOC) |
| 기억하는 것 | 계산 결과 값 | 함수 자체 | 컴포넌트의 렌더 결과 |
| 다시 만드는 조건 | 의존성 배열이 바뀔 때 | 의존성 배열이 바뀔 때 | props가 얕은 비교로 다를 때 |
| 주 목적 | 무거운 계산 생략, 객체 참조 유지 | 함수 참조 유지 | 불필요한 자식 리렌더 방지 |
| 형태 | useMemo(() => 값, [deps]) | useCallback(fn, [deps]) | memo(Component) |
useCallback(fn, deps)는 useMemo(() => fn, deps)와 같다. 함수를 빠르게 만드는 게 아니라 같은 함수 객체를 돌려줄 뿐이다.
4. 세 가지는 보통 같이 써야 효과가 난다
memo만 쓰면:onSelect가 매 렌더 새 함수라 props 비교가 항상 실패 → 효과 없음useCallback만 쓰면: 비교하는 쪽이 없으니 효과 없음- 둘 다 쓰면: List가 다시 렌더돼도 Row는 건너뜀
5. 우선순위를 나누는 훅: useTransition, useDeferredValue
메모로 줄일 수 없는 무거운 렌더링은 "급한 업데이트를 먼저 처리"하는 방식으로 대응한다. React 18의 동시성(Concurrent) 렌더링 기능이다.
| 구분 | useTransition | useDeferredValue |
| 감싸는 대상 | state를 바꾸는 코드 (setState) | 값 자체 |
| 쓰는 상황 | setState를 내가 호출할 수 있을 때 | props로 받은 값처럼 setState에 접근 못 할 때 |
| 반환 | [isPending, startTransition] | 늦게 따라오는 값 |
| 효과 | 그 업데이트를 낮은 우선순위로 | 그 값을 쓰는 렌더를 낮은 우선순위로 |
둘 다 렌더링을 빠르게 만드는 게 아니라, 무거운 렌더링 중에도 입력이 끊기지 않게 순서를 바꾸는 것이다.
6. useRef: 렌더링과 무관한 값
| 구분 | useState | useRef |
| 값이 바뀌면 | 리렌더 발생 | 리렌더 없음 |
| 용도 | 화면에 보여야 하는 값 | 타이머 ID, DOM 참조, 이전 값, 측정 시각 |
| 렌더 중 읽기 | 가능 | 권장하지 않음 |
7. 실제로 어떻게 적용되나
- 검색 필터 결과 (useMemo): 상품 수천 개를 정렬·필터링한 결과는 검색어나 원본 목록이 바뀔 때만 다시 계산한다. 무관한 state(모달 열림 등)가 바뀔 때마다 재계산하는 것을 막는다.
- 긴 목록의 행 (memo + useCallback): 행 컴포넌트를 memo로 감싸고, 행에 넘기는 클릭 핸들러를 useCallback으로 고정한다. 한 행을 선택해도 나머지 행은 다시 렌더되지 않는다.
- useEffect 의존성 안정화 (useCallback): 이펙트 안에서 쓰는 함수를 의존성에 넣어야 할 때, useCallback으로 고정하지 않으면 매 렌더마다 이펙트가 재실행된다.
- 검색창 + 무거운 결과 목록 (useDeferredValue): 입력창은 즉시 반영하고, 결과 목록은 지연된 값으로 그려서 타이핑이 버벅이지 않게 한다.
- 반응속도 측정 (useRef): 측정 시각을 state에 두면 저장할 때 렌더가 일어나 측정이 흔들린다. 화면에 보일 필요 없는 값이라 ref에 둔다.
8. 메모이제이션의 비용
- 의존성 비교와 이전 값 저장에 메모리와 시간이 든다.
- 코드가 복잡해지고, 의존성 배열을 잘못 쓰면 오래된 값을 쓰는 버그(stale closure)가 생긴다.
- 그래서 "모든 곳에 감싸기"가 아니라 측정 후 필요한 곳에만 쓴다. React DevTools Profiler로 무엇이 왜 렌더되는지 먼저 본다.
메모 없이 구조로 해결되는 경우도 많다. 자주 바뀌는 state를 그 state를 쓰는 작은 컴포넌트로 내리거나, 무거운 자식을 children으로 넘기면 부모 state가 바뀌어도 그 자식은 다시 만들어지지 않는다.
9. React Compiler
React Compiler(2025년 10월 1.0)는 빌드 시점에 컴포넌트를 분석해 메모이제이션을 자동으로 넣는다. 켜면 손으로 쓰는 useMemo/useCallback 대부분이 필요 없어진다. 단, 컴포넌트가 순수하고 훅 규칙을 지켜야 최적화 대상이 된다.
10. 면접 질문으로 정리
- Q. useMemo와 useCallback의 차이는? useMemo는 계산 결과 값을, useCallback은 함수 자체를 메모이제이션한다. useCallback(fn, deps)는 useMemo(() => fn, deps)와 같다.
- Q. React.memo는 어떻게 동작하나? 컴포넌트를 감싸서, 부모가 리렌더될 때 props를 얕은 비교해 같으면 렌더를 건너뛴다. 객체·함수 props는 참조가 바뀌면 다르다고 판단하므로 useMemo/useCallback과 함께 써야 효과가 있다.
- Q. 모든 곳에 메모이제이션을 하면 안 되는 이유는? 비교와 저장에도 비용이 들고, 단순한 계산이나 자주 바뀌는 값에는 오히려 손해다. 측정 후 병목이 확인된 곳에만 적용한다.
- Q. useTransition은 무엇을 해결하나? 무거운 상태 업데이트를 낮은 우선순위로 표시해, 그 렌더링이 진행되는 동안에도 입력 같은 급한 업데이트가 먼저 처리되게 한다.