React_memo_useMemo, useCallback, memo

Lina Hongbi Ko·2023년 12월 2일
0

React_memo

목록 보기
4/5

강의를 듣다가 useMemo, useCallback, memo hook을 써서 리액트 렌더링 성능을 최적화 하는 방법에 대한 내용을 듣고, 이와 관련된 글을 찾아보고 잘 기억하기 위해 작성.

일단, 리액트가 리렌더링을 하는 조건은

  1. state 변경 되었을 때 (자신의 상태가 변경되었을 때)
  2. props가 변경되었을 때 (부모로부터 전달받은 props가 변경되었을 때)
  3. 부모 컴포넌트가 리렌더링 되었을 때

요로코럼 조건들이 있다.

따라서 이런 조건들이 발생해서 렌더링 할 필요가 없는 컴포넌트들도 계속 리렌더링 된다면, 간단한 기능이 있는 컴포넌트는 문제 없겠지만 무거운 일을 많이 처리하는 컴포넌트는 성능면에서 좋지 않을 것이다.

그래서 우리는 리액트가 렌더링 또는 리렌더링 될 때, 함수가 같은 동작인데도 불구하고 또 다시 연산하는(렌더링) 것을 방지하기 위해 연산 결과를 재사용하는 방법을 제시했다.

이렇게 캐싱하는 방법을 우리는 Memoization(메모이제이션)기법이라고 한다.

Memoization

한 번 연산한 결과를 기억하고, 다시 같은 입력이 있으면 기억해둔 데이터를 반환하는 방법

Memoization을 하기 위해 오늘 설명할 3가지의 방법을 기록해보려한다.

React.memo

function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
export default React.memo(MyComponent, areEqual);

// Returns a component when props change
// from React 공식문서

React.memo는 컴포넌트를 메모제이션해주는데, 부모 컴포넌트로 넘겨받는 props가 같다면 메모이제이션 해둔 렌더링 결과를 가져온다. 메모이제이션한 내용을 재사용하여 렌더링시 가상 DOM에서 달라진 부분을 확인하지 않아 성능상의 이점이 생기게 된다. 즉, React는 컴포넌트를 재렌더링하지 않고 마지막으로 렌더링된 결과를 재사용한다.

React.memo는 props 변화에만 리렌더링 한다.
props가 이전과 동일한 값이면 리렌더링하지 않고, 다른 값이면 재렌더링하여 컴포넌트를 다시 만들어 반환한다. 즉, React.memo에 쓰인 컴포넌트 안에서 구현한 state가 변경되면 컴포넌트는 재렌더링 된다.

💡 props가 갖는 복잡한 객체에 대하여 얕은 비교만을 수행하는 것이 기본 동작이다. (참조값만 비교) 다른 비교 동작을 원한다면, 두번째 인자로 별도의 비교 함수를 제공하면 된다.

  • 적용 예시
import React, { useEffect, useMemo, useState } from 'react';

const TextView = ({ text }) => {
  useEffect(() => {
    console.log('text 변경됨');
  });
  return <div>{text}</div>;
};

const CountView = ({ count }) => {
  useEffect(() => {
    console.log('count가 변경됨');
  });
  return <div>{count}</div>;
};

function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('a');

  return (
    <div className="App">
      <TextView text={text} />
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <br />
      <CountView count={count} />
      <button onClick={() => setCount(count + 1)}>count 1 증가</button>
    </div>
  );
}

export default App;

요로코롬 실습코드를 확인해보면,
콘솔창에 'text 변경됨', 'count가 변경됨'이 처음 마운트 되었을 때 한 번씩 출력되는 것을 확인할 수 있다.
그리고 나서 input창에 한글자를 입력하고 count 1 증가 버튼을 누르면, 콘솔창에 'text 변경됨', 'count가 변경됨'이 2번씩 총 4번 또 출력된 것을 볼 수 있는데, 각각의 state(넘겨받은 props가 변경됨)가 변경되어서 서로에게 영향을 주어 4번 리렌더링 된 것을 알 수 있다.

text가 변경되었을 때는 count가 리렌더링 될 필요 없고, count가 변경되었을 때는 text가 리렌더링 될 필요가 없으므로

React.memo를 사용하면,

const TextView = React.memo(({ text }) => {
  useEffect(() => {
    console.log('text 변경됨');
  });
  return <div>{text}</div>;
});

const CountView = React.memo(({ count }) => {
  useEffect(() => {
    console.log('count가 변경됨');
  });
  return <div>{count}</div>;
});

count 1 증가 버튼을 30번 클릭해도 CountView 컴포넌트만 리렌더링되지 TextView 컴포넌트는 리렌더링 되지 않는다. props로 들어오는 input의 값이 바뀌지 않았기때문에 이전 렌더링 결과값을 그대로 반환하는 것이다.

💡 그렇다면 얕은 비교를 한다는 말은 무슨 말인가.

다시 하나의 예시를 보면,

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

const NumView = React.memo(({ num }) => {
  useEffect(() => {
    console.log('num 변경됨');
  });
  return <div>{num}</div>;
});

const CountView = React.memo(({ count }) => {
  useEffect(() => {
    console.log('count가 변경됨');
  });
  return <div>{count.count}</div>;
});

function App() {
  const [count, setCount] = useState({
    count: 1,
  });
  const [num, setNum] = useState(1);

  return (
    <div className="App">
      <NumView num={num} />
      <button onClick={() => setNum(1)}>num값을 1로 변경</button>
      <br />
      <CountView count={count} />
      <button onClick={() => setCount({ count: 1 })}>
        count : 1 객체 생성
      </button>
    </div>
  );
}

export default App;

위와 다르게 NumView 컴포넌트와 CountView 컴포넌트가 있다.
처음 마운트 되었을 때 'num 변경됨' ,'count가 변경됨'
'num의 값을 1로 변경 버튼'을 누르면 num의 값은 1로 초기화 되고, 'count : 1 객체 생성 버튼'을 누르면 { count : 1 }로 객체로 변경해주고 있다.

'num의 값을 1로 변경 버튼'을 10번 눌렀을때 처음 콘솔창에 출력된 것을 제외하고 아무것도 출력되지 않는 것을 확인 할 수 있다. 왜냐하면 props로 들어가는 num의 값이 계속 같기 때문에 리렌더링 되지 않기 때문이다.

그렇다면 count 객체는 어떨까?

'count : 1 객체 생성 버튼'을 10번 눌렀을때는 콘솔창에 'count가 변경됨'이 10번 출력되는 것을 확인할 수 있다. 이 말은 버튼을 누른만큼 리렌더링 되었다는 뜻이다.

이것은, 얕은 비교를 했기 때문이다.

💡 얕은 비교란 무엇인가?
: 얕은 비교는 객체의 값이 아니라 객체의 주소값을 비교하는 것을 말한다.

// 1.
let a = { count : 1 };
let b = { count : 1 };
console.log(a === b); // false

// 2.
let a = { count : 1 };
let b = a;
console.log(a === b); // true

1번의 경우 다른 주소값을 가지게 되고, 2번의 경우에는 같은 주소값을 비교한다.

따라서, React.memo()를 쓸 때 비교함수를 넣어주어서 얕은 비교를 하지 않도록 만들어주어야 한다.

const CountView = ({ count }) => {
  useEffect(() => {
    console.log('count가 변경됨');
  });
  return <div>{count.count}</div>;
};

const areEqual = (prevProps, nextProps) => {
  return prevProps.count.count === nextProps.count.count;
};

// 고착 컴포넌트
const MemoizedCountView = React.memo(CountView, areEqual);

function App() {
  const [count, setCount] = useState({
    count: 1,
  });
  const [num, setNum] = useState(1);

  return (
    <div className="App">
      <NumView num={num} />
      <button onClick={() => setNum(1)}>num값을 1로 변경</button>
      <br />
      <MemoizedCountView count={count} />
      <button onClick={() => setCount({ count: 1 })}>
        count : 1 객체 생성
      </button>
    </div>
  );

areEqual()라는 함수를 만들어 주어서 주소값을 같게 만들어주었다.
요로코럼 React.memo(컴포넌트, 비교함수)를 지정하면 같은 값이 들어간 것으로 판단해 리렌더링 하지 않는다.

useMemo

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

// Returns a memoized value
// from React 공식문서

useMemo는 의존성인 [a, b]가 변경되었을 때에만 computeExpensiveValue(a, b)를 호출해 메모이제이션된 값만 다시 계산한다. [a, b]가 변경되지 않으면, computeExpnesiveValue(a, b)를 호출하지 않고, 이전 결과값을 반환한다. -> 이 말은 a와 b가 이전 값과 동일하면 연산하지 않는다는 것이다.

💡 그렇다면 useMemo의 두번째 인자에 의존성 값인 배열이 빈 배열이라면?
useEffect처럼 처음 한번만 콜백함수를 호출한다. 그렇다고 side effect를 useMemo를 이용해 구현하지는 말아야 한다고 한다. side effect는 useEffect에서 하는 일이지 useMemo에서 하는 일이 아니기 때문이라고 함.

  • 적용예시
import { useMemo, useState } from 'react';
import './App.css';

function App() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  const getDiaryAnalysis = useMemo(() => {
    console.log('useMemo 연산 시작함');
    const sum = x + y;
    return { sum };
  }, [x, y]);

  const { sum } = getDiaryAnalysis;

  return (
    <div className="App">
      x 값 : {x}
      <br />
      <button onClick={() => setX(x + 1)}>x값 1 증가</button>
      <br />y 값 : {y}
      <br />
      <button onClick={() => setY(y + 1)}>y값 1 증가</button>
      <br />
      {sum}
      <br />
      <button onClick={() => setX(1)}>x값 1로 변경하기</button>
      <button onClick={() => setY(1)}>y값 1로 변경하기</button>
    </div>
  );
}

export default App;

위의 실습코드를 확인해 보았을 때,

x값 1증가와 y값 1증가 버튼을 각각 2번씩 누르면

처음 콘솔창에 마운트 되었을 때 'useMemo 연산 시작함'이 한번 입력 되는 것을 확인할 수 있고, x와 y의 값이 각각 두번씩 총 4번 변경되었으므로 4번 콘솔창이 출력되는 것을 확인 할 수 있다. 따라서 sum의 값도 4인 것을 볼 수 있다.

그리고나서 고정된 값이 입력되도록 하기 위해 ,

x값 1로 변경하기, y값 1로 변경하기를 2번씩을 누르면 'useMemo 연산 시작함'이 2번 콘솔창에 출력되는 것을 볼 수 있다. 왜냐하면 x와 y값을 1로 변경하기를 2번씩 누르면서 총 4번 중에 2번은 값이 변경되지 않았기 때문이다. useMemo는 state가 변경되지 않았기 때문에 동일한 입력값이 들어왔다고 판단해 같은 결과값을 그대로 반환하고 콜백함수를 호출하지 않았다. 따라서 sum의 값도 2인 것도 확인 할 수 있다.

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b], // deps
);

// Returns a memoized callback
// from React 공식문서

useCallback은 의존성인 [a, b]가 변경되었을 때에만 doSomething(a, b) 함수를 반환한다. React.memo로 감싸준 자식 컴포넌트에게 함수를 prop로 넘겨줄 경우, 넘겨받는 함수(doSomething(a, b))를 useCallback으로 감싸주면 [a, b]가 바뀔 경우를 제외하고 항상 동일한 객체(함수)를 넘겨줌으로 불필요한 리렌더링을 방지할 수 있다.

💡그렇다면 useCallback의 두번째 인자에 의존성 값인 배열이 빈 배열이라면?
useEffect처럼 처음 한번만 콜백함수를 호출한다. useMemo와 동일.

  • 적용예시
import React, {useState, useCallback} from 'react';

function App() {
  const [number, setNumber] = useState(0);	
  const onClick = useCallback(() => {
      setNumber(0);
  , []);
  
  return (
    <div className="App">
      <div className="num" onClick={() => setNumber(number+1)}>{number}</div> 
      <Button onClick={onClick}/>
    </div>
  );
}

function Button({onClick}) {
    return (
      <button className="button" onClick={onClick}>RESET</button>
    );
}

위의 실습 코드를 이용해 number를 0으로 만들어주는 콜백함수를 useCallback으로 감싸주면 처음 마운트 될 때만 Button 컴포넌트가 렌더링되고, RESET 계속 버튼을 누르더라도 Button 컴포넌트가 리렌더링 되지 않는 것을 알 수 있다.

✏️ 정리

  • React.memo : 컴포넌트를 메모이제이션함
  • useMemo : 메모이제이션된 값을 반환
  • useCallback : 메모이제이션된 함수를 반환

출처

https://leehwarang.github.io/2020/05/02/useMemo&useCallback.html

https://leego.tistory.com/entry/React-useCallback%EA%B3%BC-useMemo-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

https://ssdragon.tistory.com/106

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글