Memoization & useCallback

Taesoo Kim·2023년 3월 21일
0

React101

목록 보기
7/8

지난 시간 Virtual DOM을 살펴볼때, 리액트는 실제 DOM과 VDOM을 비교하여 차이가 있는 부분만 수정을 한다고 배웠습니다. 여기서, 더 시간을 절약하는 방법이 있는데, 바로 React.memo()와 useCallback 훅입니다.
리액트도 결국 DOM과 VDOM에 차이가 있는지 알기 위해서 diff 알고리즘을 통해 컴포넌트 하나한 비교해야 합니다. 하지만, 만약, 컴포넌트 props의 변화가 없으면 그냥 지나가면 되지않을까요? 아니면 자주 바뀌지 않는 부분을 캐싱해 사용하면 어떨까요?

React.memo()

바로 이런 방법이 React.memo() 입니다. 아래 예시를 볼게요.

export function Movie({ title, releaseDate }) {
  return (
    <div>
      <div>Movie title: {title}</div>
      <div>Release date: {releaseDate}</div>
    </div>
  );
}
export const MemoizedMovie = React.memo(Movie);

영화의 제목과 출시일을 보여주는 Movie 컴포넌트입니다.

// First render - MemoizedMovie IS INVOKED.
<MemoizedMovie 
  title="Heat" 
  releaseDate="December 15, 1995" 
/>
// Second render - MemoizedMovie IS NOT INVOKED.
<MemoizedMovie
  title="Heat" 
  releaseDate="December 15, 1995" 
/>

이런식으로 두개의 MemoizedMovie를 랜더하고 싶을때 props가 같다면, 리액트는 첫번째 컴포넌트만 랜더하고, 두번째 컴포넌트는 복붙해서 사용합니다.
따라서 memo를 사용하는 경우는, 반복되는 랜더가 보일때 사용하면 좋습니다. 위의 예를 발전시켜서,

// Initial render
<MovieViewsRealtime 
  views={0} 
  title="Forrest Gump" 
  releaseDate="June 23, 1994" 
/>
// After 1 second, views is 10
<MovieViewsRealtime 
  views={10} 
  title="Forrest Gump" 
  releaseDate="June 23, 1994" 
/>
// After 2 seconds, views is 25
<MovieViewsRealtime 
  views={25} 
  title="Forrest Gump" 
  releaseDate="June 23, 1994" 
/>

이렇게 실시간으로 백엔드에서 views를 매초 업데이트한다고 가정하겠습니다. memo를 안쓰게 되면 title과 releaseDate가 고정이어도 views때문에 컴포넌트를 전부 랜더를 해야합니다. 하지만, memo를 사용하여 일부분을 랜더하지 않게 한다면,

function MovieViewsRealtime({ title, releaseDate, views }) {
  return (
    <div>
      <MemoizedMovie title={title} releaseDate={releaseDate} />
      Movie views: {views}
    </div>
  )
}

이런 방법으로 불필요한 랜더를 최대한 막을 수 있게 됩니다.

UseCallback

memoization이 컴포넌트를 캐싱하는 방법이라면, usecallback은 함수를 캐싱하는 방법입니다.

useCallback(function, [dependecies])

dependecies가 바뀌지않는다면, function이 콜 되었을때 캐싱되어있는 함수객체를 반환하여 랜더링 시간을 줄이는 방법입니다. 간단한 예시를 보면서 언제 usecallback이 강점을 갖는지 살펴보겠습니다.

import useSearch from './fetch-items';
function MyBigList({ term, onItemClick }) {
  const items = useSearch(term);
  const map = item => <div onClick={onItemClick}>{item}</div>;
  return <div>{items.map(map)}</div>;
}
export default React.memo(MyBigList);
import { useCallback } from 'react';
export function MyParent({ term }) {
  const onItemClick = useCallback(event => {
    console.log('You clicked ', event.currentTarget);
  }, [term]);
  return (
    <MyBigList
      term={term}
      onItemClick={onItemClick}
    />
  );
}

위의 코드에서는 클릭할 수 있는 div를 items의 수만큼 만들어 냅니다. 이때, 각 컴포넌트에 해당되는 clickHandler가 만들어 질테고, 새로 랜더링 될때마다 모든 함수가 다시 생성되어야 합니다. 그런 경우를 막기위해, 아래 코드처럼 useCallback을 사용하여 모든 함수를 미리 캐싱하고, term에 변화가 있을때만 새로 함수를 생성해 줍니다. 여기서는 그냥 console.log()만 사용되었지만, 더 크고 복잡한 로직이 들어오는 경우에는 효과가 더욱 극대화 됩니다.

Conclusion

지금까지 리액트가 불필요한 랜더를 줄이기 위한 두가지 방법을 살펴보았습니다. memo는 컴포넌트나 벨류를 캐싱하고 싶을때, useCallback은 함수를 캐싱하고 싶을때 사용하면 되겠습니다. 하지만, 간단한 구조에서는 오히려 이런 방법들이 더 복잡하게 만들고 코드를 꼬이게 만들 수 있어서, 사용 타이밍을 잘 보는게 중요할것 같습니다.

Reference

https://dmitripavlutin.com/react-usecallback/
https://dmitripavlutin.com/use-react-memo-wisely/#4-reactmemo-and-callback-functions
https://www.knowledgehut.com/blog/web-development/all-about-react-usecallback
https://www.freecodecamp.org/news/memoization-in-javascript-and-react/

profile
뭔 생각을 해. 그냥 하는 거지 뭐

0개의 댓글