컴포넌트에서 자식에게 함수를 props로 넘길 때, 그 함수가 일반 함수인지 useCallback으로 감싼 함수인지에 따라 자식 컴포넌트의 렌더링 여부가 달라진다.
🎯 실험 목표
부모 컴포넌트에서 자식에게 함수를 전달할 때, 일반 함수를 전달했을 때와 useCallback으로 메모이제이션한 함수를 전달했을 때를 비교하고자 예제 코드를 준비했다.
어떤 경우에 자식 컴포넌트가 불필요하게 렌더링되는지 확인해 보자.
🧩 예제 코드
ChildRaw: 일반 함수를 props로 받는 자식
ChildMemo: useCallback 함수를 props로 받는 자식
👉 두 자식 모두 React.memo로 감싸져 있음
import { memo, useCallback, useState } from 'react';
const ChildRaw = memo(({ onClick }: { onClick: () => void }) => {
console.log('🧒 [Raw] 자식 렌더링');
return <button onClick={onClick}>자식 (일반 함수)</button>;
});
const ChildMemo = memo(({ onClick }: { onClick: () => void }) => {
console.log('🧒 [Memo] 자식 렌더링');
return <button onClick={onClick}>자식 (useCallback)</button>;
});
// 부모 컴포넌트에서 자식 컴포넌트로 함수를 넘길 때, useCallback을 쓰는 것과 그냥 함수를 넘기는 것의 차이
// 리렌더링에 어떤 영향을 주는지 확인
const Compare = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// ❌ 매번 새로 만들어지는 함수 (렌더링마다 변경됨)
const handleClickRaw = () => {
console.log('❌ 일반 함수 호출');
};
// ✅ useCallback으로 메모이제이션된 함수
const handleClickMemo = useCallback(() => {
console.log('✅ useCallback 함수 호출');
}, []);
return (
<div style={{ padding: 20 }}>
<h2>useCallback 비교 실험</h2>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="입력 시 부모 리렌더링"
/>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>부모 카운트 증가</button>
<h3>자식에 일반 함수 전달</h3>
<ChildRaw onClick={handleClickRaw} />
<h3>자식에 useCallback 함수 전달</h3>
<ChildMemo onClick={handleClickMemo} />
</div>
);
};
export default Compare;
🔄 실험 결과
✅ 일반 함수 전달 (handleClickRaw)
- 부모가 리렌더링될 때마다 handleClickRaw 함수가 새로 생성됨
- ChildRaw는 memo로 감싸져 있어도 props가 변경되므로 리렌더링 발생
✅ useCallback 함수 전달 (handleClickMemo)
- useCallback([]) 덕분에 함수 참조가 변하지 않음
- ChildMemo는 props가 같다고 판단되어 리렌더링되지 않음
📌 실제로 input에 텍스트를 입력하면
- ChildRaw는 글자마다 계속 렌더링됨
- ChildMemo는 렌더링되지 않음
💡 왜 이런 차이가 생길까?
React는 props로 전달된 값이 이전과 같다면 자식 컴포넌트를 리렌더링하지 않는다.
하지만 함수는 JS 객체이기 때문에 렌더링마다 새로 생성되면 참조값이 달라진다.
React는 props가 바뀌었는지 비교할 때 참조(주소)를 비교하기 때문에 렌더링마다 새로 만들어지는 함수는 항상 다른 값으로 간주된다.
const fn1 = () => {};
const fn2 = () => {};
console.log(fn1 === fn2); // false!
따라서 부모가 리렌더링될 때마다 새 함수를 만들면 자식은 항상 props가 바뀌었다고 판단해 렌더링된다.
✨ useCallback이 해결해 주는 것
useCallback(fn, [])을 사용하면, 함수 참조를 기억(memoize)해서 렌더링이 다시 일어나도 이전 함수 객체를 재사용한다.
즉, 자식 입장에서는 props가 변하지 않았다고 인식해서 렌더링을 건너뛴다.
⚠️ useCallback을 무조건 써야 할까?
무조건 useCallback을 쓰면 오히려 성능이 안 좋아질 수도 있다.
함수를 기억하기 위한 내부 메모이제이션 처리도 비용이 있기 때문이다.
대부분은 일반 함수로 충분하고, 리렌더링을 최적화하려는 특정 조건이 있을 때만 useCallback을 쓴다.
✔️ useCallback이 필요한 경우
- 함수 props를 자식에게 전달하고
- 자식 컴포넌트가 React.memo로 감싸져 있으며
- 부모 컴포넌트가 자주 리렌더링 되고
- 자식 렌더링을 피하고 싶을 때
위 조건이 모두 충족되면 사용을 고려한다.
✅ 결론
자식에게 함수를 넘기지 않으면 => 일반 함수
자식이 memo 처리되어 있고, 함수 props를 받으면 => useCallback
'Library & Runtime > React' 카테고리의 다른 글
[React] 계산/렌더링 최적화: useMemo + React.memo 실험 (0) | 2025.05.04 |
---|---|
[React] 무거운 연산 최적화 : 일반 계산 vs useMemo (0) | 2025.05.03 |
[React] 컴포넌트 생명주기 : useEffect, 마운트, 언마운트 (0) | 2025.05.01 |
[React/S3] 이미지 저장 구현 (+ CORS 에러 해결) (2) | 2024.11.16 |