본문 바로가기

FRONTEND

useCallback

 

 

useCallback 또한 useMemo와 마찬가지로 렌더링 최적화를 위한 memoization 기법을 사용합니다.

 

useMemo는 저장하고 싶은 값을 memoization 하여 컴포넌트를 최적화 한다면,

useCallback함수 자체를 memoization하여 컴포넌트를 최적화 합니다.

 

useMemo 와 동일하게 useCallback 또한 사전에 알아야 할 내용이 있습니다!

1) 함수형 컴포넌트는 단순한 자바스크립트 함수(객체)입니다.
2) 자바스크립트 함수는 일급객체로 취급되기 때문에 함수를 변수에 할당할 수 있습니다.
3) 컴포넌트를 렌더링 한다는 것은 해당 컴포넌트(함수)를 호출(실행)시킨다는 의미이므로 컴포넌트가 렌더링 되면 컴포넌트 내부의 변수는 전부 초기화 됩니다.

 

코드와 함께 위 내용이 구체적으로 어떤 의미인지 살펴보겠습니다.

const Component = () => {
const addOne = (number) => {
return number + 1;
}
return <div>{value}</div>
}

함수형 컴포넌트인 Component 내부에 addOne이라는 변수에 함수(객체)가 할당이 되어 있습니다. Component가 렌더링(실행)될 때마다 변수 addOne은 초기화 되어 새로운 함수(객체)를 할당 받습니다. 이 경우 addOne에 할당된 함수를 useCallback 훅으로 감싸게 된다면 렌더링 마다 addOne이 초기화 되는 것을 막을 수 있습니다.

const Component = () => {
const addOne = useCallback((number) => {
return number + 1;
}, [item]);
return <div>{value}</div>
}

이렇게 useCallback 함수를 meoization해준다면 Component가 맨 처음 렌더링 될 때만 함수 객체를 생성하여 addOne 변수를 초기화 해주고 이후 Component가 렌더링이 된다면 addOne 변수가 초기화 되는 것이 아니라 이전에 할당 받은 함수(객체)를 지속적으로 갖고 있으면서 재 사용할 수 있게 되는 것입니다.

useCallback 소개

형태

const cachedFn = useCallback(fn, dependencies)

 

fn

렌더링 마다 memoization(메모리에 저장, 캐싱)할 함수입니다. 사용자가 직접 작성하고 인자와 반환 값 모두 지정할 수 있습니다. 렌더링이 될 때 해당 함수가 실행되는 것이 아니라 반환 되어 변수 cachedFn에 저장됩니다. dependencies가 변경되지 않았다면 이전과 동일한 함수를 반환합니다.

 

dependencies

의존성 배열에 담긴 값이 변경된다면 현재 캐싱된 함수를 버리고 새로운 함수를 캐싱합니다.

 

사용

그렇다면 왜 이렇게 useCallback을 통해 함수를 기억해서 사용해야 할까요? 보통 useCallback은 컴포넌트의 불필요한 렌더링을 막아 최적화된 렌더링을 하기 위해 사용합니다. 다음 예제를 통해서 그 이유에 대해 살펴 보겠습니다.

 

예제코드는 배송 주소와 물건의 수량을 입력하는 어플리케이션입니다. 이 어플리케이션은 이 외에도 색상 테마를 변경할 수 있는 기능이 있습니다. 어플리케이션 구성은 App> Product 페이지(ProductPage.js) > ShippingForm 컴포넌트로 되어있습니다.

//App.js
import { useState } from 'react';
import ProductPage from './ProductPage.js';
export default function App() {
const [isDark, setIsDark] = useState(false);
return (
<>
<label>
<input
type="checkbox"
checked={isDark}
onChange={e => setIsDark(e.target.checked)}
/>
Dark mode
</label>
<hr />
<ProductPage
referrerId="wizard_of_oz"
productId={123}
theme={isDark ? 'dark' : 'light'}
/>
</>
);
}
//ProductPage.js
import ShippingForm from './ShippingForm.js';
export default function ProductPage({ productId, referrer, theme }) {
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
function post(url, data) {
// Imagine this sends a request...
console.log('POST /' + url);
console.log(data);
}
 
//ShippingForm.js
import { memo, useState } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
const [count, setCount] = useState(1);
console.log('[ARTIFICIALLY SLOW] Rendering <ShippingForm />');
let startTime = performance.now();
while (performance.now() - startTime < 500) {
// Do nothing for 500 ms to emulate extremely slow code
}
function handleSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const orderDetails = {
...Object.fromEntries(formData),
count
};
onSubmit(orderDetails);
}
return (
<form onSubmit={handleSubmit}>
<p><b>Note: <code>ShippingForm</code> is artificially slowed down!</b></p>
<label>
Number of items:
<button type="button" onClick={() => setCount(count - 1)}>–</button>
{count}
<button type="button" onClick={() => setCount(count + 1)}>+</button>
</label>
<label>
Street:
<input name="street" />
</label>
<label>
City:
<input name="city" />
</label>
<label>
Postal code:
<input name="zipCode" />
</label>
<button type="submit">Submit</button>
</form>
);
});
export default ShippingForm;

실제로 어플리케이션을 구성하여 작동시키면 테마를 변경할 때(Dark mode 체크 박스를 누를 때) 마다 짧은 시간 동안 화면이 아무 반응 없는 것을 알 수 있습니다. 그 이유는 테마를 변경할 때마다 연산 비용이 비싼 ShippingForm 컴포넌트가 리렌더링 되기 때문입니다. 사실 테마를 변경하는 동작으로 인해 ShippingForm 컴포넌트가 리렌더링 되는 현상은 불필요합니다. 바로 useCallback을 통해 최적화 해줘야 할 지점입니다.

 

이 현상은 테마가 변경될 경우, ProductPage 페이지 내에서 props로 전달된 theme의 변경으로 ProductPage 페이지가 리렌더링되고 리렌더링이 됨에 따라 handleSubmit 함수도 초기화가 되기 때문에 이로 인해 handleSubmitonSubmit props로 전달 받은 ShippingForm 컴포넌트가 리렌더링이 발생하는 것입니다.

 

따라서 아래의 코드처럼 ProductPage 컴포넌트의 handleSubmit 함수를 useCallback 함수로 감싸게 되면 화면이 멈추는 것 없이 테마만 적절하게 바뀌는 모습을 볼 수 있습니다.

//ProductPage.js
import { useCallback } from 'react';
import ShippingForm from './ShippingForm.js';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
function post(url, data) {
// Imagine this sends a request...
console.log('POST /' + url);
console.log(data);
}
 

또한 ShippingForm 컴포넌트를 React.memo 를 사용해 감싸준 것을 볼 수 있는데 useCallback 과 함께 memo를 사용하여 컴포넌트의 렌더링을 일으키지 않을 수 있습니다. 해당 부분은 React.memo에서 설명하겠습니다.

 

위의 경우에서 처럼, 해당 컴포넌트의 state, props 등이 변경되지 않음에도 리렌더링 되는 현상을 방지하기 위해서 주로 useCallback이 사용됩니다. useMemo와 마찬가지로 별도의 메모리를 할애해서 저장하는 것이므로 꼭 필요한 경우에만 사용할 수 있도록 합니다.

함수를 캐싱하는 useCallback 훅이 사용되는 경우가 몇가지 있는데 다음과 같습니다.

 

참고

https://react.dev/reference/react/useCallback

'FRONTEND' 카테고리의 다른 글