[React] useMemo()로 Rendering 최적화 1 (feat.일기장)

Hyun·2022년 1월 7일
0

React

목록 보기
15/22
post-thumbnail

💡useMemo()란?

React의 Rendering 성능 최적화를 위한 Hook

사용방법

React의 기능이므로 항상 상단에 import를 해줘야한다.
import React, { useMemo } from "react";

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

  • 첫번째 매개변수(Parameter) = memoization하고싶은 값을 콜백함수로 감싼다. return하는 값을 memoization
  • 두번째 매개변수(Parameter) = 배열을 전달 (useEffect와 똑같은 배열)
    배열에 값을 넣으면 그 값이 변화할때만 첫번째인자인 콜백함수가 다시 실행되게된다. 뒤에서 memoizedValue를 호출한다하더라도 두번째인자값이 변화하지않으면 똑같은 리턴만 반환된다.
  • useMemo에서 전달된 함수는 랜더링 중에 실행되므로, 랜더링 중에서 실행하지 않는 함수는 useEffect를 사용할 것
  • useRef와의 차이는, useRef는 DOM element의 특정 속성 값을 기억한다면, useMemo는 특정 함수의 return값을 기억하는 것

useMemo로 함수를 최적화하면 memoizedValue은 더이상 함수가 아니게된다. 결과적으로 함수호출이 아니라 값호출로 사용해야한다.

React공식문서

“생성(create)” 함수와 그것의 의존성 값의 배열을 전달하세요.
useMemo는 의존성이 변경되었을 때에만 메모이제이션된 값만 다시 계산 할 것입니다. 이 최적화는 모든 렌더링 시의 고비용 계산을 방지하게 해 줍니다.
useMemo로 전달된 함수는 렌더링 중에 실행된다는 것을 기억하세요. 통상적으로 렌더링 중에는 하지 않는 것을 이 함수 내에서 하지 마세요. 예를 들어, 사이드 이펙트(side effects)는 useEffect에서 하는 일이지 useMemo에서 하는 일이 아닙니다.
배열이 없는 경우 매 렌더링 때마다 새 값을 계산하게 될 것입니다.

주의
의존성 값의 배열은 함수에 인자로 전달되지는 않습니다. 그렇지만 개념적으로는, 이 기법은 함수가 무엇일지를 표현하는 방법입니다. 함수 안에서 참조되는 모든 값은 의존성 값의 배열에 나타나야 합니다. 나중에는 충분히 발전된 컴파일러가 이 배열을 자동으로 생성할 수 있을 것입니다.

💡Memoization이해하기


연산을 최적화하다 = 다시만난 문제에 대해서 또 다른 계산을 하지 않게됨을 의미


📖예제

앞서 App.js컴포넌트가 re-render가일어난다면 당연히 getDiaryAnalysis함수도 재실행이되고, 갯수에 영향을 주지않는 내용을 수정해도 getDiaryAnalysis가 re-rendering이 일어나는 경우를 없애기위해 return을 가진 함수를 memoization 해서 연산들(=함수들)을 최적화하기 위해서는 useMemo함수를 사용한 예제를 다뤄보곘다.

연산결과 재사용
현재 일기데이터를 갯수를 분석하는 함수를 제작하고 해당 함수가 일기데이터의 길이가 변화하지않을 때 re-rendering되지 않도록하기

App.js 코드

import { useMemo, useEffect, 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(() => {
    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 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(() => {
    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">
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기 : {data.length} 개</div>
      <div>기분 좋은 일기 : {goodCount} 개</div>
      <div>기분 안좋은 일기 : {badCount} 개</div>
      <div>기분 좋은일기 비율 : {goodRatio} %</div>
      <DiaryList onEdit={onEdit} onDelete={onDelete} diaryList={data} />
    </div>
  );
};

export default App;

<코드 설명>

1)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]);
= 결과적으로 getDiaryAnalysis는 useMemo를 호출하는 결과값이되고 useMemo안에 콜백함수로 원래 getDiaryAnalysis가 수행하는 요소들을 전달함
useMemo함수는 첫번째인자로 콜백함수를 받아서 콜백함수가 리턴하는값을 연산에 최적화할 수 있도록 도와주는 기능
두번째인자로 댑스인 배열을 전달받음. 배열에 data.length을 넣으면 data.length가 변화할때만 콜백함수가 재수행됨

2)const { goodCount, badCount, goodRatio } = getDiaryAnalysis;
= useMemo함수를 사용했으니까 getDiaryAnalysis는 더이상 함수역할을 하지않고 값 역할을 한다

🎯정리

어떤 함수가 있고 그 함수가 어떤 값을 리턴하고있는데 그 리턴까지의 연산을 최적화하고싶다면 useMemo를 사용해서 댑스에 어떤값이 변화할때만 그 리턴까지의 연산을 수행할것인지를 명시하면 그 함수를 값처럼 사용하여 연산최적화를 할 수 있다.

🖥결과화면


처음화면 콘솔에 두번실행된것을 확인할 수 있다

수정했을경우 (댑스에 data.length로 명시해놔서 함수가 실행되지않는다)

삭제했을경우 함수가 실행되면서 콘솔에 찍한것을 확인할수있고 개수또한 달라진것을 볼 수 있다.


실제로 useMemo사용 전 알아두어야 할것

성능 최적화를 할때는 얻을 수 있는 실제 성능 이점이 지불하는 대가에 비해서 미미하지 않은지에 대해서 반드시 따져보시고 사용을 해야한다.

예를 들어, useMemo hook 함수를 납용하면, 컴포넌트의 복잡도가 올라가기 때문에 코드를 읽기도 어려워지고 유지보수성도 떨어지게 됩니다. 또한 useMemo가 적용된 레퍼런스는 재활용을 위해서 가바지 컬렉션(garbage collection)에서 제외되기 때문에 메모리를 더 쓰게 됩니다.

오래 걸리는 로직이 있다고 해도 useEffect hook 함수 등을 이용해서 비동기로 처리하는 방안을 먼저 고려하게 된다.

[React공식문서]
useMemo는 성능 최적화를 위해 사용할 수는 있지만 의미상으로 보장이 있다고 생각하지는 마세요. 나중에 React에서는, 이전 메모이제이션된 값들의 일부를 “잊어버리고” 다음 렌더링 시에 그것들을 재계산하는 방향을 택할지도 모르겠습니다. 예를 들면, 오프스크린 컴포넌트의 메모리를 해제하는 등이 있을 수 있습니다. useMemo를 사용하지 않고도 동작할 수 있도록 코드를 작성하고 그것을 추가하여 성능을 최적화하세요.

useEffect와의 차이
useMemo는 deps가 변경되기 전까지 값을 기억하고, 실행후 값을 보관하는 역할로도 사용한다. 얘는 복잡한 함수의 return 값을 기억한다는 점에서 useEffect와는 다르다.
useRef는 특정 값을 기억하는 경우, useMemo는 복잡한 함수의 return값을 기억하는 경우에 사용


🚀참고자료

useMemo와 React.memo의 차이점
useMemo사용 전 알아두어야 할것
React공식문서-useMemo()
React강의-이정환강사

profile
FrontEnd Developer (with 구글신)

0개의 댓글