useCallback / useMemo을 과연 잘 사용하고 있는가??

우동이·2022년 9월 15일
0

React

목록 보기
4/5
post-thumbnail

1. useCallback이란?

  • useCallback이란 함수를 메모이제이션을 하기 위해 사용되는 hook입니다.
    • 첫번째 인자는 함수, 두번째 인자는 dependency 배열을 가집니다.
    • 자세한 설명은 생략!( 공식문서 참조 )
  • 보통 React 컴포넌트는 함수 안에 함수가 선언이 되어 있으면 해당 컴포넌트가 렌더링될 때 마다 새로운 함수를 재생성합니다.
  • 하지만 useCallback을 사용하게 되면 해당 컴포넌트가 리랜더링되더라도 그 함수가 의존하는 값이 변경되지 않았다면 함수를 재생성하지 않고 기존 함수를 그대로 반환합니다.
const x = 3;
const y = 7;

const add = useCallback(() => x + y, [x, y]);

2. useCallback의 고찰

  • 개인적으로 평소 프로젝트를 진행하면서 가장 많이 사용하고 있는 hook이라고 생각합니다.

  • 함수형 컴포넌트 내부에서 handler 또는 특정 함수를 생성할 때 useCallback을 사용하면 사용하지 않는 것보다는 성능 이점을 가저올 수 있겠지라고 생각했습니다. ( 손해는 안보겠지?? )

  • 하지만 useCallback에 대한 비용혜택에 관련된 글을 보면서 해당 hook에 대한 생각이 변화게 되었습니다.

3. useCallback을 적용해보자!

  • 상품을 구매하는 간단한 코드 예시입니다.
function App() {
  const initStore = ["book", "phone", "computer"];
  const [products, setProducts] = useState(initStore);

  const buyProduct = (product) => {
    setProducts((products) => products.filter((item) => product !== item));
  };

  return (
    <div>
      <h1>WooDong Shop</h1>
      <div>
        {products.map((product, index) => (
          <div key={index}>
            <button onClick={() => buyProduct(product)}>buy</button>
            <span>{product}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

export default App;
  • buyProduct 함수에 useCallback을 적용한다면 과연 성능이 개선된다고 볼 수 있을까?
// useCallback 미사용
const buyProduct = (product) => {
  setProducts((products) => products.filter((item) => product !== item));
};

// useCallback 사용
const buyProduct = useCallback((product) => {
  setProducts((products) => products.filter((item) => product !== item));
}, []);
  • 정답은 오히려 useCallback을 사용하지 않는 함수가 성능이 더 높을 수 있다고 합니다!

  • 기본적으로 프로그래밍 언어는 코드라인에 있는 코드를 실행하게 되면 비용이 수반됩니다!

  • useCallback의 원래 모습을 한번 보시죠!

    • 결론적으로 내부에서는 더 많은 일을 하고 있습니다.
    • 배열안에 있는 dependency를 참조하고 비교하는 작업도 존재!
    • 즉 컴포넌트가 리렌더링 될때마다 JS는 메모리에 함수를 정의하면서 useCallback의 내부적인 기능이 추가됨에 따라 더 많은 함수가 정의가 되고 결국 메모리를 더 사용하게 됩니다.
const buyProduct = (product) => {
  setProducts((products) => products.filter((item) => product !== item));
};

const handleUseCallBack = useCallback(buyProduct, []);

4. useMemo란?

  • useMemo는 어떤 타입 또는 값들을 모두 메모제이션해주는 hook 입니다.
  • useCallback과 매우 비슷합니다!
const initStore = ["book", "phone", "computer"]

// initStore를 다시 만들기 싫을때 useMemo를 활용
const initStore = useMemo(()=>["book", "phone", "computer"], []);
  • 하지만 위의 방법은 useCallback 처럼 효율적인 방법이 아닙니다!
  • 코드의 복잡성 증가, useCallback 처럼 함수를 호출하면서 다른 함수들의 코드가 메모리에 할당됩니다.
  • 그러면 조금더 효율적으로 변경해볼까? ( 이게 근데 의미가 있을까?... )
    • 차라리 아래처럼 전역으로 선언하고 사용하는 것이 더 효율적일수도?...
const initStore = ["book", "phone", "computer"]

function App(){
	 const [products, setProducts] = useState(initStore);
}

5. useCallback / useMemo는 무조건 사용하는 것이 과연 좋은 것일까?

  • 사실 useCallback / useMemo를 통해 최적화를 진행해도 해당 훅을 사용해서 최적화를 통해 얻어지는 효율은 거의 체감하기 힘든 것 같습니다. ( 요즘 브라우저 / 컴퓨터 사양이 높기 때문에... )

    • 차라리 다른 부분을 개선하는 것이 더 효율적일수도 있습니다!
  • 즉 성능 개선은 공짜로 이루어지는 것이 아니고 항상 그에 해당하는 비용이 더 생기게 됩니다.

  • 성능 개선을 통해 얻어지는 효율성이 그 비용을 상쇄할 수 없다면 효율적으로 볼 수 없다는 의미입니다!

6. 그럼 도대체 useCallback / useMemo을 언제 사용하는게 좋을까?

  • useMemo / useCallback이 만들어진 배경이 중요합니다!! ( 크게 2가지 )

    • 참조의 동일성
    • 비용이 많이 드는 계산

7. 참조의 동일성

  • 자바스크립의 참조 동일성
    • 자바스크립트의 경우 아래코드 예시처럼 참조타입일 경우 같은 value이지만 참조하는 값이 다르기 때문에 다른 value로 인식합니다!
    • React의 함수형 컴포넌트도 내부에 정의된 객체들은 동일한 내용을 가지더라도 같은 참조를 바라보지 않기 때문에 다른 값들로 판단하게 됩니다!
// 원시 타입
1 === 1 // true
'a' === 'a' // true

// 참조 타입
{} === {} // false
[] === [] // false

하나의 코드 예시를 통해 더 자세하게 이해해 봅시다!

  • 아래의 코드는 문제가 있는 코드입니다.
    • useEffect는 컴포넌트가 렌더링 되면 items참조 동일성을 계속 체크합니다.
    • 하지만 이미 컴포넌트 내부에서 itmes라는 객체가 매번 새로 만들어지기 때문에 렌더링 될때마다 계속 호출되게 됩니다.
    • 즉 우리의 의도는 items가 변경될 때 useEffect의 콜백 함수를 실행하고 싶었지만 결국 아무 의미없이 컴포넌트가 리랜더링 될 때마다 실행됩니다!
function Item({ book, pencil }) {
  const items = { book, pencil };
  
  useEffect(() => {
    handleItems(options);
  }, [items]); 
  
  return <div>Item</div>;
}

function Bag() {
  return <Item book="book" pencil="pencil" />;
}

위에 있는 문제를 가지고 있는 코드를 개선한다면?

  • useEffect의 dependency에 props로 전달받은 book / pencil 를 직접 할당하고 useEffect 내부에서 items를 생성하면 됩니다!
function Item({ book, pencil }) {

  useEffect(() => {
    const items = { book, pencil };
    
    handleItems(options);
  }, [book, pencil]); 
  
  return <div>Item</div>;
}

function Bag() {
  return <Item book="book" pencil="pencil" />;
}

만약에 전달받고 있는 Props가 참조타입이면??

function Bag() {
  // 참조 타입
  const book = () => { return 'book' };
  const pencil = ['red', 'blue', 'green'];
  
  return <Item book={book} pencil={pencil} />;
}
  • 위의 경우를 최적화 하기 위해 만들어진 것이 바로 useCallbackuseMemo 입니다!
    • 부모 컴포넌트가 리렌더링 되더라도 참조 동일성을 유지하여 자식 컴포넌트가 같은 값으로 판단할 수 있도록 도와줍니다!
function Item({ book, pencil }) {

  useEffect(() => {
    const items = { book, pencil };
    
    handleItems(options);
  }, [book, pencil]); 
  
  return <div>Item</div>;
}

function Bag() {
  // useCallback / useMemo를 적용
  const book = useCallback(() => { return 'book' }, []);
  const pencil = useMemo(() => ['red', 'blue', 'green'], []);
  
  return <Item book={book} pencil={pencil} />;
}

8. 비용이 많이 드는 계산

  • 비용이 많이 드는 계산이 존재할 경우 useMemo가 만들어진 또 다른 이유이기도 합니다.
  • useMemo를 사용해 연속으로 같은 값을 다시 계산하지 않도록 하여 속도를 더 향상시킬 수 있습니다.
  • 예를 들면 아래의 코드처럼 animation이라는 복잡한 계산을 진행하는 함수가 존재할 경우 start / end 값이 변화지 않으면 그대로 이미 계산된 값을 사용하도록 할 수 있습니다!
    • 하지만 복잡하다의 기준은 무엇일까? ( 효율성??, 시간복잡도?? )
    • 사실 요즘 브라우저 / 컴퓨터 성능이 뛰어나기 때문에 복잡하다라는 기준을 잡기가 애매한 부분이 있는 것 같습니다!
const animation = (start, end) => {...};

function App({start, end}) {
	const value = useMemo(() => animation(start, end), [start, end]);
  
  ...
}

9. 정리하기

  • useCallbackuseMemo를 성능을 향상시킨다는 초점에 맞춰서 무분별하게 사용한다면??
    • 코드가 더 복잡하게 보일수도 있으며 팀원들이 이해하기에 복잡성 또한 증가할 수 있습니다.
    • dependencies 배열에 들어가는 참조 value들이 누락되거나 잘못 넣게되면서 휴먼 에러가 자주 발생합니다.
    • dependencies 배열 내부의 값들이 메모제이션되면서 가비지 컬랙터가 안되게 만들 수 있습니다.
    • 성능 개선을 위해 추가된 비용때문에 오히려 효율성이 떨어져 성능이 더 하락할 수 있습니다.
  • 즉 적절한 상황에서 useCallbackuseMemo를 사용하여 성능 개선을 통해 얻어지는 이점이 추가된 비용을 상쇄할 수 있다면 의미있게 사용할 수 있지 않을까? 생각합니다!

10. 참고 링크

profile
아직 나는 취해있을 수 없다...

0개의 댓글