Portfolio, Project/Project(Programming)

React - useMemo / React.memo / useCallback

잇(IT) 2023. 9. 20. 11:30
useMemo
- useMemo란

1. 성능 최적화를 위해 사용된다.

2. 이전 결과를재사용함으로써 렌더링 성능을 향상시킬 수 있다. 결과값이 변하지 않을 경우 리렌더링 할 때 마다 연산을 하게 되면 성능 최적화가 되지 않기 때문이다.


- 위와 같이 각 일기(객체)에는 감정 점수를 할당 할 수 있고, 중간에 결과값 같이 감정에 따른 비율을 측정하는 코드를 추가할 것이다.

 

- App.js

.......

const getDiaryAnalysis = () => {
    console.log("일기 분석 시작");

    const goodCount = data.filter((it) => it.emotion >= 3).length;
    const badCount = data.length - goodCount;
    const goodRatio = (goodCount / data.length) * 100;
    return { goodCount, badCount, goodRatio };
  };

  const { goodCount, badCount, goodRatio } = getDiaryAnalysis();

  return (
    <div className="App">
      <Lifecycle />
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기 : {data.length}</div>
      <div>기분 좋은 일기 개수 : {goodCount}</div>
      <div>기분 나쁜 일기 개수 : {badCount}</div>
      <div>기분 좋은 일기 비율 : {goodRatio}%</div>
      <DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
    </div>
  );
}

- getDiaryAnalysis 함수는 1~5까지 지정되어 있는 감정 점수들을 1~3까지는 '나쁨' 4~5는 '좋음'으로 구분하여 좋음의 비율을 나타내는 함수이다.

- 위의 함수는 App 컴포넌트 안에 속한 함수기 때문에 useState에 의해 값이 하나라도 변경되면 컴포넌트 전체가 리렌더링 되고, 컴포넌트 안의 모든 코드가 재실행 된다.

- 하지만 getDiaryAnalysis()의 경우 일기의 개수가 변하지 않은 상태에서 리렌더링 될 때마다 함수가 실행되는 것은 성능 최적화에 좋지 않다.


- useMemo 사용

 

- App.js

.............

const getDiaryAnalysis = useMemo(() => {
    console.log("일기 분석 시작");

    const goodCount = data.filter((it) => it.emotion >= 3).length;
    const badCount = data.length - goodCount;
    const goodRatio = (goodCount / data.length) * 100;
    return { goodCount, badCount, goodRatio };
  }. [data.length]);

  const { goodCount, badCount, goodRatio } = getDiaryAnalysis;

  return (
    <div className="App">
      <Lifecycle />
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기 : {data.length}</div>
      <div>기분 좋은 일기 개수 : {goodCount}</div>
      <div>기분 나쁜 일기 개수 : {badCount}</div>
      <div>기분 좋은 일기 비율 : {goodRatio}%</div>
      <DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
    </div>
  );
}

- useMemo의 사용법은

1. 결과의 캐시를 사용하고 싶은 함수 앞에 useMemo()를 통해 감싸준다.

2. useMemo()로 감싸게 되면 해당 함수의 리턴 값은 값이기 때문에 더이상 함수가 아니기 때문에 사용하기 위해션 변수처럼 사용해야 한다.

3. useMemo()의 두번째 파라미터는 useEffect와 유사하게 배열을 받으면 배열 안의 값이 변경 될 때 마다 해당 함수를 실행하게 된다.

 

- 즉, useMemo를 사용하게 되면 한 번 연산된 결과를 반복해서 사용하며, 두번째 파라미터로 받은 [] 배열 안의 값이 변하지 않으면 함수 안의 내용을 연산하지 않고, [] 안의 값이 변경될 때만 함수를 재실행 하여 연산한다.


- React.memo

- React.memo

1. React의 성능 최적화를 위한 함수 컴포넌트 래퍼이다. 컴포넌트의 렌더링을 memorization하고, 이전에 렌더링한 결과를 재사용 할 수 있다.

 

- 동작 방식

1. 컴포넌트가 렌더링될 때, React.memo는 이전에 렌더링한 결과를 저장한다.

2. 다음 렌더링 시, 컴포넌트의 props가 변경되었는지 확인한다.

3. 만약 props가 변경되었다면, 컴포넌트를 다시 렌더링하고, 그 결과를 저장한다.

4. props가 변경되지 않았다면, 이전에 저장한 결과를 재사용하여 불필요한 렌더링을 피한다.

 

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

const Textview = React.memo(({ text }) => {
  useEffect(() => {
    console.log(`Update :: Text : ${text}`);
  });
  return <div>{text}</div>;
});

const CountView = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`Update :: Count : ${count}`);
  });
  return <div>{count}</div>;
});


const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [text, setText] = useState("");

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count</h2>
        <CountView count={count} />
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>
      <div>
        <h2>text</h2>
        <Textview text={text} />
        <input value={text} onChange={(e) => setText(e.target.value)} />
      </div>
    </div>
  );
};

export default OptimizeTest;

- 위 코드에서 React.memo를 제거하게 되면 useEffect() 두번째 파라미터 값이 없기 때문에, 리렌더링 될 때마다 해당 함수 안에 속하는 console.log가 계속해서 출력될 것이다.

 

const Textview = React.memo(({ text }) => {
  useEffect(() => {
    console.log(`Update :: Text : ${text}`);
  });
  return <div>{text}</div>;
});
const CountView = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`Update :: Count : ${count}`);
  });
  return <div>{count}</div>;
});

- 먼저 사용하고 싶은 함수 앞에 React.memo를 붙이면 된다. 

 

- React.memo를 붙이게 되면 화면이 새롭게 렌더링 되어도 이전 값과 비교해서 변화가 일어난 경우에만 해당 함수를 실행하는 것을 확인 할 수 있다.


- React.memo 값, 객체 비교
import React, { useState, useEffect } from "react";

const CounterA = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`CounterA Update - count : ${count}`);
  });
  return <div>{count}</div>;
});

const CounterB = React.memo(({ obj }) => {
  useEffect(() => {
    console.log(`CounterB Update - count : ${obj.count}`);
  });
  return <div>{obj.count}</div>;
});

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({
    count: 1,
  });

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        <button onClick={() => setCount(count)}>A button</button>
      </div>
      <h2>Counter B</h2>
      <CounterB obj={obj} />
      <button
        onClick={() =>
          setObj({
            count: obj.count,
          })
        }
      >
        B button
      </button>
    </div>
  );
};

export default OptimizeTest;

- 위 코드는 버튼을 누르면 useState를 통해 변경되는 값이 기존의 값과 동일하게 변화되는 코드이다.

- CounterA는 값이, CounterB에는 객체가 useState()의 초기 값으로 들어가 있고 변화되어도 동일한 값, 객체를 저장하도록 한다.

- 이전에 테스트한 결과를 생각해보면 React.memo는 리렌더링 시 이전 값과 비교하여 값이 동일하면 해당 함수를 실행하지 않는 것으로 알고 있지만 위 코드를 테스트 해보면 CounterB의 함수가 반복적으로 실행되는 것을 알 수 있다.

 

* - React는 얕은 비교를 하기 때문에 함수가 계속해서 반복된다. (객체 비교시 주소값을 비교하여 주소가 다르다면 서로 다른 객체로 판단한다.)


- areEqual (Java의 compare 함수와 유사하게 비교 메서드를 직접 작성하여 사용할 수 있다.)

- React.memo 사용 시 얕은 비교가 아닌 객체의 속성과 값만 보고 판단하기 위해선 추가적인 작업이 필요하다.

- 통상적으로 areEqual() 함수를 생성하여 렌더링 이전과 후 객체를 비교하는 코드를 작성한다.

1. true / false 를 반환함으로서 true를 반환하면 리렌더링시 함수를 재호출 하지 않고, false를 반환하면 함수를 재호출 하게 된다. 

 

const CounterB = ({ obj }) => {
  useEffect(() => {
    console.log(`CounterB Update - count : ${obj.count}`);
  });
  return <div>{obj.count}</div>;
};

const areEqual = (prevProps, nextProps) => {
  if (prevProps.obj.count === nextProps.obj.count) {
    return true;
  }
  return false;
};

const MemorizedCounterB = React.memo(CounterB, areEqual);

const OptimizeTest = () => {

  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({
    count: 1,
  });

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        <button onClick={() => setCount(count)}>A button</button>
      </div>
      <h2>Counter B</h2>
      <MemorizedCounterB obj={obj} />
      <button
        onClick={() =>
          setObj({
            count: obj.count,
          })
        }
      >
        B button
      </button>
    </div>
  );
};
const areEqual = (prevProps, nextProps) => {
  if (prevProps.obj.count === nextProps.obj.count) {
    return true;
  }
  return false;
};

const MemorizedCounterB = React.memo(CounterB, areEqual);

- 기존의 React.memo를 제거하고, 추가적으로 areEqual 함수를 작성한다. (위에 작성한 areEqual 함수는 count의 값을 비교한다.)

- 다음 React.memo()의 파라미터로 첫번째는 렌더링 할 함수, 두번째 함수로는 비교함수를 넘긴다.

 

=> MemorizedCounterB는 areEqual를 보고 true이면 CounterB의 함수를 실행하고, areEqual이 false이면 CounterB를 실행하지 않는다.

- 이전과 비교하여 같은 테스트를 실행하여도 얕은 비교가 아닌 별도의 비교를 통해 같은 값이라고 인식되기 때문에 함수가 실행되지 않는 것을 확인할 수 있다.


- useCallback

- useCallback이란 함수를 memorization하고, 컴포넌트 리렌더링 시 함수의 불필요한 재생성을 방지하는 데 사용된다. 함수를 memorization하면 이전에 생성된 함수를 재사용하여 성능을 최적화 할 수 있으며, 주로 자식 컴포넌트에 콜백 함수를 전달할 때 유용하다.

 

- DiaryEditor.js

const DiaryEditor = ({ onCreate }) => {
  useEffect(() => {
    console.log("DiaryEditor 렌더");
  });
  const authorInput = useRef();
  
  ..............
  
      </div>
  );
};
export default React.memo(DiaryEditor);

- DiaryEditor 컴포넌트의 경우 prop으로 onCreate 컴포넌트를 받아온다.

- DiaryEditor 컴포넌트는 일기의 새로운 내용을 추가하는 컴포넌트인데, prop로 넘어온 onCreate는 값이 아니기 때문에, 얕은 비교로 인해 렌더링 되는 결과가 동일하더라도, 부모 컴포넌트가 계속 리렌더링을 하게 되면 함수의 반환값은 호출될 때 마다 달라지기 때문에 얕은 복사로 인해 계속해서 onCreate prop를 전달받은 DiaryEditor가 호출될 것이다.

 

- useCallback 함수를 이용하면 반복 호출되는 함수를 제어할 수 있다.

 

- App.js

const onCreate = useCallback((author, content, emotion) => {
    const created_date = new Date().getTime();
    const newItem = {
      author,
      content,
      emotion,
      created_date,
      id: dataId.current,
    };
    dataId.current += 1;
    setData([newItem ...data]);
  }, []);

- onCreate 함수를 useCallback()으로 감싸준다. 두번째 파라미터로 넘어가는 값은 마찬가지로 배열을 넘겨줄 수 있으며, 배열 안의 값이 변경되지 않으면, 첫번째로 전달된 콜백 함수를 계속해서 사용할 수 있다. (사용할 수 있다는 것은 리렌더링 된다고 해서 함수를 반복해서 실행하지 않는다는 뜻이다.)

 

+ 주의 사항

- useCallback을 사용하게 되면 두번째 파라미터가 위와 같이 [] 빈 배열로 넘기게 되면, 최초에 함수가 실행되고 이후에 onCreate는 변경이 일어나기 전까지 이전 데이터를 사용할 것이다.

- 최초에 실행되었을 때 data의 값은 useState([])에 의해 빈 배열을 전달 받았다.

 

- 일반적으로 함수가 렌더링 될 때마다 실행되야 하는 이유는 실시간으로 변하는 데이터들을 계속해서 참조해야 하기 때문이다. 하지만 useCallback에 속하게 되면 함수는 최초에 실행된 이후에 해당 함수를 직접 호출하지 않는 이상 계속해서 최초 함수를 호출한 상태만 가지고 있기 때문에 위의 영상과 같은 현상이 일어나는 것이다.

 

- 함수형 업데이트

- 위 상황을 해결하기 위해 함수형 업데이트를 사용한다.

- 상태변화 함수*setData)의 파라미터에 함수형 업데이트를 사용하게 되면 함수가 사용될 때마다 항상 최신의 데이터를 받을 수 있다.

const onCreate = useCallback((author, content, emotion) => {
    console.log("언제 실행되는거야 10팔");
    const created_date = new Date().getTime();
    const newItem = {
      author,
      content,
      emotion,
      created_date,
      id: dataId.current,
    };
    dataId.current += 1;
    setData((data) => [newItem, ...data]);
    //함수형 업데이트
  }, []);
    setData((data) => [newItem, ...data]);
    //함수형 업데이트
  }, []);

- 위와 같이 함수형 업데이트를 사용하게 되면 최초 실행 이후에 onCreate 함수는 별도로 사용되지 않지만, 이후에 사용되었을 때, 상태 함수 안의 값은 최신의 데이터를 받아올 수 있게 된다.

728x90