리렌더링이 일어날때 필요에 따라 함수 정의를 반복하지 않고 캐시해서 값을 재사용하여 성능을 개선한다.
const cachedFn = useCallback(fn, dependencies)
fn
: 캐시하려는 함수 정의. 먼저 처음 렌더링에서 함수 정의를 반환한다.(호출이 아님!) 그리고 다음 렌더링에서 마지막 렌더링 이후 dependencies
가 변경되지 않은 경우 캐시된 함수 정의를 재사용한다. 그렇지 않으면 현재 렌더링 중에 전달한 함수로 새롭게 정의하고 나중에 재사용할 수 있도록 저장한다. 함수를 호출하지 않고 함수 정의가 반환되므로 언제 함수를 실행할지는 추후 조절할 수 있다.
dependencies
: fn
코드 내부에서 참조되는 모든 변수값 목록이고 이는 state, props 그리고 컴포넌트 내부에서 선언된 변수와 함수가 해당한다. 여기에 속한 값의 변경이 감지되면 fn 함수를 다시 정의하고 캐시한다.
// useCallback 사용예
function ProductPage({ productId, referrer, theme }) {
// 함수 객체를 캐시...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...이 종속값이 변하지 않는 이상 참조값이 변하지 않는다...
return (
<div className={theme}>
{/* ...ShippingForm은 같은 props를 받았을 경우 리렌더링을 건너뛴다 */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
자식 요소 ShippingForm 에게 함수 객체 handleSubmit 를 전달하는 상황이다. 만약 ShippingForm 컴포넌트의 실행 비용이 높아 부모 컴포넌트가 렌더링 될때마다 자식 컴포넌트를 리렌더링 시키는 것이 부담스럽다면 useCallback
과 React.memo
기능을 이용해 자식 컴포넌트의 리렌더링을 막을 수 있다.
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
React.memo
로 컴포넌트를 래핑하면 ShippingForm 가 받아오는 props 인 handleSubmit 가 변하지 않았다면 ShippingForm 컴포넌트가 리렌더링 되지 않고 만약 변했다면 리렌더링된다
// useCallback 미사용예
function ProductPage({ productId, referrer, theme }) {
// 매실행마다 다른 참조 값의 함수 객체를 반환하므로 값이 항상 변한다...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ... 그래서 ShippingForm의 props는 절대 같을 수 없고 항상 리렌더링 된다... */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
이때 만약 useCallback
을 사용하지 않는다면 React.memo
기능은 동작하지 않는다. 왜그럴까?
자바스크립트에서 함수 선언(function() {}
or () => {}
) 또한 객체 리터럴({}
)을 생성하는 것처럼 매실행마다 새로운 참조값을 가진다. 따라서 handleSubmit 함수 객체는 매실행마다 새로운 참조값을 가지고 이는 ShippingForm 에 전달되는 props 값을 매번 변화시켜 ShippingForm 함수 내부의 변화가 없을 때에도 React.memo
가 적용된 자식 컴포넌트를 리렌더링 시킨다.
따라서 useCallback
으로 props 로 전달할 함수를 래핑하면 종속된 값이 변경되지 않는한 함수의 참조값이 변하지 않게 할 수 있다.
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ todos 종속이 필요하지 않다.
// ...
useCallback
내부에서 state 를 업데이트할 경우 state 를 종속 리스트에 등록하는 것보다 setState 에 업데이트 함수를 전달하는 것이 더 직관적일 것이다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problem: This dependency changes on every render
// ...
useEffect 내부에서 외부 함수를 호출하고 해당 함수 객체를 종속 관계로 등록하면 렌더링 될때 마다 함수의 참조값이 바뀌기때문에 useEffect를 계속 실행시킬 것이다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ 오직 roomId가 바뀔 때만 변경
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ 오직 변경createOptions가 바뀔 때만 실행
// ...
이경우 위의 코드처럼 useCallback
으로 함수를 래핑하면 동일한 createOptions 일때 같은 참조값을 보장해주어서 useEffect
의 콜백 함수 실행을 줄일 수 있다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ useCallback과 함수 종속성이 필요없다.
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId])<; // ✅ 오직 roomId가 바뀔 때만 변경
// ...
하지만 useEffect
외부에 선언된 함수를 종속성에 등록해서 사용하는 것보다 외부의 함수 선언을 useEffect
내부로 옮겨서 사용하는 것이 더 효율적이다.
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,
};
}
커스텀 Hooks를 작성하는 경우 반환되는 모든 함수를 useCallback
으로 래핑하는 것이 좋다.