본문 바로가기

JavaScript/React

React - useMemo vs. useCallback

이 글은 아래 포스트를 보고 작성했습니다.

https://www.daleseo.com/react-hooks-use-memo/

https://www.daleseo.com/react-hooks-use-callback/

 

 

----

React 16.8 버전에 새로 추가된 Hook인 useMemo와 useCallback에 대해 알아보려 합니다.

 

1. useMemo

  • Memoization된 값을 반환하는 Hook입니다.
  • 즉, Re-Render가 발생했을 때 다시 계산할 필요가 없도록 값을 캐싱하는 것으로 생각할 수 있습니다.
Memoization?
프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거해 실행 속도를 개선하는 기법

 

아래와 같이 입력받은 string을 배열에 추가하는 단순한 컴포넌트가 있다고 가정합니다.

단, add 함수는 로직이 복잡해 300ms 이상의 시간적 비용이 발생하는 함수라고 가정해보겠습니다.

/* AddedWord.jsx */

export default function AddedWord({ words }) {
  const add = () => {
    console.log("AddWord");
    delay(300);
    return words;
  };

  const addedWords = add();

  return (
    <>
      <h2>Added Words</h2>
      <ul>
        {addedWords.map((word, idx) => (
          <li key={idx}>{word}</li>
        ))}
      </ul>
    </>
  )
}

function delay(ms) {
  const now = new Date().getTime();
  while( new Date().getTime() < now + ms) {}
}
/* App.js */

import { useState } from 'react';
import AddedWord from './AddedWord';

export default function App() {
  const [ words, setWords ] = useState([]);
  const [ word, setWord ] = useState("");

  const handleClick = () => {
    setWords([...words, word]);
    setWord("");
  };

  return (
    <>
      <div>
        <AddedWord words={words} />
      </div>
      <input
        value={word}
        onChange={({ target: { value } }) => setWord(value)}
      />
      <button onClick={handleClick}>+</button>
    </>
  );
}

 

input에 값을 입력할 때마다 AddWord 컴포넌트에서 Re-Rendering이 발생하고, add 함수가 매번 실행됨에 따라, 동일한 동작임에도 불구하고, 비용이 큰 로직을 불필요하게 반복함으로써 앱의 성능과 사용자 경험을 떨어뜨리고 있습니다.

 

이 때 useMemo를 사용하면, dependency가 변경될 때에만 함수를 실행하도록 설정할 수 있습니다.

 

/* AddWord.jsx */

import { useMemo } from 'react';

export default function AddWord({ words }) {

  const addedWords = useMemo(() => {
    console.log("AddWord");
    delay(300);
    return words;
  }, [ words ]);

  ...
  • useMemo( fucntion, [ dependency ] ) : dependency 값에 변화가 있을 때에만 function을 실행합니다. dependency는 배열로 전달해야 하고, 배열의 값으로 여러개를 의존하도록 설정할 수 있습니다.

 

이제 words의 값이 변경될 때만 함수를 실행하기 때문에, input에 값을 입력할 때는 함수가 실행되지 않아서 앱의 성능을 개선할 수 있었습니다.

 

2. useCallback

  • Memozation된 콜백 함수를 반환하는 Hook입니다.
  • 기본적으로 useMemo와 동작은 비슷하지만, useMemo는 값을 반환하고 useCallback은 함수를 반환합니다.

 

단순히 함수를 재활용하기 위해서 useCallback을 사용하면 성능에 큰 차이가 없을 뿐더러, 가독성이 떨어지고, 유지보수성이 낮아져 최적화와는 다소 멀어질 수 있는데요,

 

useCallback은 함수 참조의 동일성에 의존적인 자식 컴포넌트에 함수를 전달할 때 유용하게 사용할 수 있습니다.

함수 참조의 동일성?
JavaScript에서 함수의 동등관계를 비교해 봄으로써 알 수 있다.

----

function A( ) { }
function B( ) { }
console.log( A === B ); // false

 

아래와 같이 침실, 주방, 욕실의 버튼을 클릭하면 아이콘이 바뀌는 앱이 있다고 가정합니다.

/* Light.jsx */

import React from 'react';

function Light({ room, on, toggle }) {
  console.log(room, on);
  return (
    <button onClick={toggle}>
      {room} {on ? "💡" : "⬛"}
    </button>
  )
}

export default React.memo(Light);
  • React.memo : 상위 컴포넌트에서 전달된 props가 변경되지 않는 한 컴포넌트를 Re-Reder 하지 않는 React의 고차 컴포넌트(HOC)입니다.
/* App.js */

import { useState } from 'react';
import Light from './Light.jsx';

export default function App() {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);

  const toggleMaster = () => setMasterOn(!masterOn);
  const toggleKitchen = () => setKitchenOn(!kitchenOn);
  const toggleBath = () => setBathOn(!bathOn);

  return (
    <>
      <Light room="침실" on={masterOn} toggle={toggleMaster} />      
      <Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
      <Light room="욕실" on={bathOn} toggle={toggleBath} />
    </>
  );
}

 

실행해서 버튼을 눌러보면, 하나의 버튼만 눌러도 모든 컴포넌트가 Re-Rendering 되는 걸 콘솔로 확인할 수 있습니다.  분명 React.memo로 감싸서 export를 했는데 왜 다른 컴포넌트들까지 함께 Re-Rendering 되는 걸까요?

 

이는 하위 컴포넌트로 전달하는 props 중 함수의 참조가 변경되었기 때문입니다.

 

즉 App.js 내 state가 변경되면 App.js가 Re-Render 되고, toggle 함수들이 새로 만들어져서 하위 컴포넌트로 전달됩니다. 하위 컴포넌트로 전달된 함수는 새로 만들어진 함수들이고, 참조가 동일하지 않아서 props가 변경되었다고 판단하고 Re-Render를 한 것이죠.

 

이 때 useCallback을 사용하면, dependency가 변경되지 않는 한 함수가 새로 만들어지지 않기 때문에 참조 동일성을 유지할 수 있습니다.

/* App.js */

import { useState, useCallback } from 'react';
import Light from './Light.jsx';

export default function App() {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);

  const toggleMaster = useCallback(() => setMasterOn(!masterOn), [masterOn]);
  const toggleKitchen = useCallback(() => setKitchenOn(!kitchenOn), [kitchenOn]);
  const toggleBath = useCallback(() => setBathOn(!bathOn), [bathOn]);

  return (
    <>
      <Light room="침실" on={masterOn} toggle={toggleMaster} />      
      <Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
      <Light room="욕실" on={bathOn} toggle={toggleBath} />
    </>
  );
}

 

이제 버튼을 눌러도 다른 컴포넌트가 Re-Rendering 되지 않습니다.

 

3. 결론

함수 컴포넌트를 사용한 React 앱의 최적화를 위해 사용되는 useMemo와 useCallback을 간략히 알아보았습니다.

useMemo의 예는 극단적인 경우이기도 하고, 요즘은 컴퓨터와 브라우저의 성능이 좋기 때문에 성능 최적화의 큰 효과를 기대하기 어려울 뿐더러, 컴포넌트의 복잡성을 높이고 유지보수성을 떨어뜨릴 가능성이 크기 때문에 무분별하게 사용하는 경우가 없도록 주의해야 할 것 같습니다.

 

 

 

 

 

'JavaScript > React' 카테고리의 다른 글

React.FC를 사용하지 말아야 하는 이유  (0) 2022.07.26
React - 배포하기  (0) 2022.02.23
React - Hooks, Context  (0) 2022.02.08
React - ControlledComponent, UnControlledComponent  (0) 2022.02.07
React - Component Styling  (0) 2022.01.28