useCallback

useCallback은 리렌더링 사이에 함수 정의를 캐시할 수 있는 React Hook입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const cachedFn = useCallback(fn, dependencies);

// example
import { useCallback } from 'react';

function Calculator({ x, y }) {
const add = useCallback(() => x + y, [x, y]);

return (
<>
<div>{add()}</div>
</>
);
}

useCallback, 언제 사용하나요?

🐢 컴포넌트 리렌더링을 건너뛰고싶을 때 사용합니다.

하위 컴포넌트에 함수를 전달할 때 전달하는 함수를 캐싱해야 할 때가 있습니다.

1
2
3
4
5
6
7
8
9
10
import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...

useCallback을 사용하려면 두 가지를 전달해야합니다.

  1. 리렌더링 사이에 캐시하려는 함수
  2. 함수 내부에서 사용되는 모든 값을 포함한 dependencies List

처음 초기 렌더링에서 useCallback에서 returned function은 전달한 함수가 됩니다.

다음 렌더링에서 React는 종속성을 이전 렌더링에서 전달한 종속성과 비교합니다.
👉🏻 종속성 중 변경된 것이 없다면(Object.is와 비교했을 때), useCallback은 이전과 동일한 함수를 반환합니다. 그렇지 않으면 useCallback은 이 렌더링에서 전달한 함수를 반환합니다.

다시 말해, useCallback은 종속성이 변경될 때까지 리렌더링 사이에 함수를 캐시합니다.

ProductPage에서 ShippingForm 컴포넌트로 handleSubmit 함수를 전달한다고 가정해 보겠습니다.

1
2
3
4
5
6
7
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} /> // 🚧 ShippingForm 컴포넌트에서 memo 최적화를 했다고 가정
</div>
);

JavaScript에서 함수는 객체입니다.

JavaScript에서 함수 () {} 또는 () => {}는 {} 객체 리터럴이 항상 새 객체를 생성하는 것과 유사하게 항상 다른 함수를 생성합니다. 일반적으로는 문제가 되지 않지만 ShippingForm props는 동일하지 않기 때문에 memo 최적화가 작동하지 않습니다. 이 때 useCallback이 유용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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>
);
}

👉🏻 handleSubmit을 useCallback으로 감싸면 의존성이 변경될 때까지 재렌더링 간에 동일한 함수가 되도록 할 수 있습니다.


useCallback을 이용하여 앱 최적화하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// App.js
export default function App() {
const [input, setInput] = useState(1);
const [light, setLight] = useState(true);

const theme = {
backgroundColor: light ? 'White' : 'grey',
color: light ? 'grey' : 'white',
};

const getItems = () => {
return [input + 10, input + 100];
};

const handleChange = (event) => {
if (Number(event.target.value)) {
setInput(Number(event.target.value));
}
};

return (
<div style={theme}>
<input
type="number"
className="input"
value={input}
onChange={handleChange}
/>
<button
className={(light ? 'light' : 'dark') + ' button'}
onClick={() => setLight((prevLight) => !prevLight)}
>
{light ? 'dark mode' : 'light mode'}
</button>
<List getItems={getItems} />
</div>
);
}

List 컴포넌트에서는 getItems 함수를 전달받아서 useEffect 내에서 실행시킨 후 items에 배열을 저장합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// List.js
import { useState, useEffect } from 'react';

function List({ getItems }) {
/* Initial state of the items */
const [items, setItems] = useState([]);

/* This hook sets the value of items if
getItems object changes */
useEffect(() => {
console.log('아이템을 가져옵니다.');
setItems(getItems());
}, [getItems]);

/* Maps the items to a list */
return (
<div>
{items.map((item) => (
<div key={item}>{item}</div>
))}
</div>
);
}

export default List;

List 컴포넌트의 리렌더링은 App 컴포넌트의 state 상태가 변경될 때마다 계속해서 발생합니다. useEffect 내 console.log('아이템을 가져옵니다.')가 언제 출력되는지 확인해보면

  • App 컴포넌트의 input 값이 변경되었을 때
  • App 컴포넌트의 light 값이 변경되었을 때

두 가지 경우에 출력되는 것을 볼 수 있습니다. 즉, 현재는 button dark mode을 눌러도 getItems의 함수가 동작한다는거죠. 버튼을 누를 땐, List 컴포넌트가 리렌더링되면서 getItems 함수도 새로운 함수 객체로 생성되었기 때문에 새롭게 호출이 됩니다.


useCallback을 사용해 최적화를 시켜봅시다!

정의된 함수에 useCallback을 사용해야하니, getItems가 정의된 곳을 찾아갑니다. 그리고 input의 값이 변경되지 않는다면 새로운 함수를 만들지 않도록 useCallback으로 래핑합니다.

1
2
3
const getItems = useCallback(() => {
return [input + 10, input + 100];
}, [input]);

getItems를 useCallback으로 래핑함으로써 버튼을 클릭했을 때에는 getItems를 이전 메모리에 캐싱되어 있던 getItems 함수 객체를 사용하도록 만들어서 최적화를 했습니다.


useCallback은 useMemo와 어떤 차이점이 있을까요?

✅ useMemo는 함수 호출 결과를 캐시합니다.

1
2
3
4
const requirements = useMemo(() => {
// Calls your function and caches its result
return computeRequirements(product);
}, [product]);
  • 이 예제에서는 제품이 변경되지 않는 한 변경되지 않도록 computeRequirements(product)를 호출한 결과를 캐시합니다.
  • 이를 통해 불필요하게 ShippingForm을 다시 렌더링하지 않고도 요구사항 객체를 전달할 수 있습니다.
  • 필요한 경우 React는 렌더링 중에 전달한 함수를 호출하여 결과를 계산합니다.

✅ useCallback은 함수 자체를 캐시합니다.

1
2
3
4
5
6
7
8
9
10
const handleSubmit = useCallback(
(orderDetails) => {
// Caches your function itself
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
},
[productId, referrer]
);
  • useMemo와 달리 사용자가 제공한 함수를 호출하지 않습니다.
  • 대신 제공한 함수를 캐시에 저장하여 productId나 referrer가 변경되지 않는 한 handleSubmit 자체가 변경되지 않도록 합니다.
  • 이렇게 하면 불필요하게 ShippingForm을 다시 렌더링하지 않고도 handleSubmit 함수를 전달할 수 있습니다. 사용자가 양식을 제출할 때까지 코드가 실행되지 않습니다.