useCallback의 기본 개념const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b]
);
useCallback은 두 개의 인자를 받는다.props가 변경되지 않으면 재 렌더링을 막아주는 기능이다.useCallback이랑 궁합이 좋냐 ?: useCallback 없이 그냥 함수를 props로 넘겨버리면, 부모 컴포넌트가 렌더링 될 때마다 새 함수가 만들어지는데, 그럼 memo로 감싼 자식 컴포넌트도 매번 재 렌더링된다. useCallback을 쓰면 함수가 새로 만들어지지 않아서 자식 컴포넌트의 불필요한 재렌더링을 막을 수 있다....임포트 생략
function parentComponent() {
const [count, setCount] = useState(0);
// 1. 빈 의존성 배열: 컴포넌트가 처음 렌더링될 때만 함수 생성
const increment = useCallback(() => {
setCount(c => c + 1);
}, [])
// 2. count가 변경될 때마다 함수 재생성
const incrementWithLog = useCallback(() => {
console.log(`Count is now ${count + 1}`);
setCount(c => c + 1); }, [count]);
}, [count]);
return (
<>
<p>Count: {count}</p>
<p>Other State: {otherState}</p>
<ChildComponent onIncrement={increment} />
<ChildComponentWithLog onIncrementWithLog={incrementWithLog} />
<button onClick={() => setOtherState(s => s + 1)}>Update Other State</button>
</>
)
}
const ChildComponent = React.memo(({ onIncrement }) => {
console.log("ChildComponent rendered");
return <button onClick={onIncrement}>Increment</button>;
});
const ChildComponentWithLog = React.memo(({ onIncrementWithLog }) => {
console.log("ChildComponentWithLog rendered");
return <button onClick={onIncrementWithLog}>Increment with Log</button>;
});
💡
increment 함수는 항상 같은 인스턴스를 참조하므로 ChildComponent는 count나 otherState가 변경되어도 리 렌더링되지 않는다.incrementWithLog 함수는 count가 변경될 때마다 새로운 인스턴스가 생성되므로 count가 변경될 때마다 ChildComponentWithLog가 리 렌더링된다.otherState가 변경되면 ParentComponent는 리렌더링되지만, 두 자식 컴포넌트 모두 재렌더링되지 않는다.function ParentComponent() {
const [count, setCount] = useState(0);
// 이 함수는 ParentComponent가 리렌더링될 때마다 새로 생성됨
const onClickUpCount = () => setCount(c => c + 1);
return (
<div>
Count: {count}
<ChildComponent onIncrement={onClickUpCount} count={count} />
</div>
);
}
💡 count가 변경될 때마다 ParentComponent가 리렌더링된다.
리렌더링될 때마다 onClickUpCount 함수가 새로 생성된다.
결과적으로 ChildComponent가 매번 리렌더링된다.
주의사항
ChildComponent가 복잡하거나 무거운 연산을 포함하고 있다면, 불필요한 리렌더링으로 성능이 저하될 수 있다.
count만 사용하고 onClickUpCount는 사용하지 않는 경우에도 함수가 새로 생성되어 리렌더링이 발생한다.
useCallback(fn, deps)는 useMemo(() => fn, deps)와 동등하다.useCallback은 함수 자체를 메모이제이션하고, useMemo는 함수의 결과값을 메모이제이션한다.// useCallback
const memoizedFunction = useCallback(() => {
return expensiveCalculation(a, b);
}, [a, b]);
// useMemo
const memoizedResult = useMemo(() => {
return expensiveCalculation(a, b);
}, [a, b]);
useCallback은 함수 정의 자체를 기억하고, useMemo는 함수 실행 결과를 기억한다.useCallback은 이벤트 핸들러나 자식 컴포넌트에 전달할 콜백 함수를 최적화할 때 주로 사용한다.useMemo는 계산 비용이 높은 값을 메모이제이션할 때 사용한다.모든 함수에 useCallback을 사용하는 것은 불필요할 수 있다. 실제로 성능 향상이 필요한 경우에만 사용해야 한다.
의존성 배열을 올바르게 관리해야 한다. 필요한 의존성을 누락하면 오래된 클로저 문제가 발생할 수 있다.
💡 아래는 클로저 문제이다 확인해보자.
const [count, setCount] = useState(0);
// 잘못된 사용: count를 의존성 배열에 추가하지 않음
const incorrectCallback = useCallback(() => {
console.log(`Count: ${count}`);
}, []); // 항상 초기 count 값(0)을 참조
// 올바른 사용
const correctCallback = useCallback(() => {
console.log(`Count: ${count}`);
}, [count]); // count가 변경될 때마다 함수 재생성
useCallback 자체도 약간의 오버헤드가 있다.useCallback으로 최적화useCallback 사용useCallback으로 최적화const DataGrid = ({ data }) => {
const handleRowClick = useCallback((id) => {
console.log(`Row ${id} clicked`);
}, []);
return (
<div>
{data.map(item => (
<Row
key={item.id}
data={item}
onClick={() => handleRowClick(item.id)}
/>
))}
</div>
);
};
💡 이 예제에서 handleRowClick은 useCallback으로 메모이제이션되어, data가 변경되어도 각 Row 컴포넌트가 불필요하게 재렌더링되는 것을 방지한다.