useCallback은 리렌더링간 함수 정의를 캐시해주는 Hook이다.
const cachedFn = useCallback(fn, dependencies)
fn: 캐싱할 함수. 어떤 인자나 반환값을 반환해도 된다. 첫 렌더링시에 이 함수를 반환한다. (함수를 호출하는 것이 아니다!) 이후 컴포넌트가 렌더링 되는 경우 dependencies가 동일하다면 React는 같은 함수를 반환한다. 만약 dependencies가 변경되었다면, 새롭게 함수를 생성하고 저장한다.dependencies: fn에서 참조되는 반응형 값들의 배열이다. Object.is 알고리즘을 사용해 비교한다.렌더링 성능을 최적화 할때는 자식 컴포넌트로 넘기는 함수를 캐싱해야 한다. 렌더링간 함수를 캐싱하기 위해서는 함수를 useCallback으로 감싸면 된다.
function ProductPage({productId, referrer, theme}) {
const handleSubmit = useCallback((orderDetails) => {
post('/product' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
이 handleSubmit함수를 자식 컴포넌트로 전달해보자
function ProductPage({ productId, referrer, theme }) {
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
}
자식 컴포넌트 ShippingForm은 props가 변하지 않았다면 렌더링을 건너뛸 수 있다. handleSubmit이 동일하다면 ShippingForm을 굳이 렌더링 할 필요가 없다.
리액트 컴포넌트는 순수하게 작성해야하기 때문에 인풋이 같다면 아웃풋도 같아야한다. 무조건.
useCallback은 성능 최적화를 위해서 사용해야한다. useCallback 없이 동작하지 않는다면 잘못 작성한것이다.
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text }
setTodos([...todos, newTodo])
}, [todos])
위 handleAddTodo는 todos상태가 변할때마다 새로 생성된다. 하지만 업데이트함수를 사용해서 의존성을 제거 할 수 있다.
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text }
setTodos([...todos, newTodo])
}, [])
useEffect에서 함수를 호출해야 하는 경우
function ChatRoom({ roomId }) {
const [message, setMessage] = useState("")
function createOptions() {
return {
serverUrl: 'https://...',
roomId: roomId
}
}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
}, [createOptions])
}
이렇게 useEffect에서 함수를 호출하는 경우 모든 반응형 값들이 Effect의 의존성으로 선언되어야 한다. 이경우 createOptions가 의존성으로 선언되고 createOptions가 변경될때마다 useEffect가 실행되고 connect하게된다.
const createOptions = useCallback(() => {
return {
serverUrl: 'https://...',
roomId: roomId
}
}, [roomId])
이렇게 createOptions를 useCallback으로 감싸게 되면 roomId가 변경되지 않는 이상 createOptions는 새로 생성되지 않는다.
다만 여전히 useEffect에 함수에 대한 의존성이 필요한 것이 불편하다면 createOptions를 useEffect내부로 이동시킬 수 있다.
useEffect(()=> {
function createOptions() {
return {
serverUrl: "https://...",
roomId: roomId
}
}
const options = createOptions()
const connection = createConnection()
connection.connect()
return () => connection.disconnect()
}, [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,
}
}