React) useMemo, useCallback, useRef

2ast·2022년 9월 24일
2

React

목록 보기
2/3

Memoization 삼형제

react에서 최적화를 위해 제공하는 hook 중에 대표적인 친구들이 바로 useMemo, useCallback, useRef이다. 이 셋은 쓰이는 곳이 각각 다르지만 최적화를 위해 memoization을 사용한다는 공통점이 있다. 여기서 memoization이란 어떠한 연산의 결과값을 메모리에 저장해두었다가 다음에 똑같은 값이 필요할 때 다시 연산할 필요 없이 저장해두었던 값을 대신 가져오는 것을 의미한다.

useMemo

const memoHooks = useMemo(()=>{return value},[]);

useMemo는 2개의 parameter를 받으며, 각각 callback과 dependency array이다. 최초 마운트 시점에 첫번째 arg인 callback함수를 실행하고 그 결과 값을 변수에 할당한다. 한번 변수에 값이 할당되면 이후에 다시 렌더링되어도 연산을 수행하는 대신 이전에 할당한 값을 그대로 사용한다. 만약 dependency array에 변수를 추가한다면, 해당 변수값이 바뀔 때마다 callback도 함께 실행되어 변수의 값이 바뀌게 된다. 따라서 useMemo는 변수의 value를 구하기 위해 복잡한 연산이 필요하지만 그 값이 매번 바뀌지는 않는 경우 유용하게 사용될 수 있다.

//without useMemo
const TraditionalComponent = () => {
  const [toggle, setToggle] = useState(false);
  
  const complexCalculate = () => {
    for (let i = 0; i < 9999999999; i++) {}
    return true;
  }
  
  const result = complexCalculate()
  
  return (
    <div>
      <button onClick={() => console.log(result)}>run</button>
      <button onClick={() => setToggle(!toggle)}>re-render</button>
    </div>
  );
};
export default TraditionalComponent

--------------------------------------------------------------------------------------

//with useMemo
const TrandyComponent = () => {
  const [toggle, setToggle] = useState(false);
  
  const complexCalculate = () => {
    for (let i = 0; i < 9999999999; i++) {}
    return true;
  }

   const result = useMemo(complexCalculate,[])

  return (
    <div>
      <button onClick={() => console.log(result)}>run</button>
      <button onClick={() => setToggle(!toggle)}>re-render</button>
    </div>
  );
};
export default TrandyComponent

위 두개의 case를 비교해보면 그 차이를 명확하게 확인할 수 있다. useMemo를 사용하지 않은 첫번째 케이스는 result에 값을 할당하기 위해 complexCalculate를 실행하므로 처음 마운트에 오랜 시간이 걸리게 된다. 그 이후 'run'버튼을 클릭해 result를 콘솔에 찍으면 시간이 별도의 소요시간 없이 console에는 true가 찍히는 것을 확인할 수 있다. 그리고 're-render' 버튼을 클릭해 화면의 리렌더링을 유발하면 똑같은 과정을 반복하게 되므로, complexCalculate 함수가 실행되며 로딩에 오랜 시간이 걸리게 된다.
반면 useMemo를 사용한 두번 째 케이스는 조금 다르다. 처음 마운트 시점에 complexCalculate를 실행하기 때문에 오랜 로딩시간이 걸린다는 점과 "run" 버튼을 눌러 콘솔에 result를 찍어보면 소요시간 없이 true가 찍힌다는 점은 동일하지만, 're-render'버튼을 눌러 리렌더링을 유발하면 이전 케이스와는 다르게 즉시 화면이 로딩되는 것을 확인할 수 있다. 그 이유는 처음 마운트 당시에 계산한 결과값을 메모리에 저장하고, 그 이후에 몇번 리렌더링 되어도 함수를 실행하는 대신 그 결과값만을 가져와 반환하기 때문이다.

useCallback

const callbackHooks = useCallback(()=>{},[]);;

useCallback 또한 useMemo와 동일하게 callback과 dependency array를 parameters로 받게 된다. 다만 useMemo가 callback을 실행시켜 그 반환값을 변수에 할당하는 반면, useCallback은 주어진 callback 함수 자체를 할당한다는 차이점이 있다.


const TrandyComponent = () => {
  const [toggle, setToggle] = useState(false);

  const someFn = () => {};

  const functionA = useCallback(someFn, []);
  const functionB = useCallback(someFn, []);
  const functionC = someFn;

  console.log(functionA === functionB);
  console.log(functionA === functionC);

  return (
    <div>
      <button onClick={() => setToggle(!toggle)}> re-render </button>
    </div>
  );
};
export default TrandyComponent;

useCallback을 사용한 케이스와 그렇지 않은 케이스를 비교해보기 위해 간단한 코드를 구현해보았다. 최초에 someFn이라는 함수를 정의했고, useCallback을 사용해 someFn을 할당한 functionA와 functionB, 그리고 useCallback 없이 someFn을 그대로 할당한 functionC를 정의했다. 이 상태에서 functionA === functionB, functionA === functionC를 출력해보면 둘다 true가 나오는 것을 볼 수 있다.

모두 someFn이라는 동일한 함수를 할당한 것이기 때문에 어떻게 보면 당연한 결과라고 할 수 있다. 하지만 여기서 're-render'버튼을 눌러 리렌더링을 하면 결과가 달라진다.

이와같이 functionA와 functionB는 같지만 functionA와 functionC는 다르다는 결과가 나온다. 그 이유는 다음과 같다. 컴포넌트가 리렌더링되면 함수 내부의 코드가 모두 재실행되며, someFn도 함께 재정의된다. 이 과정에서 someFn의 함수가 할당된 메모리 주소가 변경되었고, 덩달아 functionC도 변경된다. 그러나 functionA와 functionB는 useCallback의 영향으로 이전에 할당된 메모리 주소값을 그대로 사용하기 때문에 렌더링 이전과 이후에 차이가 발생하지 않는 것이다.

useRef

const refHooks = useRef();

useRef는 참조값을 할당하는데 특화된 hook이다. react를 사용할 때는 jsx 외부에서 element를 제어하기 위해 reference를 할당하는 과정이 종종 필요한데, ref값은 단순히 element의 정보를 가져올 때만 사용하기 때문에 값이 바뀔필요가 없는데도, 매번 render될 때마다 새롭게 변수에 값이 할당되는 등 비효율적인 문제가 존재했다. 이런 상황에서 useRef를 사용하면 간단히 문제를 해결할 수 있다.

const App = () => {
  const [toggle, setToggle] = useState(false);
  const hookRef = useRef(Math.random());
  const generalRef = Math.random();

  console.log(`hookRef: ${hookRef.current}`);
  console.log(`generalRef: ${generalRef}`);

  return (
    <div>
      <button onClick={() => setToggle(!toggle)}> re-render </button>
    </div>
  );
};
export default App;

useRef로 할당한 변수값이 rendering 되더라도 유지되는지 확인하기 위해 위와같이 코드를 작성해보았다. 실제로 button을 눌러 state를 업데이트한 결과 re-render가 되었는데도 hookRef의 값은 변하지 않는 것을 확인할 수 있다.

profile
React-Native 개발블로그

0개의 댓글