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: 캐시하려는 함수 값이다. 어떤 인자도 받을 수 있고 어떤 값이라도 반환할 수 있다. 리액트는 초기 렌더링을 하는 동안 함수를 반환한다(호출하지 않는다). 다음 렌더링에서 리액트는 마지막 렌더링 이후 dependencies가 변경되지 않았다면 동일한 함수를 다시 제공한다. 그렇지 않으면 현재 렌더링 중에 전달한 함수를 제공하고 나중에 재사용할 수 있도록 저장한다. 리액트는 함수를 호출하지 않는다. 함수는 반환되므로 호출 시기와 여부를 결정할 수 있다.
- dependencies : fn 코드 내에서 참조된 모든 반응형 값의 배열이다. 반응형 값에는 props, state, 컴포넌트 본문 내부에서 직접 선언한 모든 변수 및 함수가 포함된다. 린터가 리액트용으로 구성된 경우, 모든 반응형 값이 의존성으로 올바르게 지정되었는지 확인한다. 의존성 배열에는 일정한 수의 항목이 있어야 하며 [dep1, dep2, dep3]과 같이 인라인으로 작성해야 한다. 리액트는 Object.is 비교 알고리즘을 사용하여 각 의존성을 이전 값과 비교한다.
반환 값
초기 렌더링에서 useCallback은 전달한 fn함수를 반환한다.
렌더링 중에는 마지막 렌더링에서 이미 저장된 fn 함수를 반환하거나 (의존성이 변경되지 않은 경우), 렌더링 중에 전달했던 fn 함수를 반환한다.
주의 사항
- useCallback은 훅이므로 컴포넌트의 최상위 레벨이나 자체 훅에서만 호출할 수 있다. 번복문이나 조건문 내부에서는 호출할 수 없다. 필요한 경우 새로운 컴포넌트로 추출하고 state를 그 안으로 옮겨야 한다.
- 리액트는 특별한 이유가 없는 한 캐시된 함수를 버리지 않는다. 예를 들어, 개발 단계에서 컴포넌트의 파일을 수정할 때 리액트는 캐시를 버린다. 리액트는 개발 중이든 생산 중이든 초기 마운트 중에 컴포넌트가 일시 중단되면 캐시를 벌니다. 향후 리액트는 캐시를 버리는 것의 이점을 취하는 더 많은 기능을 추가할 수 있다. 예를 들어, 향후 리액트는 가상화된 목록에 대한 빌트인 지원이 추가되면 가상화된 테이블 뷰포트에서 스크롤되는 항목에 대한 캐시도 버리는 것도 이해가 될 것이다. 성능 최적화를 위해 useCallback에 의존하는 경우 기대에 부합할 것이다. 그렇지 않은 경우 state 변수나 ref가 더 적합할 수 있다.
사용법
컴포넌트 리렌더링 건너뛰기
렌더링 성능을 최적화할 때 자식 컴포넌트에 전달하는 함수를 캐시해야 할 때가 있다. 먼지 이를 수행하는 방법에 대한 구문을 살펴 본 다음 어떤 경우에 유용한지 알아보자.
컴포넌트의 리렌더링 사이에 함수를 캐시하려면, 해당 함수의 정의를 useCallback 훅으로 감싼다.
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
useCallback을 사용하려면 두 가지를 전달해야 한다.
- 리렌더링 사이에 캐시할 함수
- 함수 내에서 사용되는 컴포넌트 내부의 모든 값을 포함하는 의존성 배열
초기 렌더링 시에는 useCallback에서 반환되는 함수는 처음에 전달했던 함수이다.
다음 렌더링부터는 리액트는 이전 렌더링에서 전달된 의존성과 비교한다. 만약 의존성 중 변경된 것이 없다면(Object.is로 비교), useCallback은 이전과 같은 함수를 반환한다. 그렇지 않으면 useCallback은 이번 렌더링에서 전달한 함수를 반환한다.
즉, useCallback은 의존성이 변경되기 전까지는 리렌더링에 대해 함수를 캐시한다.
useCallback이 언제 유용한지 예시를 통해 살펴보자.
ProductPage에서 ShippingForm컴포넌트로 handleSumit 함수를 전달한다고 가정해보면
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
theme prop을 토글하면 앱이 잠시 멈추는 것을 알아차렸겠지만 JSX에서 <ShippingForm/>을 제거하면 빠르게 느껴진다. 이는 ShippingForm 컴포넌트를 최적화할 가치가 있다는 것을 알려준다.
기본적으로 컴포넌트가 리렌더링되면 리액트는 모든 자식들을 재귀적으로 리렌더링한다. 이는 ProductPage가 다른 theme로 리렌더링될 때 ShippingForm 컴포넌트도 리렌더링 되기 때문이다. 이는 리렌더링하는 데 많은 계산이 필요하지 않은 컴포넌트에는 괜찮으나 리렌더링이 느리다는 것을 확인했다면, props가 지난 렌더링과 동일한 경우 memo로 감싸 ShippingForm에게 리렌더링을 건너뛰도록 지시할 수 있다.
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
이 변경으로 ShippingForm은 모든 props가 마지막 렌더링과 동일한 경우 리렌더링을 건너뛴다. 바로 이때 함수 캐싱이 중요해진다. useCallback없이 handleSubmit을 정의 했다고 가정해보면
function ProductPage({ productId, referrer, theme }) {
// Every time the theme changes, this will be a different function...
// 테마가 변경될 때마다, 이 함수는 달라집니다...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ... so ShippingForm's props will never be the same, and it will re-render every time */}
{/*따라서 ShippingForm의 props는 절대 같지 않으며, 매번 리렌더링 됩니다.*/}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
JavaScript에서 function(){} 또는 () => {}는 {} 객체 리터럴이 항상 새 객체를 생성하는 것과 유사하게 항상 다른 함수를 생성한다. 일반적으로는 문제가 되지 않지만 ShippingForm의 props는 결코 동일하지 않으며 memo 최적화가 작동하지 않는다는 의미이다. 바로 이 지점에서 useCallback이 유용하다.
function ProductPage({ productId, referrer, theme }) {
// Tell React to cache your function between re-renders...
// 리렌더링 사이에 함수를 캐싱하도록 지시합니다...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...so as long as these dependencies don't change...
// ...따라서 이 의존성이 변경되지 않는 한...
return (
<div className={theme}>
{/* ...ShippingForm will receive the same props and can skip re-rendering */}
{/* ...ShippingForm은 동일한 props를 받으므로 리렌더링을 건너뛸 수 있습니다.*/}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
handleSubmit을 useCallback으로 감싸면, 리렌더링 사이에 동일한 함수가 되도록 할 수 있다(의존성이 변경될때까지). 특별한 이유가 없는 한 함수를 useCallback으로 감쌀 필요는 없다. memo로 감싼 컴포넌트에 함수를 전달하면 이로 인해 리렌더링을 건너뛸 수 있게 되기 때문이다.
메모된 콜백에서 state 업데이트하기
때로는 메모된 콜백의 이전 state를 기반으로 state를 업데이트해야 할 수도 있다.
이 handleAddTodo 함수는 다음 할일을 계산하기 위해 todos를 의존성으로 지정하였다.
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
일반적으로 메모화된 함수는 가능한 적은 의존성을 갖기를 원할 것이다. 다음 state를 계산하기 위해 일부 state만 읽어야 하는 경우, 대신 업데이터 함수를 전달하여 해당 의존성을 제거할 수 있다.
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ No need for the todos dependency
// ✅ todos에 대한 의존성이 필요하지 않음
// ...
여기서는 todos를 의존성으로 만들고 내부에서 읽는 대신, state를 업데이트하는 방법에 대한 지시사항( todos => [...todos, newTodo])을 리액트에 전달한다.
Effect가 너무 자주 발동되지 않도록 하기
때론 Effect내부에서 함수를 호출하고 싶은 경우가 있다.
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();
// ...
이로 인해 문제가 발생한다. 모든 반응형 값을 Effect의 의존성으로 선언해야 한다. 그러나 createOptions를 의존성으로 선언하면 Effect가 채팅방에 계속 재연결하게 된다.
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problem: This dependency changes on every render
// 🔴 문제: 이 의존성은 렌더링시마다 변경됨
// ...
이 문제를 해결하려면 Effect에서 호출해야 하는 함수를 useCallback으로 감싸면 된다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Only changes when roomId changes
// ✅ roomId 변경시에만 변경됨
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Only changes when createOptions changes
// ✅ createOptions 변경시에만 변경됨
// ...
이렇게 하면 roomId가 동일한 경우 리렌더링 사이에 createOptions 함수가 동일하게 적용된다. 하지만 함수 의존성을 없애는 편이 더 좋다. 함수를 Effect 내부로 이동해야 한다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ No need for useCallback or function dependencies!
// ✅ useCallback이나 함수에 대한 의존성이 필요하지 않음!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Only changes when roomId changes
// ✅ roomId 변경시에만 변경됨
// ...
이제 코드가 더 간단해졌으며 useCallback이 필요하지 않다.
커스텀 훅 최적화하기
커스텀 훅을 작성하는 경우 반환하는 모든 함수를 useCallback으로 감싸는 것이 좋다.
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,
};
}
이렇게 하면 훅의 소비자가 필요할 때 자신의 코드를 최적화할 수 있다.
'React-study > dil' 카테고리의 다른 글
[DIL] useDeferredValue (0) | 2024.06.24 |
---|---|
[DIL] useTransition (0) | 2024.06.24 |
[DIL] useMemo (0) | 2024.06.17 |
[DIL] useLayoutEffect (0) | 2024.06.11 |
[DIL] useEffect (0) | 2024.06.04 |