보통 공부를 하다보면 많이 접하는 useCallback
과 useMemo
는 많이들 이렇게 알고있다.
useCallback
: 메모이제이션된 함수를 반환useMemo
: 메모이제이션된 값를 반환
설명만 보면 리랜더링 될때 함수 또는 값을 재생성하지 않고 메모이제이션된 값을 사용하는 GOAT hook이다. 여기서 리랜더링이라하면
- props가 변경되었을 때
- state가 변경되었을 때
- 부모 컴포넌트가 렌더링되었을 때
- forceUpdate() 를 실행하였을 때
의 조건으로 리액트는 리랜더링을 하고있다. 처음에 위의 hook을 보게된다면 이런생각을 할 수도 있다.
모든 함수나 값에 해당 hook을 사용해서 메모이제이션을 하면 최적화에 아주 도움이 되겠군!!!
이러한 생각을 했다면 다시한번 생각해보자. 메모이제이션을 하기위해 비싼 비용을 들이고 해당 값을 캐싱하게 된다. 추후 의존성 배열안에 있는 값이 바뀔때마다 다시 캐싱을하기위해 비싼 비용들 들이게된다(리랜더시 비교할때도 비용이 든다).
여기서 함수 또는 값을 두 hook을 사용해서 캐싱한다면 비용이 들더라도 캐싱되긴하지만 함수나 값만을 다시 재생성하는데에는 사실 비용이 크지 않아 큰 효과를 볼 수 없다(때로는 오히려 손해일 경우도 있다) 그렇다면 언제 어떻게 사용해야 유의미한 이득을 볼 수 있을까?
바로 useMemo에 넣는 계산이 눈에 띄게 느리고 의존성이 거의 변하지 않는 경우와 해당 함수나 값을 전달하려는 자식컴포넌트(자식 컴포넌트는 React.memo기능을 사용하여 메모이제이션을 해서 랜더최적화 한경우)의 리랜더링을 막기위해 사용할때 유의미한 효과를 볼 수 있다. (공식문서에서는 이득이 없다 라고 표현되어있다)
여기서 React.memo란
- props가 이전과 동일한 값이면 재렌더링하지 않고, 다른 값이면 재렌더링하여 컴포넌트를 다시 만들어 반환한다.
- React.memo에 쓰인 컴포넌트 안에서 구현한 state가 변경되면 컴포넌트는 재렌더링이 된다.
의 기능을 가지고있다. 물론 React.memo도 만능같고 GOAT같지만 만약 프로퍼티가 계속 변경되는 컴포넌트를 React.memo로 해봤자 괜히 프로퍼티를 비교하는 과정만 추가되지 실속은 없다. 그리고 메모이제이션을 위해 계속 기록을 해야 하므로 매번 만들어진 컴포넌트들을 기록하는 공간만 조금씩 더 차지하게 될 것이다.
그렇다면 여기서 props에 useCallback 을 사용하지않고 함수를 전달한다고 생각해보자.
// 리액트 공식문서
// ShippingForm은 React.memo로 감싸져있다고 가정한다.
function ProductPage({ productId, referrer, theme }) {
// 테마가 변경될 때마다, 이 함수는 달라집니다...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
여기서 React.memo는 props의 변경을 확인하기위해 얕은비교를 하게된다. 여기서 얕은 비교란 실제 데이터가 같은지 확인하는게 아닌, 참조(또는 주소값)를 통해서 같은 값인지를 판단한다. 리액트 공식문서에서는 이렇게 나와있다.
JavaScript에서 function () {} 또는 () => {}는 객체 리터럴이 항상 새 객체를 생성하는 것과 유사하게 항상 다른 함수를 생성합니다.
일반적으로는 문제가 되지 않지만 ShippingForm 의 props는 결코 동일하지 않으며 memo 최적화가 작동하지 않는다는 의미입니다. 바로 이 지점에서 useCallback이 유용합니다
부모 컴포넌트가 리랜더링이 일어났다고 생각해볼때 handleSubmit이라는 함수 또한 재생성되게 된다. 이때 해당 함수는 참조값(또는 주소값)이 변경되게되어 얕은비교시에 다르다고 판단이되어 제대로 동작하지 않게 된다. 이럴때 공식문서에 써있는 것 처럼 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>
);
}
예제와 같이 handleSubmit을 useCallback으로 감싸주고 productId와 referrer을 의존성 배열에 넣어줬다. 의존성 배열에 있는 값이 변경되지않는한 handleSubmit은 리랜더링이 일어나도 기존에 캐싱되엇던 함수를 사용하게되고 동일한 참조(또는 주소값)를 가지고 있기 때문에 ShippingForm 컴포넌트는 정상적으로 React.memo동작을 하게된다.
끝으로 최적화지 막 쓴다고 최적화가 되는 것은 아니다. 다른 계산 과정이 끼어들어가는 것이고, 기록을 위한 메모리도 잡아야 한다. 최적화는 공짜로 이뤄질 수가 없다. 그래도 그 중에서 어떻게 효율적으로 이 최적화 기능들을 사용할 수 있을지 고민하고 잘 사용해야 할 것이다.
'React' 카테고리의 다른 글
Reactjs code snippets을 사용해서 자동 템플릿을 설정해보자 (0) | 2023.06.11 |
---|---|
[React] Context API란? (0) | 2022.01.31 |
댓글