React Hooks - useMemo()

RumbleBi·2022년 7월 14일
0

React

목록 보기
4/9
post-thumbnail

useMemo() 란?

useState() 의 방식으로 작성한 컴포넌트에서는 값이 바뀌게 된다면 매번 렌더링이 일어나기 때문에 컴포넌트 최적화에 부족한 부분이 있다. 하지만 useMemo(), useCallback()을 사용하여 컴포넌트 최적화를 가능하게 한다. 여기서는 우선 useMemo()에 대해 알아보자.

useMemo의 Memo는 Memoization 을 의미한다. Memoization이란 동일한 값을 리턴하면 함수를 반복적으로 호출해야 된다면 맨 처음 값을 계산할 때 해당 값을 메모리에 저장해서 필요할 때마다 다시 호출하지 않고 메모리에서 꺼내 재사용하는 기법이다. 그러므로 불필요한 리렌더링이 일어나지 않고 성능을 최적화 시킬 수 있는 것이다.

함수형 컴포넌트 라는 것은 말 그대로 함수로 이루어져 있고 함수형 컴포넌트가 렌더링이 된다는 것은 함수가 호출된다는 것이다. 매번 함수가 호출 될 때, 모든 내부 변수들은 초기화 된다는 점을 인지하자.

useMemo() 의 구조

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

useMemo는 두 개의 인자를 받는다. 첫 번째 인자로는 callback 함수, 두 번째 인자로는 의존성 배열을 받는다.

첫 번째 인자인 callback 함수는 우리가 Memoization 해줄 값을 계산해서 리턴해 주는 함수다. 결국 callback 함수가 return 하는 값이 바로 useMemo가 리턴하는 값이다.

두 번째 인자인 의존성 배열은 배열 안의 값이 업데이트 될 때만 콜백함수를 호출하여 Memoization 된 값을 다시 업데이트 해준다. 만약 빈 배열이라면 맨 처음 컴포넌트가 마운트 되었을 때만 실행된다.

useMemo() 주의점

useMemo는 무분별하게 사용하면 오히려 성능저하의 원인이 될 수 있다. 결국 useMemo를 사용한다는 것은 값을 재활용하기 위해서 따로 메모리에 저장을 해 놓는다는 것이다. 그렇기 때문에 불필요한 값들 까지 모두 useMemo를 사용해 버리면 오히려 성능이 떨어질 수 있다는 것이다.

useMemo() 예제 1

import React, { useState } from "react";

const hardCalculate = (n) => {
  console.log("hard");
  for (let i = 0; i < 999999999; i++) {} // 복잡한 로직

  return n + 10000;
};

function App() {
  const [number, setNumber] = useState(1);
  const sum = hardCalculate(number);

  const onChangeNumber = (e) => {
    setNumber(parseInt(e.target.value));
  };

  return (
    <>
      <h3>복잡한 로직</h3>
      <input type="number" value={number} onChange={onChangeNumber} />
      <span> + 10000 = {sum}</span>
    </>
  );
}

export default App;

위의 컴포넌트를 실행하고 값을 올리게 된다면 매번 콘솔에 리렌더링이 찍히게 될 것이다. 또한 for문의 반복횟수가 많기 때문에 값을 올려도 바로 반영되지 않는 것을 알 수 있다. 왜나하면 함수가 실행 될 때마다 초기화되는 sum 의 값을 받아오기 위해서는 hardCalculate() 의 실행이 완료되어야지 받을 수 있기 때문이다.

그렇다면 이건 어떻게 될까?

import React, { useState } from "react";

const hardCalculate = (n) => {
  console.log("hard");
  for (let i = 0; i < 999999999; i++) {} // 시간이 걸리는 로직
  return n + 10000;
};
const easyCalculate = (n) => {
  console.log("easy");
  return n + 10000;
};

function App() {
  const [number, setNumber] = useState(1);
  const [number2, setNumber2] = useState(1);

  const sum = hardCalculate(number);
  const sum2 = easyCalculate(number2);

  const onChangeNumber = (e) => {
    setNumber(parseInt(e.target.value));
  };

  const onChangeNumber2 = (e) => {
    setNumber2(parseInt(e.target.value));
  };

  return (
    <>
      <h3>복잡한 로직</h3>
      <input type="number" value={number} onChange={onChangeNumber} />
      <span> + 10000 = {sum}</span>
      <h3>간단한 로직</h3>
      <input type="number" value={number2} onChange={onChangeNumber2} />
      <span> + 10000 = {sum2}</span>
    </>
  );
}

export default App;

밑에 추가로 간단한 로직을 추가하여 하나 만들어보았다. 간단하니 금방 값이 변화할까?

확인해보면 전혀 그렇지 않고 똑같이 느리게 값이 바뀐다. 그 이유는 App 컴포넌트가 함수형 컴포넌트이기 때문이다. 위에서 설명했듯이 함수가 호출되면 값은 초기화가 되기 때문에 똑같이 느려지게 된 것이다. 그렇다면 우리는 sum2 의 state를 변경할 때는 sum 이 불리지 않게 하는 방법이 없을까 고민하게 된다. 즉 여기서 useMemo의 효과를 볼 수 있다.

import React, { useState, useMemo } from "react";

const hardCalculate = (n) => {
  console.log("hard");
  for (let i = 0; i < 999999999; i++) {} // 시간이 걸리는 로직
  return n + 10000;
};
const easyCalculate = (n) => {
  console.log("easy");
  return n + 10000;
};

function App() {
  const [number, setNumber] = useState(1);
  const [number2, setNumber2] = useState(1);

  // const sum = hardCalculate(number);
  const sum = useMemo(() => {
    return hardCalculate(number);
  }, [number]);

  const sum2 = easyCalculate(number2);

  const onChangeNumber = (e) => {
    setNumber(parseInt(e.target.value));
  };

  const onChangeNumber2 = (e) => {
    setNumber2(parseInt(e.target.value));
  };

  return (
    <>
      <h3>복잡한 로직</h3>
      <input type="number" value={number} onChange={onChangeNumber} />
      <span> + 10000 = {sum}</span>
      <h3>간단한 로직</h3>
      <input type="number" value={number2} onChange={onChangeNumber2} />
      <span> + 10000 = {sum2}</span>
    </>
  );
}

export default App;

이렇게 useMemo를 사용하게 되면 sum의 값만 변경되었을 때 조금 느려지게 되고 sum2 의 값은 값이 빠르게 변화되는 것을 알 수 있다.

hardCalculate(number) 의 콜백함수와 의존성 배열 안에 number가 있으며 number의 값이 바뀔 때 까지 메모리에 처음 실행했던 값을 그대로 저장한 상태로 있는 것이다.

useMemo() 예제 2

import React, { useState, useEffect } from "react";

function App() {
  const [number, setNumber] = useState(0);
  const [isKorea, setIsKorea] = useState(true);

  const location = isKorea ? "한국" : "미국";

  useEffect(() => {
    console.log("useEffect 호출");
  }, [location]);
  const onChangeNumber = (e) => {
    setNumber(e.target.value);
  };

  const onClickEscape = () => {
    setIsKorea(!isKorea);
  };
  return (
    <>
      <h3>하루에 몇끼 먹나요?</h3>
      <input type="number" value={number} onChange={onChangeNumber} />
      <hr />
      <h3>어느 나라에 있나요?</h3>
      <p>나라: {location}</p>
      <button onClick={onClickEscape}>미국가기</button>
    </>
  );
}
export default App;

이 예제는 useEffect의 의존성 배열이 원시타입이 아닌 객체라면 어떻게 될까?

// const location = isKorea ? "한국" : "미국";
  const location = {
    country: isKorea ? "한국" : "미국",
  };

원시타입을 객체형식으로 바꾸면 벌써 React에서 경고가 날라온다. 매번 렌더링이 된다는 경고가. 몇 끼 먹는지를 변경해도 똑같이 콘솔창에 useEffect가 호출된다. 즉 그렇다면 useEffect를 사용하는 의미가 없어지게 되는 것이다. 아니 그럼 왜 상관없는 몇 끼의 값이 변경되었는데 useEffect가 계속 호출되는 것일까?

원시타입 vs 참조타입

간단히 설명하자면 String, Number, Boolean, Null, Undefined, BigInt, Symbol 은 원시타입이며 참조 타입은 Object, Array .. 같은 것들이다.

원시타입의 변수들은 데이터 복사가 일어날 때 메모리 공간을 새로 확보하여 독립적인 값을 저장하지만,

참조 타입은 메모리에 직접 접근이 아닌 메모리의 주소에 대한 간접적인 참조를 통해 메모리에 접근하는 데이터 타입이다.

const one = '1'
const two = '1'

one === two // true

const A = {
  country: 'korea'
}
const B = {
  country: 'korea'
}

A === B // false

원시타입의 변수는 이미 메모리 안에서 저장된 값이 있으며 이것은 불변성을 띄고, 같은 메모리주소를 참조하고 있기 때문에 true 가 나오는 것이며, 두 개의 객체에는 다른 주소에 저장되어 있기 때문이다. 당연히 다른 주소를 서로 비교하기 때문에 false가 나오게 되는 것이다.

그렇다면 위의 몇 끼 먹는 state를 변화시킨다면 App 컴포넌트는 다시 렌더링이 될 것이고 location 객체는 매번 새로운 메모리 주소로 할당이 다시 될 것이다. 그러므로 react에서는 새로운 객체가 생성되었다고 여겨지고, 또 다시 {location.country} 부분은 새로운 객체의 메모리를 참조하게 되는 것이다.

이 문제를 해결하기 위해 location 객체에 useMemo를 사용한다면 해결할 수 있다.

import React, { useState, useEffect, useMemo } from "react";

function App() {
  const [number, setNumber] = useState(0);
  const [isKorea, setIsKorea] = useState(true);

  const location = useMemo(() => {
    return {
      country: isKorea ? "한국" : "미국",
    };
  }, [isKorea]);
  // const location = isKorea ? "한국" : "미국";
  // const location = {
  //   country: isKorea ? "한국" : "미국",
  // };

  useEffect(() => {
    console.log("useEffect 호출");
  }, [location]);

  const onChangeNumber = (e) => {
    setNumber(e.target.value);
  };

  const onClickEscape = () => {
    setIsKorea(!isKorea);
  };
  return (
    <>
      <h3>하루에 몇끼 먹나요?</h3>
      <input type="number" value={number} onChange={onChangeNumber} />
      <hr />
      <h3>어느 나라에 있나요?</h3>
      <p>나라: {location.country}</p>
      <button onClick={onClickEscape}>미국가기</button>
    </>
  );
}
export default App;

이러한 방식으로 효과적으로 컴포넌트를 최적화 시킬 수 있게 되는 것이다.

profile
기억보다는 기록하는 개발자

0개의 댓글