React 최적화

DOYOUNG·2023년 8월 14일
0

reactjs

목록 보기
9/15
post-thumbnail

React 최적화

React의 컴포넌트가 리랜더링 되는 경우는 다음과 같다.
1. 컴포넌트 자신이 가진 state에 변화가 생겼을 경우
2. 부모 컴포넌트에 리랜더링이 일어날 경우
3. 자신이 받은 prop이 변경되는 경우
컴포넌트가 꼭 필요한 경우에만 리랜더링 될 수 있도록 React에는 컴포넌트를 최적화할 수 있는 다양한 방법이 있다.

1) useMemo (연산 최적화)

import React, {useMemo} from 'react';

React의 기능이기 때문에 useMemo를 import 해야함.

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; // getDiaryAnalysis을 함수가 아닌 값으로 사용

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

기본공식 : useMemo(콜백함수,[]);

useMemo(콜백함수,[data.length]); => data.length 즉, 데이터의 길이가 변할때만 작동함
📌 중요!! useMemo를 사용하면 콜백함수가 리턴하는 값을 반환하기 때문에 getDiaryAnalysis 은 더이상 함수가 아니므로 값으로 사용.
useMemo를 사용해서 뒤의 배열에 어떤 값이 변화할 때 연산을 다시 수행할지 명시하면 그 함수를 값처럼 사용하여 연산 최적화를 할 수 있다.

📍 Memoization

이미 계산해 본 연산 결과를 기억해두었다가 동일한 계산을 시키면
다시 연산하지 않고 기억해두었던 데이터를 반환시키게 하는 방법

2) React.memo

함수형 컴포넌트에게 업데이트 조건 걸기.

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;

OptimizeTest와 내부 컴포넌트 TextView, CountView가 있다.
내부 컴포넌트가 받고 있는 prop인 text나 count 중 하나만 바뀌어도 두개의 컴포넌트가 모두 리랜더 된다.
이러한 낭비를 막기 위하여 컴포넌트가 자신이 받고있는 prop이 변화할때만 리랜더될 수 있도록 조건을 걸어줄 수 있는것이
React.memo이다.
React.memo 를 사용하여 컴포넌트를 감싸주면 그 컴포넌트는 prop이 변화할 때만 리랜더된다.

컴포넌트 통채로 React.memo 사용

하나의 컴포넌트 파일을 통채로 React.memo로 감싸기 위해서는 컴포넌트 마지막 줄 export에 아래와 같이 사용하면 된다.

export default React.memo(DiaryEditor);

3) areEqual 함수 (prop이 객체일 때)

React.memo는 props가 변화할 때 리랜더된다. 그러나 props는 객체에 대하여 얕은 비교만 하기 때문에 같은 객체의 값을 가지더라도 컴포넌트는 계속 리랜더가 되는 상황이 발생한다.
그러한 상황에서 성능 최적화를 위한 areEqual 함수를 사용할 수 있다.

📌 areEqual 함수 기본공식

nextProps가 prevProps와 같은 값을 가지면 true, 그렇지 않으면 false 반환함.

const areEqual = (prevProps, nextProps)=>{
  return true // 이전 props와 현재 props가 같다 = 리랜더링 X
  return false // 이전 props와 현재 props가 다르다 = 리랜더링 O
}

자식컴포넌트 CounterB와 areEqual 함수

자식 컴포넌트 CounterB는 객체인 obj를 props로 받고 있기 때문에 최적화를 위하여
React.memo가 아닌 areEqual 함수를 사용한다. CounterB에 React.memo를 사용하여 감싸면 컴포넌트는 버튼을 누를 때마다 리랜더가 되는것을 useEffect를 통해 확인할 수 있었다.

prevProps과 nextProps을 비교하여 리랜더링 여부를 결정하는 areEqual 함수를 사용하고 새로운 컴포넌트 MemoizedCounterB를 만들어서 React.memo에 (최적화할 컴포넌트, areEqual) 를 넣어준다.

const CounterB = ({obj}) => {

  useEffect(()=>{
    console.log(`CounterB Update - count : ${obj.count}`)
  })

  return <div>{obj.count}</div>
}

const areEqual = (prevProps, nextProps)=>{ // areEqual 함수사용

  if(prevProps.obj.count === nextProps.obj.count){
    return true;
  }
  return false;

  // return prevProps.obj.count === nextProps.obj.count;
  // 위의 if문을 이렇게 쓸 수도 있음.
}

const MemoizedCounterB = React.memo(CounterB, areEqual);
// CounterB는 areEqual 함수의 판단에 따라서 리랜더링을 여부를 결정하는 메모화된 컴포넌트가 된다.

부모 컴포넌트 OptimizeTest

부모 컴포넌트에서는 CounterB가 아닌 메모화된 새로운 컴포넌트 MemoizedCounterB를 불러온다.

const OptimizeTest = ()=>{

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

  
  return (
    <div>
      <h2>Counter B</h2>
      <MemoizedCounterB obj={obj}/>
      <button onClick={()=>setObj({
          count: obj.count
        })}>B button</button>
    </div>
  )
};

4) useCallback

최적화할 컴포넌트 DiaryEditor 파일에서 React.memo를 사용하여 감싸주고,
해당 컴포넌트에서 prop으로 받고 있는 oncreate 함수가 포함되어 있는 부모 컴포넌트에 useCallback 을 사용한다.

기본공식 : useCallback(함수,[]);

자식컴포넌트 DiaryEditor (React.memo 사용)

export default React.memo(DiaryEditor);

부모컴포넌트 App.js (useCallback 사용)

리액트의 기능이므로 useCallback import 해준다.

import React, {useCallback} from 'react';
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((data)=>[newItem, ...data]); 
},[]);

자식 컴포넌트에 prop으로 넘겨주는 함수인 oncreate에 useCallback을 사용한다.
여기서 useCallback(기존 함수,[]); 형식으로 사용하게 되는데, 이 때 뒤의 배열을 비워두면 해당 함수가 실행될 때 기존의 데이터가 사라지고 새로운 데이터만이 남게된다. 마운트 될 때, 초기값인 뒤의 배열이 비어있기 때문인데 이 문제를 보완하기 위해 함수형 업데이트를 사용할 수 있다.
함수형 업데이트란 상태변화 함수인 setState 함수에 함수를 전달하는 것이다.
현재 함수 내부에 있는 상태변화 함수인 setData에 데이터를 전달하는 콜백함수를 넣어주면 해당 함수와 컴포넌트는 최적화 되어 정상적으로 작동하게 된다.

profile
프론트엔드 개발자 첫걸음

2개의 댓글

comment-user-thumbnail
2023년 8월 14일

유익한 글이었습니다.

1개의 답글