← 개인 공부 · Frontend
STUDY NOTE · 개념 정리

React 렌더링 최적화 훅 정리: useMemo, useCallback, React.memo, useTransition

Frontend
메모이제이션은 "이전 결과를 기억해 두고, 입력이 같으면 다시 계산하지 않는 것"이다. 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. 한눈에 비교

구분useMemouseCallbackReact.memo
종류훅훅고차 컴포넌트 (HOC)
기억하는 것계산 결과 값함수 자체컴포넌트의 렌더 결과
다시 만드는 조건의존성 배열이 바뀔 때의존성 배열이 바뀔 때props가 얕은 비교로 다를 때
주 목적무거운 계산 생략, 객체 참조 유지함수 참조 유지불필요한 자식 리렌더 방지
형태useMemo(() => 값, [deps])useCallback(fn, [deps])memo(Component)

useCallback(fn, deps)는 useMemo(() => fn, deps)와 같다. 함수를 빠르게 만드는 게 아니라 같은 함수 객체를 돌려줄 뿐이다.

4. 세 가지는 보통 같이 써야 효과가 난다

const Row = memo(function Row({ item, onSelect }) {
return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});

function List({ items }) {
const [selected, setSelected] = useState(null);
const onSelect = useCallback((id) => setSelected(id), []);
return items.map((it) => <Row key={it.id} item={it} onSelect={onSelect} />);
}
  1. memo만 쓰면: onSelect가 매 렌더 새 함수라 props 비교가 항상 실패 → 효과 없음
  2. useCallback만 쓰면: 비교하는 쪽이 없으니 효과 없음
  3. 둘 다 쓰면: List가 다시 렌더돼도 Row는 건너뜀

5. 우선순위를 나누는 훅: useTransition, useDeferredValue

메모로 줄일 수 없는 무거운 렌더링은 "급한 업데이트를 먼저 처리"하는 방식으로 대응한다. React 18의 동시성(Concurrent) 렌더링 기능이다.

구분useTransitionuseDeferredValue
감싸는 대상state를 바꾸는 코드 (setState)값 자체
쓰는 상황setState를 내가 호출할 수 있을 때props로 받은 값처럼 setState에 접근 못 할 때
반환[isPending, startTransition]늦게 따라오는 값
효과그 업데이트를 낮은 우선순위로그 값을 쓰는 렌더를 낮은 우선순위로

둘 다 렌더링을 빠르게 만드는 게 아니라, 무거운 렌더링 중에도 입력이 끊기지 않게 순서를 바꾸는 것이다.

6. useRef: 렌더링과 무관한 값

구분useStateuseRef
값이 바뀌면리렌더 발생리렌더 없음
용도화면에 보여야 하는 값타이머 ID, DOM 참조, 이전 값, 측정 시각
렌더 중 읽기가능권장하지 않음

7. 실제로 어떻게 적용되나

  1. 검색 필터 결과 (useMemo): 상품 수천 개를 정렬·필터링한 결과는 검색어나 원본 목록이 바뀔 때만 다시 계산한다. 무관한 state(모달 열림 등)가 바뀔 때마다 재계산하는 것을 막는다.
  2. 긴 목록의 행 (memo + useCallback): 행 컴포넌트를 memo로 감싸고, 행에 넘기는 클릭 핸들러를 useCallback으로 고정한다. 한 행을 선택해도 나머지 행은 다시 렌더되지 않는다.
  3. useEffect 의존성 안정화 (useCallback): 이펙트 안에서 쓰는 함수를 의존성에 넣어야 할 때, useCallback으로 고정하지 않으면 매 렌더마다 이펙트가 재실행된다.
  4. 검색창 + 무거운 결과 목록 (useDeferredValue): 입력창은 즉시 반영하고, 결과 목록은 지연된 값으로 그려서 타이핑이 버벅이지 않게 한다.
  5. 반응속도 측정 (useRef): 측정 시각을 state에 두면 저장할 때 렌더가 일어나 측정이 흔들린다. 화면에 보일 필요 없는 값이라 ref에 둔다.

8. 메모이제이션의 비용

  1. 의존성 비교와 이전 값 저장에 메모리와 시간이 든다.
  2. 코드가 복잡해지고, 의존성 배열을 잘못 쓰면 오래된 값을 쓰는 버그(stale closure)가 생긴다.
  3. 그래서 "모든 곳에 감싸기"가 아니라 측정 후 필요한 곳에만 쓴다. React DevTools Profiler로 무엇이 왜 렌더되는지 먼저 본다.

메모 없이 구조로 해결되는 경우도 많다. 자주 바뀌는 state를 그 state를 쓰는 작은 컴포넌트로 내리거나, 무거운 자식을 children으로 넘기면 부모 state가 바뀌어도 그 자식은 다시 만들어지지 않는다.

9. React Compiler

React Compiler(2025년 10월 1.0)는 빌드 시점에 컴포넌트를 분석해 메모이제이션을 자동으로 넣는다. 켜면 손으로 쓰는 useMemo/useCallback 대부분이 필요 없어진다. 단, 컴포넌트가 순수하고 훅 규칙을 지켜야 최적화 대상이 된다.

10. 면접 질문으로 정리

  1. Q. useMemo와 useCallback의 차이는? useMemo는 계산 결과 값을, useCallback은 함수 자체를 메모이제이션한다. useCallback(fn, deps)는 useMemo(() => fn, deps)와 같다.
  2. Q. React.memo는 어떻게 동작하나? 컴포넌트를 감싸서, 부모가 리렌더될 때 props를 얕은 비교해 같으면 렌더를 건너뛴다. 객체·함수 props는 참조가 바뀌면 다르다고 판단하므로 useMemo/useCallback과 함께 써야 효과가 있다.
  3. Q. 모든 곳에 메모이제이션을 하면 안 되는 이유는? 비교와 저장에도 비용이 들고, 단순한 계산이나 자주 바뀌는 값에는 오히려 손해다. 측정 후 병목이 확인된 곳에만 적용한다.
  4. Q. useTransition은 무엇을 해결하나? 무거운 상태 업데이트를 낮은 우선순위로 표시해, 그 렌더링이 진행되는 동안에도 입력 같은 급한 업데이트가 먼저 처리되게 한다.
Gunmo Lee
React 렌더링 최적화 훅 정리: useMemo, useCallback, React.memo, useTransition | Gunmo's Dev Life