본문 바로가기
Front-End/React

[ React ] React 기본 ⑦ 최적화 - useMemo

by 2CHAE._.EUN 2022. 10. 15.

[ 리액트 어플리케이션 성능 최적화를 위한 연산 결과를 재사용 ]

 

해당 일기 데이터를 분석하는 함수를 제작하고, 해당 함수가 일기 데이터의 길이가 변화하지 않을 때,

값을 다시 계산하기 않도록 하기 → Memoization 기법을 적용한 연산 최적화

 

Memoization : 이미 계산해본 연산 결과를 기억해두었다가 동일한 계산을 시키면, 다시 연산하지 않고

기억해 두었던 데이터를 반환시키게 하는 방법 

→ Memoization을 이용해서 연산 과정을 최적화할 수 있다. 

 


[ Memoization이 필요한 상황 만들어보기 ]

 

App 컴포넌트에 data state가 가지고 있는 일기들 중에 emotion에 따른 함수를 만들어보기

 

App.js

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

function App() {
  const [data, setData] = useState([]);
  // data state는 일기 데이터 배열을 저장하기 때문에 배열로 초기값을 만들어준다.

  const dataId = useRef(1); // ID는 1부터 시작

  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(() => {
    getData();
  }, []);

  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 onDelete = (targetId) => {
    const newDirayList = data.filter((it) => it.id !== targetId);
    setData(newDirayList);
  };

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

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

    // emotion이 3이상인 일기의 개수
    const goodCount = data.filter((it) => it.emotion >= 3).length;

    // emotion이 3미만인 일기의 개수
    const badCount = data.length - goodCount;

    // emotion이 3이상인 일기 비율
    const goodRatio = (goodCount / data.length) * 100;

    return { goodCount, badCount, goodRatio };
  };

  //getDiaryAnalysis()의 반환값이 객체이므로 객체로 비구조화 할당을 받는다.
  const { goodCount, badCount, goodRatio } = getDiaryAnalysis();

  return (
    <div className="App">
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기 : {data.length}</div>
      <div>emotion이 3이상인 일기 개수 : {goodCount}</div>
      <div>emotion이 3미만인 일기 개수 : {badCount}</div>
      <div>emotion이 3이상인 일기 비율 : {goodRatio}</div>
      <DiaryList onEdit={onEdit} diaryList={data} onDelete={onDelete} />
    </div>
  );
}

export default App;

 

getDiaryAnalysis 함수가 호출될 때마다 일기 분석 시작이 콘솔에 출력되도록 해놓으면 여러 번 동작하는 것을

확인할 수 있다. data state의 초기 값은 빈배열이지만 getData로 API를 받아오는 것을 성공하고 setData가 이루어

지면서 data state가 바뀌게 되면 App 컴포넌트에 리랜더링이 일어난다. 그래서 getDiaryAnaysis() 안에 있는

변수들이 재생성이 되고 코드가 다시 수행이 되면서 getDiaryAnaysis()가 다시 호출이 된다.

 

결론적으로는 App 컴포넌트가 리턴하는 JSX 문법의 HTML 요소들은 화면에 반영이 될뿐이지 자바스크립트의 함수가

호출되고 반환하는 것은 똑같다. 즉 리랜더링이 된다는 것이 App 컴포넌트가 한번더 호출된다는 의미이다. 

 

App 컴포넌트가 리랜더링이 일어나게 된다면 아래 코드가 다시 재실행이 된다.

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

 


[ Memoization 기법 활용해보기 ]

 

일기 데이터를 수정하는 행위는 일기를 분석하는 행위에 아무런 영향을 미치지 않는다. 하지만 일기를 수정할 경우

App 컴포넌트를 리랜더링하기 때문에 일기를 분석하는 행위도 또 수행이 되게 된다. 

 

리액트에서 리턴을 가지고 있는 함수를 Memoization을 해서 연산들을 최적화하기 위해서는 useMemo 기능을

사용한다. useMemo의 사용법으로는 Memoization을 하고 싶은 함수를 ()를 통해 감싸주면 된다.

 

import { useMemo } from "react";

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

    // emotion이 3이상인 일기의 개수
    const goodCount = data.filter((it) => it.emotion >= 3).length;

    // emotion이 3미만인 일기의 개수
    const badCount = data.length - goodCount;

    // emotion이 3이상인 일기 비율
    const goodRatio = (goodCount / data.length) * 100;

    return { goodCount, badCount, goodRatio };
  }, [data.length]);

 

getDiaryAnalysis 함수는 useMemo를 호출한 결과값이 되었고 useMemo 안에 콜백 함수로 getDiaryAnalysis가

수행하는 기능을 전달하는 형태이다. 

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

 

useMemo에 두번째 인자로 배열을 전달해주면 useEffect의 dependency array와 동일한 역할을 한다. 

배열에 일기 데이터의 총 개수인 data.length를 전달해주면 data.length가 변화할 때만 useMemo의 첫번째

인자로 전달한 콜백 함수가 다시 수행이 된다.

 

즉, 아무리 getDiaryAnalysis()를 호출한다 하더라도 data.length가 변하지 않는 이상 똑같은 리턴을 계산하지 않고

반환하게 된다. 

 


[ useMemo 사용시 주의할 점 ]

 

useMemo로 어떤 함수를 감싸고 두번째로 dependency array를 전달해서 함수를 최적화할 경우 더 이상 그 함수는

함수가 아니다. 왜냐하면 useMemo는 어떤 함수를 전달받아서 콜백 함수가 리턴하는 값을 그냥 리턴하기 때문이다.

즉, getDiaryAnalysis()는 useMemo로 부터 값을 리턴받게 되므로 더이상 함수가 아니라 값으로 사용을 해야한다.

 

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

    // emotion이 3이상인 일기의 개수
    const goodCount = data.filter((it) => it.emotion >= 3).length;

    // emotion이 3미만인 일기의 개수
    const badCount = data.length - goodCount;

    // emotion이 3이상인 일기 비율
    const goodRatio = (goodCount / data.length) * 100;

    return { goodCount, badCount, goodRatio };
  }, [data.length]);

  //getDiaryAnalysis()의 반환값이 객체이므로 객체로 비구조화 할당을 받는다.
  const { goodCount, badCount, goodRatio } = getDiaryAnalysis;

 

일기를 추가해서 data.length의 값이 변하지 않는 이상 일기를 분석하는 기능을 하는 getDiaryAnalysis()는

수행되지 않는다.

 

어떤 값을 리턴하고 있는 함수의 연산 과정을 최적화하고 싶다면 useMemo를 사용해서 어떤 값이 변화할 때만

연산을 수행할 것인지 명시해주면 그 함수를 값처럼 사용해서 연산 최적화를 할 수 있다.