useCallback(fn, dependencies)useCallback 매개변수useCallback 반환값useCallback 사용시 주의 사항useCallback 사용법1. 컴포넌트 리렌더링 건너뛰기Q. useCallback이 언제 유용할까?Q. useCallback과 useMemo는 무슨 관련이 있을까요?Q. useCallback을 모든 곳에 추가해야 하나요?2. 메모된 콜백에서 state 업데이트하기3. Effect가 너무 자주 발동되지 않도록 하기4. 커스텀 훅 최적화하기[문제 해결]1. useCallback은 컴포넌트가 렌더링될 때마다 다른 함수를 반환해요2. 루프 안에서 목록의 각 항목에 대해 useCallback를 호출하려 하는데, 허용되지 않는다고 합니다
useCallback(fn, dependencies)
최상위 컴포넌트에서
useCallback
을 호출하여 리렌더링 사이에 함수 정의를 캐시한다.useCallback 매개변수
fn
: 캐시하려는 함수 값. 어떤 인자도 받을 수 있고 어떤 값이라도 반환할 수 있다. React는 초기 렌더링을 하는 동안 함수를 반환한다 (호출하지 않는다!). 다음 렌더링에서 React는 마지막 렌더링 이후 dependencies
가 변경되지 않았다면 동일한 함수를 다시 제공한다. 그렇지 않으면 현재 렌더링 중에 전달한 함수를 제공하고 나중에 재사용할 수 있도록 저장한다. React useCallback은 함수를 호출하지 않고 반환만하기 때문에 호출 시기와 여부를 결정할 수 있다.dependencies
: fn
코드 내에서 참조된 모든 반응형 값의 배열이다. 반응형 값에는 props, state, 컴포넌트 본문 내부에서 직접 선언한 모든 변수 및 함수가 포함된다. 린터가 React용으로 구성된 경우, 모든 반응형 값이 의존성으로 올바르게 지정되었는지 확인한다. 의존성 배열에는 일정한 수의 항목이 있어야 하며 [dep1, dep2, dep3]
과 같이 인라인으로 작성해야 한다. React는 Object.is
비교 알고리즘을 사용하여 각 의존성을 이전 값과 비교한다.useCallback 반환값
초기 렌더링에서
useCallback
은 전달한 fn
함수를 반환한다. /렌더링 중에는 마지막 렌더링에서 이미 저장된
fn
함수를 반환하거나(의존성이 변경되지 않은 경우), 렌더링 중에 전달했던 fn
함수를 반환한다.useCallback 사용시 주의 사항
useCallback
은 훅이므로 컴포넌트의 최상위 레벨이나 자체 훅에서만 호출할 수 있다. 반복문이나 조건문 내부에서는 호출할 수 없다. 필요한 경우 새로운 컴포넌트로 추출하고 state를 그 안으로 옮겨라
useCallback 사용법
1. 컴포넌트 리렌더링 건너뛰기
렌더링 성능을 최적화할 때 자식 컴포넌트에 전달하는 함수를 캐시해야 할 때가 있다.
컴포넌트의 리렌더링 사이에 함수를 캐시하려면, 해당 함수의 정의를
useCallback
훅으로 감싸라import { useCallback } from 'react'; function ProductPage({ productId, referrer, theme }) { const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }, [productId, referrer]); // ...
useCallback
을 사용하려면 두 가지를 전달해야 합니다:- 리렌더링 사이에 캐시할 함수
- 함수 내에서 사용되는 컴포넌트 내부의 모든 값을 포함하는 의존성 배열
초기 렌더링 시에는
useCallback
에서 반환되는 함수는 처음에 전달했던 함수이며,다음 렌더링부터는, React는 이전 렌더링에서 전달된 의존성과 비교합니다. 만약 의존성 중 변경된 것이 없다면,
useCallback
은 이전과 같은 함수를 반환합니다. 그렇지 않으면 useCallback
은 렌더링에서 전달한 함수를 반환합니다.즉,
useCallback
은 의존성이 변경되기 전까지는 리렌더링에 대해 함수를 캐시한다.Q. useCallback이 언제 유용할까?
Example : ShippingForm 컴포넌트를 최적화하고 있다.
ProductPage
에서 ShippingForm
컴포넌트로 handleSubmit
함수를 전달한다고 가정해 보겠다function ProductPage({ productId, referrer, theme }) { // ... return ( <div className={theme}> <ShippingForm onSubmit={handleSubmit} /> </div> );
가정 :
theme
prop을 토글하면 앱이 잠시 멈추는 것을 알아차렸겠지만, JSX에서 <ShippingForm />
을 제거하면 빠르게 느껴진다. 이는 ShippingForm
컴포넌트를 최적화할 가치가 있다는 것을 알려준다.기본적으로 컴포넌트가 리렌더링되면 React는 모든 자식들을 재귀적으로 리렌더링한다. 이는 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
로 감싼 컴포넌트에 함수를 전달하면 이로 인해 리렌더링을 건너뛸 수 있게 되기 때문이다. useCallback
은 성능 최적화를 위해 사용해야 한다.
만약 useCallback
없이 코드가 동작하지 않는다면, 먼저 근본적인 문제를 찾아 수정해라. 그런 다음 useCallback
을 다시 추가해라.Q. useCallback과 useMemo는 무슨 관련이 있을까요?
useMemo
와 함께 useCallback
이 자주 사용되는 것을 볼 수 있다. 자식 컴포넌트를 최적화하려고 할 때, 두 가지 모두 유용하다. 전달하는 값을 memoize(캐시)할 수 있게 해준다.import { useMemo, useCallback } from 'react'; function ProductPage({ productId, referrer }) { const product = useData('/product/' + productId); 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
객체를 전달할 수 있다. 필요한 경우, React는 렌더링 중에 전달된 함수를 호출하여 결과를 계산한다.useCallback
은 함수 자체를 캐시한다.useMemo
와 달리, 제공한 함수를 호출하지 않는다. 대신 제공한 함수를 캐시하여productId
또는referrer
가 변경되지 않는 한handleSubmit
자체가 변경되지 않도록 한다. 이렇게 하면 불필요하게ShippingForm
을 리렌더링하지 않고도 함수를 전달할 수 있다. 사용자가 폼을 제출할 때까지 코드가 실행되지 않는다.
Q. useCallback을 모든 곳에 추가해야 하나요?
이 사이트(리액트 공식문서)와 같이 대부분의 인터랙션이 투박한 앱의 경우(페이지 또는 전체 섹션 교체 등) 일반적으로 메모화는 필요하지 않다. 반면 앱이 그림 편집기처럼 대부분의 인터랙션이 도형 이동처럼 세분화되어 있다면 메모화가 매우 유용할 수 있다.
✨
useCallback
으로 함수를 캐싱하는 것은 몇 가지 경우에만 유용하다memo
로 감싼 컴포넌트에 prop으로 함수를 전달하고자 할 때
값이 변경되지 않았다면 렌더링을 건너뛰고 싶을 것이다. 메모화를 사용하면 의존성이 변경된 경우에만 컴포넌트를 리렌더링할 수 있다.
- 전달한 함수는 나중에 일부 훅의 의존성으로 사용될 때
예를 들어,
useCallback
으로 감싼 다른 함수가 이 함수에 의존하거나, useEffect
에서 이 함수에 의존할 수 있다. (의존성으로 사용되는 함수가 항상 바뀌어버리면 계속 렌더링이 일어나게 됨)그 외의 경우에는 함수를
useCallback
으로 감싸는 것이 이득이 없다. 그렇게 한다고 해서 크게 해가 되는 것도 아니기 때문에 일부 팀에서는 개별 사례에 대해 생각하지 않고 가능한 한 많이 메모하는 방식을 선택하기도 한다. 그러나! 단점은 코드 가독성이 떨어진다는 것이다.
또한 모든 메모화가 효과적인 것은 아니다.
“항상 새로운” 단일 값만으로도 전체 컴포넌트에 대한 메모화가 깨질 수 있다.
useCallback
은 함수 생성을 막는 기능이 아니다여러분은 항상 함수를 생성하지만(그리고 그것은 괜찮다!), React는 변경된 것이 없다면 이를 무시하고 캐시된 함수를 반환한다.
사실, 다음 몇 가지 원칙을 따르면 많은 메모화를 불필요하게 만들 수 있다
- 컴포넌트가 다른 컴포넌트를 시각적으로 감쌀 때, JSX를 children으로 받아들이도록 해라 그러면 wrapper 컴포넌트가 자신의 state를 업데이트 하더라도 React는 자식 컴포넌트가 리렌더링할 필요가 없다는 것을 알 수 있다.
- 로컬 state (컴포넌트 내에서만 사용하는 state)를 선호하고 필요 이상으로 state를 끌어올리지 마라. 트리 상단 또는 전역 상태 라이브러리에 폼(form)이나 아이템(item)의 호버(hover) 상태와 같은 일시적인 state를 유지하지 마라.
- 렌더링 로직을 순수하게 유지하라. 만약 컴포넌트를 리렌더링했을 때 문제가 발생하거나 눈에 띄는 시각적 부자연스러움이 생성된다면 컴포넌트에 버그가 있는 것이다! 메모화를 추가하는 대신 버그를 수정해라
(캐싱으로 리렌더링을 막기 전에 실제 버그가 뭔지 발견하고 처리할 것!)
- state를 업데이트하는 불필요한 Effect를 피해라 React 앱에서 대부분의 성능 문제는 컴포넌트를 반복적으로 렌더링하게 하는 Effect의 업데이트 체인으로 인해 발생한다.
- Effect에서 불필요한 의존성을 제거해라. 예를 들어, 메모화 대신 일부 객체나 함수를 Effect 내부나 컴포넌트 외부로 이동하는 것이 더 간단할 때가 많다.
- 특정 인터랙션이 여전히 느리게 느껴진다면 React 개발자 도구 프로파일러를 사용해 어떤 컴포넌트가 메모화의 이점을 가장 많이 누리는지 확인하고, 필요한 경우 메모화를 추가해라. 이러한 원칙은 컴포넌트를 더 쉽게 디버깅하고 이해할 수 있게 해주므로 어떤 경우든 이 원칙을 따르는 것이 좋다. 장기적으로는 이 문제를 완전히 해결하기 위해 메모화를 자동으로 수행하는 방법을 연구하고 있다.
2. 메모된 콜백에서 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]
)을 React에 전달한다.3. 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
이 필요하지 않다. Effect 의존성 제거4. 커스텀 훅 최적화하기
커스텀 훅을 작성하는 경우 반환하는 모든 함수를
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, }; }
이렇게 하면 훅의 소비자가 필요할 때 자신의 코드를 최적화할 수 있다.
[문제 해결]
1. useCallback
은 컴포넌트가 렌더링될 때마다 다른 함수를 반환해요
두 번째 인자로 의존성 배열을 지정했는지 확인하세요!
의존성 배열을 잊어버린 경우
useCallback
은 매번 새로운 함수를 반환한다.function ProductPage({ productId, referrer }) { const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }); // 🔴 Returns a new function every time: no dependency array // 🔴 매 번 새 함수를 반환함: 의존성 배열이 없음 // ...
다음은 의존성 배열을 두 번째 인수로 전달하는 수정된 버전이다
function ProductPage({ productId, referrer }) { const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }, [productId, referrer]); // ✅ Does not return a new function unnecessarily // ✅ 불필요하게 새 함수를 반환하지 않음 // ...
그래도 도움이 되지 않는다면 의존성 중 하나 이상이 이전 렌더링과 다르기 때문일 수 있다. 의존성을 콘솔에 수동으로 로깅하여 이 문제를 디버그할 수 있다
const handleSubmit = useCallback((orderDetails) => { // .. }, [productId, referrer]); console.log([productId, referrer]);
그런 다음 콘솔에서 서로 다른 리렌더의 배열을 마우스 오른쪽 버튼으로 클릭하고 두 배열 모두에 대해 “전역 변수로 저장”을 선택해라. 첫 번째 배열이
temp1
로 저장되고 두 번째 배열이 temp2
로 저장되었다고 가정하면 브라우저 콘솔을 사용하여 두 배열의 각 의존성이 동일한지 확인할 수 있다:Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays? // 각 배열의 첫번째 의존성이 동일한가? Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays? // 각 배열의 두번째 의존성이 동일한가? Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ... // ... 나머지 모든 의존성에 대해 반복 ...
어떤 의존성이 메모화를 방해하는지 찾았다면, 그 의존성을 제거할 방법을 찾거나 해당 의존성도 메모화하면 된다.
2. 루프 안에서 목록의 각 항목에 대해 useCallback
를 호출하려 하는데, 허용되지 않는다고 합니다
Chart
컴포넌트가 memo
로 감싸져 있다고 가정해 보자. ReportList
컴포넌트가 리렌더링할 때 목록의 모든 Chart
를 리렌더링하는 것을 건너뛰고 싶을 수 있다. 그러나 반복문에서는 useCallback
을 호출할 수 없다function ReportList({ items }) { return ( <article> {items.map(item => { // 🔴 You can't call useCallback in a loop like this: // 🔴 useCallback는 루프 안에서 호출할 수 없습니다: const handleClick = useCallback(() => { sendReport(item) }, [item]); return ( <figure key={item.id}> <Chart onClick={handleClick} /> </figure> ); })} </article> ); }
대신 개별 항목에 대한 컴포넌트를 추출하고 거기에
useCallback
을 넣으세요:function ReportList({ items }) { return ( <article> {items.map(item => <Report key={item.id} item={item} /> )} </article> ); } function Report({ item }) { // ✅ Call useCallback at the top level: // ✅ useCallback은 컴포넌트의 최상위 레벨에서 호출하세요: const handleClick = useCallback(() => { sendReport(item) }, [item]); return ( <figure> <Chart onClick={handleClick} /> </figure> ); }
또는 마지막 스니펫에서
useCallback
을 제거하고 대신 report
자체를 memo
로 감싸는 방법도 있다. item
의 prop이 변경되지 않으면 report
가 리렌더링을 건너뛰므로 Chart
도 리렌더링을 건너뛸 것이다function ReportList({ items }) { // ... } const Report = memo(function Report({ item }) { function handleClick() { sendReport(item); } return ( <figure> <Chart onClick={handleClick} /> </figure> ); });