준영이의 성장일기(FrontEnd)
React 공식문서 도장깨기(5) - useCallback 본문
이전 내용에 이어서 이번에는 useCallback에 대해서 정리하고자 한다. useMemo와 비슷하게 성능 최적화와 관련이 있다.
<참고 레퍼런스>
https://react-ko.dev/reference/react/useCallback
useCallback – React
The library for web and native user interfaces
react-ko.dev
[React] useCallback 사용법 정리
👩💻 이 글을 쓰게 된 계기... React는 Virtual DOM을 사용하여 컴포넌트의 렌더링 성능을 최적화한다. 그러나 불필요한 렌더링이 발생할 수 있는 상황이 있다. 이러한 상황에서는 React의 useCallback
opendeveloper.tistory.com
공식문서와 블로그들을 정독 하면서 개념을 다시 학습했다. 특히 해당 블로그 마지막 부분에서 말해주신 useEffect와 useCallback을 혼용해서 잘 사용하는 것이 중요한거 같다. 다들 읽어봤으면 좋겠다.
<useCallback>
개념: useCallback은 리렌더링 사이에 함수 정의를 캐시할 수 있게 해주는 React 훅이다.
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
✅ fn
캐시하려는 함수 값을 의미하고 위에 코드에서 ,를 기준으로 첫 번째 인자에 해당한다. 초기 렌더링을 하는 동안 함수를 반환하고(호출 X) 다음 렌더링에서 React는 마지막 렌더링 이후 dependencies가 변경되지 않았다면 동일한 함수를 다시 제공한다. useMemo와 마찬가지로 재 사용성 이점을 가지고 있고 차이점은 함수의 재 사용성이 가능하도록 해준다.
✅ dependencies
useEffect, useMemo와 마찬가지로 fn 코드 내에서 참조된 모든 반응형 값의 배열이다. 즉, 함수 내에서 사용되는 컴포넌트 내부의 모든 값을 포함하는 의존성 배열을 의미한다.
❗주의: useCallback은 모든 상황에서 사용해야 하는 것은 아니다. 오히려 불필요하게 많은 함수를 생성하면 렌더링 성능이 떨어질 수 있다. props로 전달되는 함수가 자주 재 생성되는 경우처럼 빈번하게 재 생성되는 경우를 판단하여 적용하는 것이 좋다.
<컴포넌트 리렌더링 건너뛰기>
성능을 최적화하는 것이 목표일 때 자식 컴포넌트에 전달하는 함수를 캐시해야 하는 상황, 즉 자식 컴포넌트에 props로 함수를 전달하는 상황이 있을 것이다. 해당 상황에서 컴포넌트의 리렌더링 사이에 함수를 캐시하려면, 해당 함수의 정의를 useCallback 훅으로 감싸주면 된다.
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
useCallback은 의존성이 변경되기 전까지는 리렌더링에 대해 함수를 캐시한다. 초기 렌더링 시에는 useCallback에서 반환되는 함수는 처음에 전달했던 함수이고 만약 의존성 배열의 값이 변경 됬다면 이번 렌더링에서 전달한 함수를 반환한다.(이전과는 다른 함수)
다른 예시를 보면 더 쉽게 이해할 수 있다.
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>
);
}
이전 useMemo에서 정리한 거와 동일하게 컴포넌트가 리렌더링 될 때 컴포넌트 내부에 있는 모든 코드들을 재귀적으로 호출한다. 이렇게 되면 리렌더링 될 때마다 새로운 handleSubmit 함수가 생성되므로 메모리제이션 이점을 살릴 수 없다. 또한 현재 코드를 보면 ProductPage는 theme를 매개변수로 받고 있는데 theme가 달라질 때마다 자식 컴포넌트도 리렌더링 되기 때문에 현재 코드는 수정해야 할 필요성이 있다.
이때 바로 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으로 감싸면서 의존성이 변경되기 전 까지 리렌더링 사이에 동일한 함수가 되도록 할 수 있다.
<useMemo vs useCallback>
const requirements = useMemo(() => { // Calls your function and caches its result
// 함수를 호출하고 그 결과를 캐시
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
// 함수 자체를 캐시
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
차이점은 캐시의 대상이다.
useMemo
호출한 함수의 결과를 캐시한다. product가 변경되지 않는 한 변경되지 않도록 computeRequirements(product)를 호출한 결과를 캐시한다. 즉, 불필요하게 ShippingForm을 리렌더링하지 않고도 requirements를 props로 전달할 수 있다.
useCallback
함수 자체를 캐시한다. 제공한 함수를 캐시하여 productId 또는 referrer가 변경되지 않는 한 handleSubmit 자체가 변경되지 않도록 한다. 즉, 불필요하게 ShippingForm을 리렌더링하지 않고도 handleSubmit 함수를 전달할 수 있다.
<메모된 콜백에서 state 업데이트 하기>
일반적으로 메모화된 함수는 가능한 적은 의존성을 갖기를 원한다. 이때 업데이터 함수를 이용하여 기존에 의존하던 todos를 제거할 수 있다. 두 코드의 차이점을 봐보자
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]);
}, []); // ✅ No need for the todos dependency
// ✅ todos에 대한 의존성이 필요하지 않음
todos에 대해서 의존하지 않으면서 newTodo를 하나씩 추가할 수 있다.
<useEffect와 useCallback의 콜라보>
import React, { useCallback, useEffect, useState } from "react";
function ExampleComponent(props) {
const [data, setData] = useState([]);
const fetchData = useCallback(async () => {
const response = await fetch("https://example.com/data");
const data = await response.json();
setData(data);
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
return (
<div>
{data.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
export default ExampleComponent;
다음 코드는 데이터를 불러오는 fetchData 함수를 최적화해주는 코드이다. 다른 분이 작성해준 블로그의 코드를 가져왔는데 실제 프로젝트에서도 api 호출을 통해 데이터를 가져오기 때문에 이해하면 좋은 코드라고 생각했다.
fetchData 함수가 변경될 때에만 useEffect가 실행되고 useCallback 훅을 사용하여 fetchData 함수가 매번 재생성되지 않고, 의존성 배열이 비어 있으므로 최초 렌더링 시 한 번만 생성한다.
<커스텀 훅과 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 { //useRouter라는 커스텀 훅에서 반환되는 함수들
navigate,
goBack,
};
}
navigate, goBack함수 둘 다 dispatch가 변경되지 않는 한 동일한 함수를 사용한다. 해당 함수들을 다른 컴포넌트에서 불러와서 이용할 경우에 동일한 함수 이기 때문에 불필요한 리렌더링을 막아준다.
<MyComponent라는 컴포넌트가 존재한다고 가정>
function MyComponent() {
const { navigate, goBack } = useRouter();
return (
<div>
<button onClick={() => navigate('/home')}>Go Home</button>
<button onClick={goBack}>Go Back</button>
</div>
);
}
➡️ navigate와 goBack이 useCallback으로 감싸져 있기 때문에 MyComponent가 리렌더링되더라도 두 함수는 동일한 참조를 유지한다.
다음 블로그 글은 React 공식문서 도장깨기(6) - useTransition 으로 돌아올 예정이다. 읽어주셔서 감사합니다:)
<다음 블로그 주제>
https://react-ko.dev/reference/react/useTransition
useTransition – React
The library for web and native user interfaces
react-ko.dev
'프론트엔드 > React.js' 카테고리의 다른 글
React 공식문서 도장깨기(7) - useContext (0) | 2024.09.09 |
---|---|
React 공식문서 도장깨기(6) - useTransition (2) | 2024.09.07 |
React 공식문서 도장깨기(4) - useMemo (2) | 2024.09.03 |
React 공식문서 도장깨기(3) - useRef (1) | 2024.08.29 |
React 공식문서 도장깨기(2)-2 : useEffect, useLayoutEffect (3) | 2024.08.27 |