useCallback
은 리렌더 과정 사이에서 함수 정의를 캐싱하는 리액트 훅이에요.
const cachedFn = useCallback(fn, dependencies)
Reference | 레퍼런스
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]);
}
더 많은 예시를 보려면 아래를 참고하세요.
Parameters | 파라미터(매개변수)
fn
: 캐싱하고 싶은 함수값. 어떤 인자든 받을 수 있고 어떤 값이든 반환할 수 있어요. 리액트는 초기 렌더링 동안 함수를 다시 반환해요. 다음 렌더링에서 리액트는 마지막 렌더링에서dependencies
가 변하지 않았다면 같은 함수를 다시 반환할 거예요. 그렇지 않으면 현재 렌더링 동안 인자로 넘겨 받은 함수를 주고 나중에 재사용될 것을 대비하여 그것을 저장해요. 리액트는 당신의 함수를 호출하지 않아요. 함수는 반환되기 때문에 언제 호출할지 혹은 호출을 할지말지는 당신이 결정할 수 있어요.
dependencies
:fn
코드 내부에서 참조하는 모든 반응값의 목록. 반응값은 props, 상태 그리고 모든 변수와 컴포넌트 바디 안에서 직접 선언된 함수를 포함해요. 만약 사용하고 있는 린트(linter)가 리액트용으로 구성되었다면, 모든 반응 값이 종속성으로 알맞게 지정되었는지 검증해요. 종속성의 목록은 아이템의 상수값을 갖고 있고[dep1, dep2, dep3]
와 같은 인라인 형태로 작성되어야해요. 리액트는 각 종속성을Object.js
의 비교 알고리즘을 사용하여 이전 값과 비교해요.
Returns | 반환값
최초 렌더링에서 useCallback
은 넘겨받은 fn
함수를 반환해요.
이후 진행되는 렌더링에서 (만약 종속성에 변화가 없다면) 마지막 렌더링에서 이미 저장된 fn
함수를 반환하거나 이번 렌더링에서 넘겨받은 fn
함수를 반환해요.
Caveat | 주의사항
useCallback
은 훅이기 때문에 최상단 컴포넌트 안 또는 커스텀 훅에서만 호출할 수 있어요. 반복문이나 조건문 안에서는 호출하면 안돼요. 만약에 반복문이나 조건문 안에서 호출해야한다면 새로운 컴포넌트를 추출하고 그 안에 상태를 넣으세요.- 리액트는 캐싱된 함수를 버려야하는 특별한 이유가 없는 한 버리지 않아요. 예를 들어, 개발과정에서 리액트는 컴포너틑 파일을 수정할 때 캐시를 버려요. 개발과 프로덕션 과정에서 리액트는 최초 마운트 중에 컴포넌트가 연기된다면 캐시를 버려요. 추후에 리액트는 캐시를 버리는 것의 이득을 얻을 수 있는 기능들이 추가될 수도 있어요. 예를 들어 추후에 리액트가 가상화된 목록에 대한 내장 지원을 추가한다면 가상 테이블의 뷰포트 밖으로 스크롤된 항목에 대한 캐시를 삭제하는 게 합리적일 거예요. 만약 성능 최적화를
useCallback
에 의존한다면 이 기능은 기대에 충족할 거예요. 그렇지 않다면 상태 변수나 레프(ref)가 더 적절해요.
Usage | 용법
Skipping re-rendering of components | 컴포넌트의 리렌더링을 건너뛰기
렌더링 성능 최적화를 할 때 때때로 자식 컴포넌트에 넘겨줘야하는 함수를 캐싱해야 할 필요가 있을 거예요. 어떻게 하는지 문법을 먼저 보고 언제 유용할지 살펴볼게요.
컴포넌트를 리렌더링 하는 중에 함수를 캐싱하기 위해서는 함수 정의를 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.js
로 비교해서) 바뀐 종속성이 없다면 useCallback
은 전과 동일한 함수를 반환해요. 그렇지 않다면 useCallback
은 현재 렌더링에서 전달받은 함수를 반환해요.
즉, useCallback
은 종속성이 바뀔 때까지 리렌더링 사이에 함수를 캐싱해요.
이 기능이 언제 유용한지 예시를 통해 살펴보아요.
ProductPage
컴포넌트에서 ShippingFrom
컴포넌트로 handleSubmit
함수를 넘긴하고 해볼게요.
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
theme
prop을 토글하면 앱이 잠시 멈추지만 <ShippingForm />
을 JSX에서 삭제하면 빠르다고 느낄 거예요. ShippingForm
컴포넌트 최적화를 시도할 가치가 있음을 알려줘요.
기본적으로 컴포넌트가 리렌더링 될 때, 리액트는 재귀적으로 모든 자식 컴포넌트를 리렌더링해요. 이것이 바로 ProductPage
가 다른 theme
으로 리렌더링 될 때, ShippingForm
컴포넌트 또한 리렌더링이 되는 이유에요. 리렌더링 할 때 많은 계산이 필요하지 않는 컴포넌트는 괜찮아요. 하지만 리렌더링이 느리다는 것을 확인한다면, memo
로 감싸서 ShippingForm
의 props가 마지막 렌더링과 같을 때 리렌더링을 건너뛰도록 할 수 있어요.
import { memo } from 'react';
const ShippingForm = memo(function ShippingFrom({ }) {
// ...
});
이러한 변경으로 ShippingForm
의 모든 props가 마지만 렌더링 상태와 동일하다면 ShippingForm
은 리렌더링을 건너뛸 거예요. 이 때가 함수를 캐싱하는 것이 중요해지는 순간이에요. useCallback
훅 없이 handleSubmit
를 정의한다고 생각해봐요.
function ProductPage({ productId, referrer, theme }) {
// theme이 바뀔 때 마다, 다른 함수가 될 것이고 ...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{
/* ... 그래서 ShippingForm의 props는 절대 같을 수 없으며 매번 리렌더링 될 거예요. */
<ShippingForm onSubmit={handleSubmit} />
}
</div>
);
}
자바스크립트에서 function () {}
또는 () => {}
는 {}
객체 리터럴이 항상 새로운 객체를 생성하는 방법과 비슷하게 항상 다른 함수를 생성해요. 일반적으로 이건 문제가 되지는 않지만 ShippingForm
props가 절대 같지 않고 memo
최적화가 제대로 동작하지 않는다는 것을 의미해요. 이곳이 바로 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>
)
}
useCallback
안에 handleSubmit
컴포넌트를 넣고 감싸면 (의존성들이 변하지 않을 때까지) 리렌더링 사이에 동일한 함수임을 보장해요. 특정한 이유로 리렌더링을 하지 않는다면, useCallback
안에서 함수를 래핑할 필요가 없어요. 이 예시에서는 함수를 memo
로 감싸진 컴포넌트에 전달하기 때문이며, 이를 통해 리렌더링을 건너뛸 수 있어요. 이 페이지에서 더 자세히 설명된 것처럼 useCallback
이 필요한 다른 이유들도 있어요.
NOTE
성능 최적화 수단으로만useCallback
에 의존해야만 해요.useCallback
없이 코드가 동작하지 않는다면 근본적인 문제를 찾고 그 문제를 먼저 해결해야해요. 그런 다음에useCallback
을 추가하세요.
Updating state from a memoized callback | 메모이제이션 된 콜백으로부터 상태 업데이트 하기
가끔 메모이제이션 된 콜백으로부터 온 이전의 상태에 기반한 상태 업데이트가 필요할 때가 있어요.
이 handleAddTodo
함수는 todos
로 부터 다음 todos를 계산하기 때문에 의존성으로 todos
를 열거해요.
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
}
보통은 가능한 적은 의존성을 가지고 함수를 메모이제이션 하고 싶을 거예요. 다음 상태를 계산하기 위해서만 일부 상태를 읽고 싶다면, 의존성 대신 updater function을 전달하여 해당 의존성을 제거할 수 있어요.
function TodoList() {
const [todos, setTodos] = setState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ todos 의존성이 필요하지 않아요
// ...
}
여기서는 todos
를 의존성으로 만드는 대신 내부에서 읽어 어떤헤 상태를 업데이트하는지를 리액트로 전달해요. updater function에 대해 더 알고 싶다면 이 문서를 읽어보세요.
Preventing an Effect from firing too often | 너무 자주 발생하는 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의 의존성으로 선언되어야만 해요. 하지만 creactOptions
를 의존성으로 선언한다면 Effect를 지속적으로 채팅방과 재연결시킬 거예요.
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 문제점 : 이 의존성은 모든 렌더링에서 변화해요.
}
// ...
이를 해결하려면 Effect에서 불러와야하는 함수를 useCallback
으로 감싸야해요.
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가 바뀔 때만 변경돼요.
}
이는 roomId
의 값이 같으면 리렌더링 사이에 createOptions
함수가 동일하는 것을 보장해요. 그러나, 함수 의존성 배열의 필요성을 없애는 것이 좋아요. Effect 안으로 함수를 옮겨볼게요.
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가 바뀔 때만 변경돼요.
// ...
}
이제 코드가 더 간단해지고 useCallback
을 이용할 필요가 없어졌어요. Effect 의존성 배열을 지우는 것을 더 알아보고 싶다면 여기를 눌러주세요.
Optimizaing a custom Hook | 커스텀 훅 최적화하기
만약 커스텀 훅을 사용하고 있다면, 반환하는 모든 함수를 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,
};
}
이 훅의 사용자가 필요할 때 그들의 코드를 최적화할 수 있도록 만들어요.
Troubleshooting | 트러블슈팅
Every time my component renders, useCallback returns a different function | 내 컴포넌트가 렌더링 될 때마다, useCallback
가 다른 함수를 반환해요.
두번째 인자로 의존성을 지정해주세요!
의존성 배열을 잊어버린다면, useCallback
은 매번 새로운 함수를 반환해요:
function ProductPage({ productId, referrer }){
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 매번 새로운 함수를 리턴해요: 의존성 배열이 없기 때문이에요.
// ...
}
다음 예시는 두번째 인자로 의존성 배열을 전달한 올바른 예시에요:
function ProductPage({ productId, referrer }){
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ 불필요하게 새 함수를 리턴하지 않아요.
// ...
}
만약 도움이 되지 않는다면. 최소 한개의 의존성이 이전 렌더링과 달라져서 생긴 문제예요. 콘솔창을 통해 직접 의존성을 로깅하여 이 문제를 디버깅할 수 있어요.
const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);
console.log([productId, referrer]);
그리고 나서 콘솔창에서 서로 다른 리렌더 과정에서 출력된 배열을 우클릭하여 " 전역변수로 저장하기"를 선택하세요. 첫 번째는 temp1
로, 두번째는 temp2
로 저장된다면 두 배열에 있는 각각의 의존성이 같은지를 브라우저 콘솔을 사용해 확인할 수 있어요.
Object.is(temp1[0], temp2[0]); // 첫번째 의존성이 두 배열에서 같나요?
Object.is(temp1[1], temp2[1]); // 두번째 의존성이 두 배열에서 같나요?
Object.is(temp1[2], temp2[2]); // 위와 동일한 의미를 가져요.
어떤 의존성이 메모이제이션을 방해하는지 찾을 때, 의존성을 제거하거나 메모이제이션할 수 있는 방법을 찾으세요.
I need to call useCallback for each list item in a loop, but it’s not allowed | 반복문에서 각 리스트를 위한 useCallback을 호출하고 싶은데 허용되지 않아요.
Chart
컴포넌트가 memo
에 감싸져있다고 가정해볼게요. ReportList
컴포넌트가 리렌더링 될 때, 리스트에 있는 모든 Chart
리렌더링 과정을 건너뛰고 싶어요. 그러나 반복문에서는 useCallback
을 호출할 수 없어요.
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 아래와 같이 반복문 안에서 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:
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>
);
});
'리액트 공식문서 | React Docs > Reference > react@18.2.0' 카테고리의 다른 글
[Hooks] useDebugValue | useDebugValue 훅 (0) | 2024.01.11 |
---|---|
[Hooks] useContext | useContext 훅 (2) | 2024.01.07 |
[Hooks] use | use 훅 (1) | 2023.12.28 |
[Hooks] Hooks | 리액트 내장 훅 (0) | 2023.12.22 |
[Overview] React Reference Overview | 리액트 공식문서 개요 (0) | 2023.12.19 |