불필요한 리렌더를 줄이는 memo·useMemo·useCallback.
성능 최적화
React는 충분히 빠르지만, 앱이 커지면 불필요한 리렌더가 쌓여 느려질 수 있습니다. 언제 최적화가 필요하고, 어떤 도구를 써야 하는지 알아봅시다.
학습 목표
- 컴포넌트가 다시 렌더되는 원인을 안다.
React.memo로 props가 같으면 리렌더를 건너뛸 수 있다.useMemo로 비싼 계산을,useCallback으로 함수를 기억할 수 있다.- 최적화를 언제 써야 하고 언제 하지 말아야 하는지 판단할 수 있다.
리렌더는 왜 일어날까
컴포넌트는 다음 중 하나면 다시 렌더됩니다.
- 자신의 state가 바뀔 때
- 받는 props가 바뀔 때
- 부모가 리렌더될 때 (props가 그대로여도 기본적으로 따라 렌더)
💡 TIP — 리렌더 자체는 나쁜 게 아닙니다. React가 가상 DOM을 비교해 실제 변경만 반영하니까요. "리렌더가 비싼 계산을 동반"하거나 "매우 자주 일어날 때"만 문제가 됩니다.
React.memo — props 같으면 건너뛰기
부모가 렌더돼도, props가 이전과 같으면 자식 렌더를 생략합니다.
import { memo } from 'react';
const Item = memo(function Item({ name }) {
console.log('render', name);
return <li>{name}</li>;
});
memo 는 props를 얕게 비교(===)합니다. 그래서 객체·배열·함수를 props로 넘기면, 매 렌더마다 새로 만들어진 참조 때문에 비교가 항상 "다름"이 됩니다. 여기서 useMemo·useCallback 이 필요해집니다.
useMemo — 비싼 계산 기억하기
의존성이 그대로면 이전 계산 결과를 재사용합니다.
import { useMemo } from 'react';
function ProductList({ products, keyword }) {
const filtered = useMemo(
() => products.filter((p) => p.name.includes(keyword)),
[products, keyword] // 둘 다 그대로면 다시 계산 안 함
);
return <ul>{filtered.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
useMemo 는 객체·배열의 참조 안정화에도 씁니다. memo 자식에 넘길 객체를 감싸면 불필요한 리렌더를 막습니다.
useCallback — 함수 기억하기
useCallback 은 함수의 참조를 유지합니다. memo 된 자식에 콜백을 넘길 때 핵심입니다.
import { useCallback, useState } from 'react';
function List({ items }) {
const [, setLog] = useState([]);
// ❌ 매 렌더 새 함수 → memo된 자식이 매번 리렌더
// const onSelect = (id) => setLog((l) => [...l, id]);
// ✅ 참조 유지 → 자식 리렌더 방지
const onSelect = useCallback((id) => {
setLog((l) => [...l, id]);
}, []);
return items.map((it) => (
<Item key={it.id} item={it} onSelect={onSelect} />
));
}
useCallback(fn, deps) 는 useMemo(() => fn, deps) 와 같다고 보면 됩니다.
| 도구 | 무엇을 기억 | 주 용도 |
|---|---|---|
React.memo | 컴포넌트 | props 같으면 렌더 생략 |
useMemo | 값(계산 결과) | 비싼 계산·참조 안정화 |
useCallback | 함수 | 함수 참조 안정화 |
언제 쓰고 언제 안 쓰나
이 도구들은 공짜가 아닙니다. 비교 비용과 메모리, 그리고 코드 복잡도가 늘어납니다.
쓰면 좋은 경우
- 측정해 보니 실제로 느린 비싼 계산이 매 렌더 반복될 때.
memo된 자식에게 객체·함수 props를 넘겨, 그 자식 리렌더가 비쌀 때.- 리스트가 크고 자주 갱신될 때.
굳이 안 써도 되는 경우
- 계산이 가볍고(작은 배열 map 등) 렌더가 드물 때.
memo로 감싸지 않은 평범한 자식에 함수를 넘길 때 —useCallback만 써봐야 효과 없음.
⚠️ 주의 — "혹시 몰라서" 모든 곳에
useMemo·useCallback을 두르는 건 안티패턴입니다. 먼저 React DevTools Profiler로 측정하고, 병목이 확인된 곳만 최적화하세요.
💡 TIP — React Compiler(React 19와 함께 발전 중)를 쓰면 이런 메모이제이션을 자동으로 처리해 줍니다. 수동 최적화 전에 도구의 도움을 받을 수 있는지 살펴보세요.
요약
- 리렌더는 state·props 변경, 부모 렌더로 일어난다. 리렌더 자체는 정상.
React.memo는 props가 얕게 같으면 자식 렌더를 건너뛴다.useMemo는 값을,useCallback은 함수를 기억해 참조를 안정화한다.memo자식에 객체·함수를 넘길 때useMemo·useCallback이 짝을 이룬다.- 먼저 측정하고, 병목이 있는 곳만 최적화한다.
연습문제
console.log를 넣어 부모 state가 바뀔 때 자식이 함께 리렌더되는지 확인하세요.- 위 자식을
React.memo로 감싸고, 관련 없는 props가 그대로일 때 렌더가 생략되는지 보세요. - 큰 배열을 정렬·필터하는 계산을
useMemo로 감싸 의존성이 같을 때 재계산이 없음을 확인하세요. memo된 자식에 콜백을 넘길 때useCallback유무에 따른 리렌더 차이를 비교하세요.
힌트 — React DevTools의 "Highlight updates"나 Profiler로 리렌더를 눈으로 확인할 수 있습니다.
💡 연습문제 풀이
불러오는 중…
댓글 0
“React.js” 강좌에 대한 댓글입니다.