[React] useCallback
'리액트를 다루는 기술'에 나오는 useCallback 개념을 정리하고, 이해가 가지 않아 찾아 본 내용들을 정리해보겠습니다.
useCallback
useMemo와 상당히 비슷한 함수
주로 렌더링 성능을 최적화해야 하는 상황에서 사용 ➡️ useCallback 사용 시 만들어 놨던 함수의 재사용이 가능
useCallback(fn, deps)은 useMemo(() => fn, deps)와 동일
useCallback 함수를 사용하지 않으면,
컴포넌트 내에 선언된 함수들은 컴포넌트가 리렌더링될 때마다 새로 만들어진 함수를 사용하게 됩니다.
▶️ 대부분의 경우에는 문제가 되지 않으나, 컴포넌트의 렌더링이 자주 발생하거나 렌더링해야 할 컴포넌트의 개수가 많아지면 이 부분을 최적화해 주는 것이 좋습니다.
useCallback을 사용한 최적화
import React, { useState, useMemo, useCallback } from "react";
const getAverage = (numbers) => {
console.log("평균값 계산 중..");
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a, b) => a + b);
return sum / numbers.length;
};
const Average = () => {
const [list, setList] = useState([]);
const [number, setNumber] = useState("");
const onChange = useCallback(e => {
setNumber(e.target.value);
}, []); // 컴포넌트가 처음 렌더링 될 때만 함수 생성
const onInsert = useCallback(e => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber("");
}, [number, list]); // number 혹은 list가 바뀌었을 때만 함수 생성
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값: </b> {avg}
</div>
</div>
);
};
export default Average;
useCallback의 사용 방법
첫 번째 파라미터에는 생성하고 싶은 함수를, 두 번째 파라미터(deps)에는 배열을 넣습니다.
* 이 배열에는 어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지 명시
결국 useCallback 함수는 메모이제이션된 콜백을 반환합니다.
useCallback은 콜백의 메모이제이션된 버전을 반환할 것입니다.
그 메모이제이션된 버전은 콜백의 의존성이 변경되었을 때에만 변경됩니다.
이것은, 불필요한 렌더링을 방지하기 위해 (예로 shouldComponentUpdate를 사용하여) 참조의 동일성에 의존적인 최적화된 자식 컴포넌트에 콜백을 전달할 때 유용합니다.
위의 예시에서 onChange 처럼 비어 있는 배열을 넣게 되면, 컴포넌트가 렌더링될 때 만들었던 함수를 계속해서 재사용하게 되며, onInsert처럼 배열 안에 number와 list를 넣게 되면, 인풋 내용이 바뀌거나 새로운 항목이 추가될 때 새로 만들어진 함수를 사용하게 됩니다.
작성하다보니 '리다기'의 내용을 거의 복붙한것 과 다름없게 되었는데, 작성하다 보니 모든 부분이 중요하다고 생각이되었습니다.
'리다기' 내용을 읽다보니 useCallback 함수가 함수를 새롭게 생성하는 것을 방지하여 재사용하게 한다는 것 까지는 어렴풋이 이해하겠으나, 위에 빨간 글씨로 적힌 새로 만들어진 함수를 사용하게 된다는 부분이 이해가 되지 않았습니다(어떤 새로운 함수가 만들어진단 거야..?).
우선 최적화를 왜 해야하는지 알아보았습니다.
함수들은 컴포넌트가 리렌더링 될 때 마다 새로 만들어집니다. 함수를 선언하는 것 자체는 사실 메모리도, CPU 도 리소스를 많이 차지 하는 작업은 아니기 때문에 함수를 새로 선언한다고 해서 그 자체 만으로 큰 부하가 생길일은 없지만, 한번 만든 함수를 필요할때만 새로 만들고 재사용하는 것은 여전히 중요합니다.
아니 그래서 왜 중요한데?
그 이유는, 우리가 나중에 컴포넌트에서 props 가 바뀌지 않았으면 Virtual DOM 에 새로 렌더링하는 것 조차 하지 않고 컴포넌트의 결과물을 재사용 하는 최적화 작업을 할건데요, 이 작업을 하려면, 함수를 재사용하는것이 필수입니다.
결국 기존 컴포넌트의 결과물을 재사용하기 위해서 우리는 useCallback 함수를 사용하는 것입니다.
옥게이... 최적화는 해야지.
근데 왜 useCallback 함수를 사용하더라도, 새로 항목이 추가될 때 마다 새로 만들어진 함수를 사용해야할까요?
함수 내부에서 상태 값에 의존해야 할 때, deps 배열 안에서 참조하지않으면 props가 변경되어도 함수가 새로 생성되지 않습니다.
함수가 새로 생성되지 않은 경우에는 함수 내의 props가 변경되더라도 최신 props 값이 사용되지 않고, 이전에 함수 생성시의 props가 사용되어지기 때문입니다.
그렇기 때문에 deps 배열이 변경이 될 경우, 함수는 새롭게 만들어지게 되고, 그 함수를 사용하게 되는 것입니다.
결론
useCallback 함수를 통해 컴포넌트가 리 렌더링이 되더라도 최초 렌더링 때 만들었던 함수를 재사용할 수 있으며,
props가 변경되는 경우에만 함수가 새롭게 생성이 되어 사용됨으로써 최적화에 유리하다는 것입니다.