세현's 개발로그

[React] useMemo(), React.memo 최적화(feat. 일기장) 본문

React

[React] useMemo(), React.memo 최적화(feat. 일기장)

SarahPark 2023. 5. 7. 16:33

◈ Memoization

답을 기억하는 메모이제이션을 프로그래밍에도 사용할 수 있다.

 

◈ 최적화

최적화는 쉽게 말해 re-render 되는 횟수를 줄이는 것이다.

<re-rendering 되는 경우>
1. Props가 변경
2. State가 변경
3. 부모 컴포넌트가 re-render
4. Context value가 변경

메모이제이션을 이용하여 rendering 최적화를 해보자.

App.js

1)   const getDiaryAnalysis = useMemo(() => {
    if (data.length === 0) {
      return { goodcount: 0, badCount: 0, goodRatio: 0 };
    }
    console.log("일기 분석 시작");

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

 

useMemo함수는 첫번째 인자로 콜백함수를 받아서 콜백함수가 리턴하는 값을 연산에 최적화할 수 있도록 도와준다.

goodCount는 filter를 이용하여 감정 점수가 3이상인 일기의 개수를 센다.

badCount는 전체개수-goodCount

goodRatio는 전체 일기의 개수에서 감정 점수가 3이상인 일기의 비율을 계산해준다.

이 세 가지를 객체로 담아 return 한다.

이때 두 번째 인자로 [data.length]를 전달해준 것은, data의 길이가 바뀔 때(일기가 추가되거나 삭제될 때)만 re-rendering 되도록 하기 위함이다.

 

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

 

비구조화할당으로 값을 변수에 담아준다. 이때, 메모이제이션을 이용했으므로 getDiaryAnalysis는 함수를 호출하는 것이 아닌 값을 저장하고 있는 것이므로 getDiaryAnalysis()로 쓰지 않도록 주의해야 한다.

 

import { useEffect, useMemo, useRef, useState } from "react";
import "./App.css";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";

const App = () => {
  const [data, setData] = useState([]);
  const dataId = useRef(0);

  const getData = async () => {
    const res = await fetch(
      "https://jsonplaceholder.typicode.com/comments"
    ).then((res) => res.json());

    const initData = res.slice(0, 20).map((it) => {
      return {
        author: it.email,
        content: it.body,
        emotion: Math.floor(Math.random() * 5) + 1,
        created_date: new Date().getTime(),
        id: dataId.current++
      };
    });

    setData(initData);
  };

  useEffect(() => {
    setTimeout(() => {
      getData();
    }, 1500);
  }, []);

  const onCreate = (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]);
  };

  const onRemove = (targetId) => {
    const newDiaryList = data.filter((it) => it.id !== targetId);
    setData(newDiaryList);
  };

  const onEdit = (targetId, newContent) => {
    setData(
      data.map((it) =>
        it.id === targetId ? { ...it, content: newContent } : it
      )
    );
  };

  const getDiaryAnalysis = useMemo(() => {
    if (data.length === 0) {
      return { goodcount: 0, badCount: 0, goodRatio: 0 };
    }
    console.log("일기 분석 시작");

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

  const { goodCount, badCount, goodRatio } = getDiaryAnalysis;

  return (
    <div className="App">
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기 : {data.length}</div>
      <div>기분 좋은 일기 개수 : {goodCount}</div>
      <div>기분 나쁜 일기 개수 : {badCount}</div>
      <div>기분 좋은 일기 비율 : {goodRatio}</div>
      <DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
    </div>
  );
};
export default App;
useMemo는 deps가 변경되기 전까지 값을 기억하고, 실행 후 값을 보관하는 역할로도 사용한다. 복잡한 함수의 return 값을 기억한다는 점에서 useEffect와 다르다. useRef는 특정 값을 기억하는 경우, useMemo는 복잡한 함수의 return값을 기억하는 경우에 사용한다.

◈ React.memo란?

[React 공식 문서]
React.memo는 고차 컴포넌트(Higher Order Component)이다.
(=컴포넌트를 가져와 새 컴포넌트를 반환하는 함수)

즉, rendering 결과를 Memoizing 함으로써, 불필요한 re-rendering을 건너뛴다.

 

cont MyComponent = React.memo(function MyComponent(props) {
  /* props를 사용하여 랜더링 */
 });

혹은 export 할 때 감싸서 내보낸다.

export default React.memo(OptimizeTest);

- React.memo는 props에 변화가 일어날 때만 영향을 준다. 

- React.memo(re-rendering이 일어나지 않았으면 하는 컴포넌트)를 입력하면 props가 변화하지 않으면 반환되지 않는 고차 컴포넌트(강화된 컴포넌트)를 돌려준다. 물론 자기 자신의 state가 바뀌면 re-rendering이 일어난다.

 

OptimizeTest.js 라는 테스트용 컴포넌트를 생성하여 count와 obj(객체)를 자식컴포넌트로 두어 re-rendering이 어떻게 일어나는지 확인하자.

OptimizeTest.js

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

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

 

count와 odj 상태를 받아서 prop으로 받아서 활용할 두 개의 자식컴포넌트를 만들어준다. 

odj는 객체이기 때문에 점표기법으로 count를 꺼내서 쓴다.

 

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


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

 

areEqual 함수를 만들어서 true반환을 하면 리랜더링을 일으키지 않고 false라면 리랜더링을 일으킨다.

그리고나서 MemoizedCounterB 컴포넌트를 새로 만들어서 React.memo에 첫 번째 인자로 CounterB를 넣고 두 번째 인자로는 areEqual 함수를 넣어주었다. areEqual의 상태에 따라 리랜더링 여부를 결정하게 된다.

 

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

 

odj useState 안에는 객체로 count:1이라는 프로퍼티를 넣어둔다.

 

4)  <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        <button onClick={() => setCount(count)}>A Button</button>
      </div>

 

CounterA는 setCount에 count를 전달한다. 그러면 setCount로 상태변화를 일으켰지만 바뀌는 값은 없으므로 리랜더링이 되지 않는다.

 

5) <div>
        <h2>Counter B</h2>
        <MemoizedCounterB obj={obj} />
        <button onClick={() => setObj({ count: 1 })}>B Button</button>
      </div>

 

CounterB는 setObj로 상태변화를 시킬건데 마찬가지로 값이 바뀌지 않으므로 리랜더링이 되지 않는다.

 

 

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

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

const CounterB = ({ obj }) => {
  useEffect(() => {
    console.log(`CountB 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 MemoizedCounterB = 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>
      <div>
        <h2>Counter B</h2>
        <MemoizedCounterB obj={obj} />
        <button onClick={() => setObj({ count: 1 })}>B Button</button>
      </div>
    </div>
  );
};

export default OptimizeTest;

 

Comments