웹개발/React

useMemo란? useMemo의 개념 및 실습

harusari 2023. 6. 22. 20:18

useMemo 에 대하여

React hook중 최적화(optimization)를 위한 훅 두 가지가 있음

  • useMemo
  • useCallback

useMemo란?

  • useMemo의 memo = memoization

메모이제이션(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. (출처 : 위키백과)

  • 동일한 값 리턴하는 함수를 반복적으로 호출해야 한다면, 맨 처음 값을 계산할 때 해당 값을 메모리에 저장해 필요할 때마다 다시 계산하지 않고 메모리에서 꺼내서 재사용하는 기법
  • 즉, 자주 필요한 값을 맨 처음 계산 시 캐싱해두어 값이 필요할 때 다시 계산하지 않고 캐시에서 꺼내 사용

함수형 컴포넌트에서 왜 useMemo가 필요한가?

  • 함수형 컴포넌트도 함수임
  • 함수형 컴포넌트가 렌더링된다면 함수가 호출된다는 것
  • 함수는 호출될 때마다 함수 내부에 있는 모든 변수가 초기화됨
function Component() {
  const value = calculate();

  return <div>{value}</div>;
}

function calculate() {
  return 1;
}
  • 현재 컴포넌트 안 value라는 변수 → calculate라는 함수로부터 값을 얻어오고 있음
  • 대부분 리액트 컴포넌트는 state와 props의 변화로 수많은 렌더링을 거침
    • 컴포넌트가 렌더링 될때마다 value 변수가 초기화되므로, calculate함수는 반복적으로 호출
    • 만약 calculate함수가 무거운 함수라면 굉장히 비효율적일 것
    • calculate함수는 무의미한 계산을 반복해 value라는 변수에 같은 값을 반복적으로 할당하기 때문
  • useMemo를 사용하면 이런 상황을 간편히 해결할 수 있음
  • useMemo는 처음 계산된 결과값을 메모리에 저장하여 컴포넌트가 반복적 렌더링되어도 계속 함수를 다시 호출하지 않고 이전에 계산된 결과값을 메모리에서 꺼내와 재사용할 수 있게 해줌

useMemo의 구조

//1. dependency에 빈 배열 전달
const value = useMemo(() => {
	return function();
}, [])

//2. dependency에 요소 전달
const value = useMemo(() => {
	return function();
}, [item])
  • 두 개의 인자를 받음
    • 첫 번째 인자 : 콜백함수
    • 두 번째 인자 : dependency 배열
  • 첫 번째 인자인 콜백함수는 메모이제이션해줄 값을 계산해 return해줄 함수
    • 콜백함수가 리턴하는 값이 useMemo가 리턴하는 값
  • 두 번째 인자인 배열은 의존성 배열이라고 불림
    • useMemo는 배열 안의 요소값이 업데이트될 때만 콜백함수를 호출해 메모이제이션해둔 값을 업데이트해 다시 메모이제이션해줌
    • 만약 빈 배열을 넘겨주면 맨 처음 컴포넌트가 마운트되었을 때만 값을 계산하고, 이후에는 항상 메모이제이션된 값을 꺼내와 사용

useMemo 사용 시 주의점

  • useMemo는 값을 재활용하기 위해 따로 메모리를 소비해 저장한다는 의미
  • 즉 불필요한 값까지 메모이제이션하면 성능이 악화됨
  • 따라서, 필요시만 적절히 사용하는 게 중요함

useMemo의 사용 1) : 계산기

import { ChangeEvent, useState } from "react";

const hardCalculator = (n: number) => {
  console.log("어려운 계산 🥵");
  for (let i = 0; i < 999999999; i++) {}
  return n + 10000;
};

export const UseMemo1: React.FC = () => {
  const [hardNumber, setHardNumber] = useState<number>(0);

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    setHardNumber(Number(event.target.value));
  };

  return (
    <div>
      <h1>짱 어려운 계산기</h1>
      <input value={hardNumber} type="number" onChange={handleChange} />
      <span>+10000 = {hardCalculator(hardNumber)}</span>
    </div>
  );
};
  • 어떤 숫자에 10000을 더한 값을 화면에 출력해주는 계산기
  • hardCalculate()라는 함수가 있음
    • 어떤 숫자를 받고 숫자에 10000을 더한 값을 리턴해주는 함수
    • 이는 어려운 계산이기 때문에 의미없는 for루프를 많이 돌려 오래 걸리게 만듦 → delay 만듦
  • hardNumber라는 state를 생성함
  • 컴포넌트 내부에는 number타입인 input태그, value로는 hardNumber
    • 화면에 있는 숫자를 증가시키거나 감소시킬 때마다 onChange안의 setHardNumber 함수가 불려 hardNumber state를 업데이트 시켜줌
  • 아래의 span태그는 hardSum값을 화면에 출력해 줌
    • 이는 컴포넌트 안에 정의된 변수
    • hardCalculate함수가 호출되면 이 함수가 리턴하는 값으로 할당이 됨

[렌더링시]

  • 컴포넌트는 함수형 컴포넌트
    • 컴포넌트가 렌더링된다는 것은 이 함수가 호출이 된다는 것
    • 함수가 호출되면 함수 내부의 변수는 초기화됨
    • 즉, 이 UseMemo1 컴포넌트가 반복해 렌더링된다면 이 안에 있는 hardSum 변수도 계속 초기화됨
    • 이는 hardCalculate 함수가 반복적으로 불려서 이 hardSum 변수에 반복적으로 값을 할당해준다는 이야기

[상태 업데이트시]

  • 브라우저에 있는 숫자 바꾸면 hardSum 스테이트가 업데이트되어 우리의 컴포넌트는 다시 렌더링됨
    • 렌더링될 때마다 hardCalculate함수가 호출되는지 확인해보자
import { ChangeEvent, useState } from "react";

// delay 만들기 위해 CPU 많이 사용하는 함수 생성
const hardCalculator = (n: number) => {
  console.log("어려운 계산 🥵");
  for (let i = 0; i < 999999999; i++) {}
  return n + 10000;
};

const easyCalculator = (n: number) => {
  console.log("쉬운 계산 😄");
  return n + 1;
};

export const UseMemo1: React.FC = () => {
  const [hardNumber, setHardNumber] = useState<number>(0);
  const [easyNumber, setEasyNumber] = useState<number>(0);

  const handleHDChange = (event: ChangeEvent<HTMLInputElement>) => {
    setHardNumber(Number(event.target.value));
  };

  const handleEZChange = (event: ChangeEvent<HTMLInputElement>) => {
    setEasyNumber(Number(event.target.value));
  };

  return (
    <div>
      <div>
        <h1>짱 어려운 계산기</h1>
        <input value={hardNumber} type="number" onChange={handleHDChange} />
        <span>+10000 = {hardCalculator(hardNumber)}</span>
      </div>
      <div>
        <h1>짱 쉬운 계산기</h1>
        <input value={easyNumber} type="number" onChange={handleEZChange} />
        <span>+1 = {easyCalculator(hardNumber)}</span>
      </div>
    </div>
  );
};
  • 이번에는 쉬운 계산기를 만들어보기
    • easyNumber라는 state 하나 더 만들기
  • 이번에는 함수를 easyCalculate라고 하고 의미없는 for 루프를 지우고 인자로 받은 넘버에다가 1을 더해주는 함수로 만들어봄
  • easySum 변수를 만들어 easyCalculate 함수로 값을 할당받고, 인자로 easyNumber 스테이트를 전달
  • 어라? 근데 예상으로는 콘솔에 바로 찍힐 것 같았는데 hardCalculate처럼 1초 딜레이 이후에 콘솔에 찍히게 됨

이유 : UseMemo1 컴포넌트가 함수형 컴포넌트라는데에 있음

  • 쉬운 계산기의 숫자를 증가시켜주면 easyNumber 스테이트가 바뀜
    • 이는 곧 UseMemo1 컴포넌트가 다시 렌더링된다는 것
    • UseMemo1 이라는 함수 안에 정의되어있는 하드썸과 이지섬 두 개의 변수 모두 초기화가 되어버림
    • 그래서 hardC..함수도 불리기 때문에, 하드넘버를 바꾸건 이지넘버를 바꾸건 hardCalculator 함수 안에 있는 의미없는 for루프가 이만큼 돌아가게됨
    • 너무 비효율적
  • 그럼 이지넘버 스테이트 변경할때는 하드칼큘 함수가 불리지 않게 하는 방법은 없을까?
    • 있다. useMemo를 사용하면 됨
    • 이를 사용하면 어떤 조건이 만족되었을 때만 변수가 초기화되게 할 수 있음
  • 만약 조건을 만족시키지 않았다면 앱 컴포넌트가 다시 렌더링되더라도 다시 초기화시키는 게 아니라 이전에 가지고 있던 값을 그대로 사용하게 해줌
  • 이걸 다른말로 메모이제이션이라고 부름
const hardSum = useMemo<number>(() => {
    return hardCalculator(hardNumber);
  }, [hardNumber]);
  • hardSum이라는 변수를 생성해 useMemo를 불러옴
    • 첫 번째 인자인 콜백함수에 return값으로 메모할 값을 주면 됨
    • 이 값은 hardCalculate함수를 호출해 리턴되는 값
    • return안에 붙여넣음
    • 두 번째 인자는 의존성 배열인데, 이 안에 값을 넣어주면 값이 바뀔때만 콜백 안의 하드칼큘함수를 다시 호출해 hardSum에 할당해 줌
    • 즉 [] 안이 조건이고 조건을 만족시켜야만 초기화되게 만들어주는 것
    • 여기에 hardNumber를 집어넣으면, 이 hardN이 변경되어야지만 hardC가 다시 불려 hardS을 초기화
    • 만약 hardN가 변경되지 않았다면 그 전에 가지고 있던 hardSum의 값을 재사용하게 됨
  • 저장해서 다시 확인해보면 어려운 계산기를 증가해보면 1초 딜레이가 있음
    • 콘솔을 봐도 어려운 계산과 짱 쉬운 계산 → 두 함수가 모두 불림
  • 쉬운 값을 변경해주면 클릭때마다 즉각적으로 delay없이 화면에 보여짐
    • hardC 함수는 실행되지 않고, 오직 easyC 함수만 실행됨
  • 왜냐하면, useMemo를 사용했으므로 hardN이 바뀌었을 때만 hardC를 부르기 때문에 쉬운 계산기를 증가시킬 때에는 hardN state에는 변화가 없어, 이미 메모된 값을 재사용하는 것
  • 하지만 사실 리액트로 개발하며 1초 이상 걸리는 함수로 컴포넌트 내부 변수를 초기화줄 일은 그렇게 많지 않음
  • useMemo가 빛을 발하는 상황은 따로 있음 ⇒ 아래 예시

예제 2 - useMemo의 진짜 사용 예시

import { ChangeEvent, useState } from "react";

export const UseMemo2: React.FC = () => {
  const [number, setNumber] = useState<number>(0);
  const [isKorea, setIsKorea] = useState<boolean>(true);

  const handleNumberChange = (event: ChangeEvent<HTMLInputElement>) => {
    setNumber(Number(event.target.value));
  };

  const toggleCountry = () => {
    setIsKorea((prev) => !prev);
  };

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

  return (
    <div>
      <h1>하루에 몇 끼나 먹나요? 🐷</h1>
      <input type="number" value={number} onChange={handleNumberChange} />
      <h1>어느 나라에 있나요? : {location}</h1>
      <button onClick={toggleCountry}>비행기 타자! ✈️</button>
    </div>
  );
};
  • 두 가지의 화면
    • 하루에 몇끼? 숫자 입력 화면
    • 어느 나라에 잇어요? 지금 나라 한국이고 비행기타자 버튼 누르면 외국 한국 이렇게 토글링되는 버튼이 있음
  • 현재 컴포넌트의 구조는 두 가지 스테이트
    • 첫 번째 스테이트는 number → 0으로 초기화되어있음
      • 이는 input의 value로 쓰임
    • 두 번째 스테이트는 isKorea 스테이트 → boolean값, 초기화는 true
      • 밑의 비행기타자 버튼 누르면 isKorea가 true-false 토글링
  • 컴포넌트 내부 변수는 location 변수
    • 만약 isKorea가 true라면 한국이라는 String을, false라면 외국이라는 String을 할당받음
useEffect(() => {
    console.log("useEffect 실행!");
  }, [location]);
  • 이제 useEffect를 추가해 봄
    • dependency에는 location을 전달
    • 맨 처음 화면에 렌더링될때, location이 바뀌었을 때 실행
    • 콜백함수에는 console.log로 ‘useEffect 호출’ 메시지
  • 현재 number 스테이트 업데이트해주면 리렌더링이 되지 않음
    • 아무리 렌더링 많이 되어도 location 값은 계속 초기화되긴 하지만, number 스테이트만 바꿔줘서 location에 들어있는 값은 변하지 않음
    • useEffect가 불릴지 말리 판단할 때는 location의 값이 렌더링 이전과 이후에 차이가 있는지 확인
  • 그래서 location의 값이 바뀌었을 때만 useEffect 호출
  • 그래서 비행기타자를 클릭하면 location값이 외국으로 바뀌었으니까 useEffect가 실행

하지만, 만약 dependency로 전달해준 location이 String같은 원시타입이 아니라 object라면 이야기가 달라짐

const location = {
    country: isKorea ? "한국" : "외국",
  };
  • 이제 location에 object 할당해보자
    • country라는 key에 isKorea가 true라면 한국, false라면 외국
    • p태그 안에는 location.country
  • 비행기타자 버튼 누르면 아까와 동일하게 useEffect 호출
  • 하루에 몇끼 먹어요 증가시켜도 똑같이 useEffect 호출됨
  • location은 변경되지 않고 number만 변경되었는데 대체 useEffect 왜 불림?

자바스크립트 타입

  • 변수는 어떤 값을 넣어둘 수 있는 상자
  • 어떤 변수에 원시타입의 값을 할당하면 그 값은 상자에 바로 들어감
  • 하지만 어떤 변수에 객체타입을 할당하면 객체는 너무 크기 때문에 변수라는 상자에 넣어지는 게 아닌, 어떤 메모리상의 공간에 할당되어 메모리 안에 보관
  • 변수 안에는 객체가 담긴 메모리 주소가 할당

  • input의 수를 증가시키면 number의 state가 바뀌어 UseMemo2라는 함수가 다시 호출
  • 그럼 location이라는 변수도 object를 또 할당받음
    • 이는 사실 다른 object → 이전 object랑은 다른 메모리상 공간에 저장
  • location 변수는 또 생성된 object의 주소를 참조
  • 그래서 react 관점에서는 location 변수의 주소가 바뀌었기 때문에 location이 변경되었다고 인식 (참조하는 주소가 바뀌었으므로)
  • 해결하려면, 컴포넌트가 렌더링되었을 때 location 변수가 초기화되는 걸 막아주면 됨
  • 이 location 변수를 isKorea 변수가 바뀌었을때만 초기화되게 해보자
  • useMemo를 사용해 location을 메모이제이션 해줌
const location = useMemo(() => {
    return {
      country: isKorea ? "한국" : "외국",
    };
  }, [isKorea]);

  // const location = {
  //   country: isKorea ? "한국" : "외국",
  // };
  • location에 useMemo를 추가
  • 첫 번째 인자로 콜백함수
    • 리턴할 값은 메모해줄 값 → object
  • 두 번째 인자 dependency 배열에 isKorea 전달
  • 이제 input을 증가시키면 숫자가 증가되지만(number의 state가 바뀌지만) useEffect 불리지 않음
  • 이는 효과적으로 컴포넌트 최적화시킬 수 있음
  • useEffect에서 만약 오래걸리는 작업을 해야한다면 꼭 필요할때만 호출되는 것이 좋음

(참조 : https://youtu.be/e-CnI8Q5RY4, 별코딩)