React.memo + useCallback 조합으로 컴포넌트 리렌더링을 막아보자!
🧠 React.memo + useCallback 조합으로 컴포넌트 리렌더링을 막아보자!
React 개발을 하다 보면 종종 이런 경험 해보셨을 거예요.
“버튼 하나 눌렀을 뿐인데, 자식 컴포넌트가 자꾸 리렌더링돼요… 😫”
그럴 때 사용하는 두 가지 무기! 바로 React.memo와 useCallback입니다.
이번 글에서는 이 둘을 언제, 왜, 어떻게 조합해서 써야 하는지를 쉽고 정확하게 알려드릴게요.
🧩 기본 개념부터 다시 정리
✅ React.memo
함수형 컴포넌트를 메모이제이션해서, props가 바뀌지 않으면 리렌더링을 막는 고차 컴포넌트(HOC)입니다.
✅ useCallback
함수를 메모이제이션해서, 참조값(reference)이 변경되지 않도록 해주는 React 훅이에요.
이 둘을 조합하면? 자식 컴포넌트에게 콜백을 props로 넘길 때 리렌더링을 방지할 수 있습니다! 🎉
💥 문제 상황 예시: 함수를 props로 넘기면?
// 자식 컴포넌트
const Child = React.memo(({ onClick }) => {
console.log("Child 렌더링");
return <button onClick={onClick}>클릭</button>;
});
// 부모 컴포넌트
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log("버튼 클릭!");
};
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>부모 증가</button>
</>
);
}
문제는 여기서 부모 컴포넌트가 리렌더링될 때마다 handleClick 함수도 새로 생성된다는 거예요.
그 결과 React.memo가 props가 바뀌었다고 판단하고 자식도 함께 리렌더링되죠.
✅ 해결책: useCallback으로 함수 메모이제이션
const handleClick = useCallback(() => {
console.log("버튼 클릭!");
}, []);
이렇게 작성하면 handleClick 함수가 항상 같은 참조를 유지하게 됩니다.
이제 자식 컴포넌트는 진짜 props가 바뀔 때만 리렌더링되죠!
전체 코드 예시:
const Child = React.memo(({ onClick }) => {
console.log("Child 렌더링");
return <button onClick={onClick}>클릭</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("버튼 클릭!");
}, []);
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>부모 증가</button>
</>
);
}
Parent의 state(count)가 바뀌어도 Child는 리렌더링되지 않습니다! 🎯
📌 실무에서 자주 쓰는 패턴
- 자식 컴포넌트가 props로 콜백을 받는 경우
- 렌더링 비용이 큰 리스트 요소
- React.memo로 감싼 컴포넌트가 불필요하게 리렌더링되는 걸 막고 싶을 때
예: 쇼핑몰에서 상품 카드 클릭 처리, 토글 버튼 등
📚 useCallback + React.memo 시리즈 패턴
// 자식 컴포넌트
const ProductItem = React.memo(({ product, onAddToCart }) => {
console.log("ProductItem 렌더링");
return (
<div>
<p>{product.name}</p>
<button onClick={() => onAddToCart(product.id)}>담기</button>
</div>
);
});
// 부모 컴포넌트
function ProductList({ products }) {
const [cart, setCart] = useState([]);
const handleAdd = useCallback((id) => {
setCart((prev) => [...prev, id]);
}, []);
return (
<>
{products.map((p) => (
<ProductItem key={p.id} product={p} onAddToCart={handleAdd} />
))}
</>
);
}
이 구조에서는 ProductList가 리렌더링되더라도 handleAdd 함수는 고정되어 있어서, React.memo
로 감싼 ProductItem
컴포넌트가 불필요하게 다시 렌더링되지 않아요!
⚠️ 주의할 점
- useCallback 남용 금지! 너무 자주 쓰면 코드 가독성 떨어지고, 실제 성능 향상도 없음
- 정말 리렌더링 병목 구간에서만 쓰는 것이 좋음
- React.memo는 props 변경 여부만 체크한다는 점 잊지 말기!
🧠 최적화 흐름 요약
- 성능 병목 구간 발견 (React DevTools로 확인)
- 자식 컴포넌트에 React.memo 적용
- props로 전달하는 함수 → useCallback으로 고정
- 객체/배열 props → useMemo로 고정
📚 추천 공식 리소스
✅ 마무리
React.memo와 useCallback을 함께 사용하면 불필요한 컴포넌트 리렌더링을 방지할 수 있습니다.
하지만 무조건 적용한다고 좋은 건 아니고, 정말 렌더링 비용이 클 때만 전략적으로 사용하는 것이 핵심이에요.
필요한 곳에만 적절히 사용하는 것이
"지능적인 최적화 전략"입니다 😎
다음 글에서는 React DevTools로 리렌더링 감지하고 최적화 지점 찾는 법에 대해 알아볼게요!