useCallback은 렌더링 사이에 함수를 캐시합니다. 의존성 중 하나가 바뀔 때까지 같은 함수 객체를 반환합니다. 이것이 중요한 것은 무언가가 함수를 참조로 비교할 때뿐이며, 실제로는 memo로 감싼 자식이나 그 함수를 의존성으로 나열한 이펙트를 뜻합니다.
두 버튼 모두 메모이제이션되어 있고 같은 일을 합니다. 테마를 토글하고 Console을 보세요. render Inline function만 나타납니다. saveInline은 렌더링마다 새 함수이므로 memo가 prop이 바뀌었다고 보기 때문입니다. saveCached는 매번 같은 함수이므로 그 버튼은 건너뜁니다.
무엇을 캐시할까
const cachedFn = useCallback(fn, dependencies);
fn은 유지하고 싶은 함수입니다. React는 이것을 호출하지 않고 돌려줍니다.dependencies는fn이 읽는 컴포넌트의 모든 값을 나열합니다. React는 렌더링마다Object.is로 이전 값과 비교합니다.- 아무것도 바뀌지 않았다면 이전 렌더링의 함수를 받습니다. 무언가 바뀌었다면 이번에 넘긴 함수를 받고, React는 다음을 위해 그 함수를 보관합니다.
이것이 뜻하는 바를 보세요. useCallback 안의 화살표 함수는 인자일 뿐이므로 여전히 렌더링마다 만들어집니다. useCallback은 그 작업을 줄여 주지 않습니다. 유일한 일은 참조가 같게 유지되도록 이전의 동일한 함수를 돌려주는 것입니다.
useCallback과 useMemo
둘은 같은 메커니즘입니다. useMemo는 함수를 호출한 결과를 캐시하고, useCallback은 함수 자체를 캐시합니다.
// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);
함수에는 더 읽기 쉬운 useCallback을 쓰고, 필터링된 목록이나 옵션 객체 같은 계산된 값에는 useMemo를 쓰세요.
업데이터 함수로 의존성 줄이기
캐시된 함수는 자기가 읽는 모든 state 값을 나열해야 하고, 그 state가 바뀔 때마다 함수도 바뀝니다. 메모이제이션된 행 목록에서는 이것이 문제가 됩니다. todos를 읽는 remove 함수는 todos가 바뀔 때마다 바뀌므로, 삭제할 때마다 남은 모든 행이 다시 렌더링됩니다. setter의 업데이터 형태를 쓰면 함수가 더 이상 todos를 읽지 않습니다.
행 하나를 삭제해 보세요. 다른 행들은 이전과 같은 todo 객체와 같은 remove 함수를 받았으므로 Console은 조용합니다. 함수를 useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos])로 바꾸고 다시 해 보면, 이제 삭제할 때마다 남은 모든 행이 렌더링 로그를 남깁니다.
이펙트 의존성으로 쓰는 함수
컴포넌트에서 선언한 함수를 호출하는 이펙트는 그 함수를 의존성으로 나열해야 합니다. 함수가 렌더링마다 새것이라면 이펙트는 렌더링마다 실행됩니다. useCallback은 함수 자신의 입력이 바뀔 때까지 함수를 안정적으로 유지합니다.
메시지를 입력해도 다시 연결되지 않습니다. 방을 바꾸면 이전 방에서의 연결 해제와 새 방으로의 연결이 기록됩니다. useCallback 래퍼를 지우면(화살표 함수는 유지) 키를 누를 때마다 연결이 끊겼다가 다시 연결됩니다.
하지만 여기에는 더 간단한 해결책이 있습니다. 함수를 이펙트 안으로 옮기는 것입니다. 그러면 이펙트가 roomId에 직접 의존하고, 안정적이어야 할 함수가 없습니다.
useEffect(() => {
function createOptions() {
return { serverUrl: 'wss://chat.example', roomId };
}
const options = createOptions();
// ...
}, [roomId]);
함수를 이펙트만 쓴다면 그 방법을 선호하세요. useCallback은 아래로 넘기거나 커스텀 훅에서 반환하는 함수, 즉 호출하는 쪽이 자기 의존성 배열에 넣을 수 있는 함수에 남겨 두세요.
함수가 바뀌었는지 확인하기
memo 자식이 계속 렌더링되는데 이유를 모르겠다면, 함수 prop이 렌더링 사이에 정말 같은 객체인지 확인하세요. 이전 함수를 ref에 보관하고 비교합니다.
Count를 클릭하면 step이 바뀌지 않았으므로 Console에 increment changed: false가 나옵니다. Step을 클릭하면 true가 나옵니다. step이 의존성이므로 React가 새 함수를 돌려준 것입니다. 렌더링마다 바뀌는 함수는 보통 컴포넌트에서 선언한 객체나 다른 함수처럼 의존성 자체가 렌더링마다 새것이라는 뜻입니다.
커스텀 훅 안의 useCallback
함수를 반환하는 커스텀 훅은 누가 호출할지 모르더라도 useCallback을 쓰기 좋은 곳입니다. 훅을 쓰는 컴포넌트가 그 함수들을 memo 자식에 넘기거나 이펙트에 나열할 수 있는데, 바깥에서는 그것들을 안정적으로 만들 수 없습니다. 훅 안에서 감싸 두면 모든 호출자가 기본적으로 안정적인 참조를 받습니다.
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => setCount((c) => c + 1), []);
const reset = useCallback(() => setCount(initial), [initial]);
return { count, increment, reset };
}
흔한 실수: 모든 것을 감싸기
<button>이나 <input>으로 바로 가는 핸들러를 useCallback으로 감싸도 쓸모 있는 일은 없습니다. 핸들러가 그대로라고 해서 React가 DOM 요소에 대한 작업을 건너뛰지는 않으며, memo로 감싸지 않은 컴포넌트는 부모가 렌더링되면 어차피 렌더링됩니다.
// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;
useCallback을 하나 더할 때마다 누군가 틀릴 수 있는 의존성 배열이 하나 늘고, 낡은 의존성은 실제 버그입니다(함수가 이전 state를 계속 읽습니다). 받는 쪽에 memo 자식이나 의존성 배열이 있을 때 추가하고, 기본으로 붙이지는 마세요.
빌드에서 React Compiler 1.0을 쓰면 함수와 값을 자동으로 메모이제이션하므로, 직접 작성하는 useCallback 호출 대부분이 필요 없어집니다. 컴파일러가 없다면 위의 규칙이 여전히 적용되며, 직접 쓴 useCallback은 기존 코드에서 여전히 흔합니다.
자주 묻는 질문
useCallback은 무엇을 하나요?
의존성 중 하나가 바뀔 때까지 렌더링마다 같은 함수 객체를 반환합니다. 함수가 만들어지는 것을 막지는 않습니다. 새 함수 대신 이전 렌더링의 함수를 돌려줄 뿐입니다.
useCallback은 언제 써야 하나요?
memo로 감싼 자식에 함수를 넘길 때, 또는 그 함수가 이펙트나 다른 훅의 의존성일 때입니다. 두 경우 모두 렌더링마다 새 함수가 생기면 최적화가 무력화되거나 이펙트가 다시 실행됩니다.
useCallback과 useMemo의 차이는 무엇인가요?
useCallback(fn, deps)는 fn 자체를 캐시합니다. useMemo(() => value, deps)는 함수가 반환하는 값을 캐시합니다. useCallback(fn, deps)는 정확히 useMemo(() => fn, deps)입니다.
모든 함수를 useCallback으로 감싸야 하나요?
아니요. 함수가 일반 DOM 요소나 메모이제이션되지 않은 컴포넌트로 간다면 안정적인 참조는 아무것도 바꾸지 않으며, 늘어난 의존성 배열은 코드를 읽기 어렵고 틀리기 쉽게 만듭니다.
useCallback을 쓰면 컴포넌트가 빨라지나요?
그것만으로는 아닙니다. memo나 이펙트의 의존성 배열처럼 아래쪽에서 함수를 참조로 비교하는 무언가가 있을 때만 도움이 됩니다.