개요
useCallback
은 컴포넌트가 리렌더링될 때마다 함수 정의를 캐시(저장)할 수 있게 해주는 React Hook입니다.
const cachedFn = useCallback(fn, dependencies)
Reference
useCallback(fn, dependencies)
컴포넌트의 최상위에서 useCallback
을 호출하여 함수 정의를 리렌더링 사이에 캐시할 수 있습니다.
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
}
파라미터
fn
: 캐시하고 싶은 함수. 어떤 인자든 받을 수 있고, 어떤 값이든 반환할 수 있습니다. React는 이 함수를 직접 호출하지 않고, 반환만 해줍니다.dependencies
: 함수 내부에서 참조하는 모든 반응형 값(예: props, state, 컴포넌트 내부 변수/함수 등)의 배열. 의존성 배열이 이전 렌더와 동일하다면, 이전에 저장된 함수를 반환합니다.
반환값
- 최초 렌더에서는 전달한
fn
을 반환합니다. - 이후 렌더에서는 의존성 배열이 바뀌지 않았다면 이전에 저장된 함수를, 바뀌었다면 새로 전달한 함수를 반환합니다.
주의사항
useCallback
은 반드시 컴포넌트의 최상위(혹은 커스텀 Hook 내부)에서만 호출해야 하며, 반복문/조건문 내부에서는 사용할 수 없습니다.- React는 특별한 이유가 없는 한 캐시된 함수를 버리지 않습니다. (예: 개발 중 파일 수정, 컴포넌트가 suspend될 때 등)
- 성능 최적화가 목적이 아니라면, state 변수나 ref가 더 적합할 수 있습니다.
사용법
자식 컴포넌트의 불필요한 리렌더링 방지
자식 컴포넌트에 함수를 prop으로 전달할 때, 매번 새로운 함수가 생성되어 불필요한 리렌더링이 발생할 수 있습니다. 이럴 때 useCallback
으로 함수를 감싸면, 의존성이 바뀌지 않는 한 동일한 함수가 전달되어 리렌더링을 방지할 수 있습니다.
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
자식 컴포넌트가 React.memo
로 감싸져 있다면, prop이 바뀌지 않는 한 리렌더링을 건너뜁니다.
메모이즈된 콜백에서 state 업데이트
state를 읽어서 새로운 state를 계산하는 경우, 의존성 배열에 state를 넣지 않고 updater 함수를 사용할 수 있습니다.
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // todos를 의존성에 넣지 않아도 됨
}
Effect의 불필요한 실행 방지
Effect 내부에서 함수를 호출해야 할 때, 그 함수가 매번 새로 생성된다면 Effect가 불필요하게 자주 실행될 수 있습니다. 이럴 때 useCallback
으로 함수를 감싸거나, 아예 Effect 내부에 함수를 정의하면 의존성 문제를 줄일 수 있습니다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]);
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]);
}
더 좋은 방법은 Effect 내부에 함수를 직접 정의하는 것입니다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}
커스텀 Hook 최적화
커스텀 Hook에서 반환하는 함수도 useCallback
으로 감싸면, Hook을 사용하는 쪽에서 성능 최적화를 할 수 있습니다.
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return { navigate, goBack };
}
트러블슈팅
매 렌더마다 useCallback이 다른 함수를 반환함
의존성 배열을 두 번째 인자로 반드시 전달해야 합니다. 의존성 배열이 없으면 매번 새로운 함수가 반환됩니다.
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 의존성 배열이 없으므로 매번 새 함수 반환
}
아래처럼 의존성 배열을 추가해야 합니다.
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ 불필요한 함수 재생성 방지
}
리스트 아이템마다 useCallback을 호출하고 싶을 때
반복문(예: map) 내부에서 useCallback을 호출하는 것은 허용되지 않습니다. 대신, 각 아이템을 별도의 컴포넌트로 분리하고 그 컴포넌트의 최상위에서 useCallback을 호출하세요.
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
또는, Report 컴포넌트 자체를 memo로 감싸 prop이 바뀌지 않으면 리렌더링을 건너뛰게 할 수도 있습니다.
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});