React 최적화 Hook(React.memo, useCallback, useMemo)

김민성·2022년 4월 30일
0
post-thumbnail

React 성능최적화

리액트 프로젝트를 진행하면서 A부분이 변하는데 왜 B부분도 같이 렌더링이 되지? 라는 의문점이있었다.
리액트에서 이부분을 고려해서 어떤것을 제공해주지 않을까? 라는 의문이있었고 역시 성능 최적화 관련 hook을 제공하고 있었다.
페이스북 아니 메타 믿었다고..!

그리하여 성능 최적화 관련 Hook 3가지 React.memo, useCallback, useMemo에 대해서 정리하고자 한다.

1) 왜 하는데 최적화?

성능 최적화 hook에 들어가기에 앞서 성능 최적화를 왜 할까?
쓸때없는 렌더링을 방지하기 위해서이다. 소규모 프로젝트에서는 큰 손실이 없을지 모르지만 프로젝트가 커질수록 쓸모없는 렌더링이 메모리를 사용해 속도를 느리게 만들것이고 사용자 경험에도 영향을 미치게 된다.

2) 개념

여기서 계속 등장하는 단어가 있다. 바로 "렌더링"
간단히 말하면 결국 "리액트의 최적화" = "렌더링을 최적화 하는것"이다.

3) 렌더링 개념

리액트에서 컴포넌트가 렌더링을 언제 수행하나?
1. Props가 변경될때
2. State가 변경될때
3. forceUpdate() 를 실행했을때
4. 부모 컴포넌트가 렌더링 되었을때 자식 컴포넌트도 렌더링됨

*참고 :렌더링 과정에서 변수나 함수는 초기화 된다.

이러한 과정에서 의도치 않은 렌더링이 발생하고 최적화를 통해 이를 최소화 하는것이다.

하나의 동작으로 여러가지의 렌더링이 일어난다면 의심해볼만 하다.

최적화를 통해 의도치 않은 렌더링을 최소화 시키자!

4) React hook을 통한 최적화 방법

이 벨로그에서는 React.memo, useCallback, useMemo 3가지 hook에 대해서 알아볼텐데, 3가지 모두 memoization 개념을 활용한다.

갑자기 왜 영어 쓰냐고요? 리액트 공식문서가 쓰더라고요..

memoization : 한번 계산한거 메모리에 저장해뒀다가 똑같으면 그대로 쓰겠다는 뜻

대충 이정도면 감은 왔다. 똑같은짓 두번하기 싫다 이거구만 맘에든다.
이제 Hook을 하나하나 알아보고 사용예시도 정리해보자.

React.memo

"컴포넌트의 결과값"을 기억한다

React.memo는 고차 컴포넌트(Higher Order Component)이다.

고차컴포넌트 : 컴포넌트를 받아 새 컴포넌트를 반환하는 함수

React.memo는 아래와 같이 컴포넌트(MyComponent)를 받아
자신의 props가 바뀔때만 리렌더링 되는 고차 컴포넌트를 반환하는 함수이다.

컴포넌트가 동일한 props로 동일한 결과를 렌더링하면 React.memo는 마지막으로 렌더링 된 결과를 재사용 하는것이다.

하기와 같이 component에 사용하기도 하고 export시 사용하기도 한다.

const MyComponent = React.memo(function MyComponent(props) {
  /* props를 사용하여 렌더링 */
});
function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);

비교참고 : 비교로직 관련하여 커스터마이징을 하고싶다면 areEqeual자리에 로직을 만들어 넣어주면 된다!

자 아래의 버튼의 예시를 통해 어떻게 최적화 할지 알아보자.
아래의 코드는 Count 부모컴포넌트에 Button 자식 컴포넌트가 있는 간단한 구조이다.

import React, { useState } from 'react';
import Button from './Button';

const Count = () => {
  const [number, setNumber] = useState(0);

  return (
    <div className="wrapper">
      <div className="count" onClick={() => setNumber(number + 1)}>
        {number}
      </div>
      <Button />
    </div>
  );
};

export default Count;

위의 코드는 초록색 div를 클릭하면 숫자가 올라고 reset 버튼이 있다.

렌더링 현황은 React Developers tool를 통해 쉽게 알 수 있다.
현재 div를 클릭해서 count가 올라갈때 자식 컴포넌트인 Reset버튼도 같이 렌더링 되는걸 알수가 있다.

자식 컴포넌트인 버튼에 React.memo를 적용하여 최적화를 해보자.

import React from 'react';

const Button = () => {
  return <button>RESET</button>;
};

export default React.memo(Button);

이제는 부모컴포넌트인 count를 클릭해도 Reset이 렌더링 되지 않는다!

그럼 Reset에 count를 초기화 해주는 코드를 추가해보자.
OnClick 함수를 Button 컴포넌트에 props로 전달해 주면 어떻게 될까?

const Count = () => {
  const [number, setNumber] = useState(0);

  const onClick = () => {
    setNumber(0);
  };

  return (
    <div className="wrapper">
      <div className="count" onClick={() => setNumber(number + 1)}>
        {number}
      </div>
      <Button onClick={onClick} />  //추가!
    </div>
  );
};

밑에서 보다시피 부모컴포넌트를 클릭했음에도 버튼(자식)컴포넌트가 렌더링 되는것을 볼 수 있다.

왜 그런것일까? React.memo를 적용해 props가 변경될때만 리렌더링 되어아야하는게 정상이아닌가??

Javascript관점에서는 해당 Props는 다른 props 였기 때문이다!

객체와 얕은비교

결론만 먼저 말하면 부모의 컴포넌트가 리렌더링 되었을때 Onclick함수는 재생성 되었고 다른 props로 인식한 것이다.

부모컴포넌트가 리렌더링 될때 해당 컴포넌트의 객체(함수도 객체이니 포함)는 재생성된다.
객체는 참조형 타입이므로 주소값을 통해 비교를 하게 된다.

아래의 예를 보면 a와 b는 동일한 값을 갖는다. 하지만 a와 b는 다른 주소값을 가지므로 다른 값으로 인식한다.
하지만 c에 a를 할당했을경우 동일한 주소값을 바라보고 있으므로 같은 값으로 인식하게 된다.

const a = { 
a:"hi",
  b:"bye"
}

const b = { 
a:"hi",
b:"bye"
}

a === b //false 값은 똑같으나 다른 주소값을 가진다.

const c = a 

a === c //true 같은 주소값을 바라본다.

그럼 뭐야 React.memo 헛빵인건가..? 아니 그래서 useCallback과 useMemo라는 2가지 훅이 더 남아있다.

참고 : 그럼 "얕은비교, 깊은비교"에 대한 개념도 깊게 알면 더 좋을것 같다.
참고 블로그 https://leehwarang.github.io/docs/tech/2020-03-15-object.html

useCallback

"함수"를 기억하고 재사용한다.

위에서 리렌더링시 함수를 재생성한다고 했다. 하지만 useCallback을 사용하면 콜백함수를 메모리에 저장해두고 재사용 할 수 있다.

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

const memoizedCallback = useCallback(fn, deps)

deps(의존성배열)이 변경되었을때만 함수를 재생성하고 그렇지 않은경우는 동일한 함수(fn)를 리턴하여 사용한다.

자식 컴포넌트에게 전달했던 onClick함수에 useCallback을 적용하면 어떨까?

 const onClick = useCallback(() => {
    setNumber(0);
  }, []);

의존성 배열이 없을 경우 렌더링 마다 리렌더링을 하게될것이기 때문에 빈배열로 넣어 주었다.
아래와 같이 성공적으로 쓸대없는 리렌더링을 막을 수 있게 되었다.

UseMemo

"값"을 기억한다

UseCallback이 함수를 기억했다면 UseMemo는 값을 기억한다.
Component는 state가 변하면 리렌더링을 한다. 값이 변경되지 않았는데 리렌더링 되는것은 불필요하기 때문에 useMemo를 통해 최적화를 할 수 있다.

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

위와 같이 useMemo의 구조는 useCallback과 다를바 없다.

예를들어 버튼에 스타일값을 주고, useMemo를 사용하여 리렌더링을 방지해보자.

 const buttonStyle = useMemo(() => ({ backgroundColor: 'red' }), []);

  return (
    <div className="wrapper">
      <div className="count" onClick={() => setNumber(number + 1)}>
        {number}
      </div>
      <Button onClick={onClick} style={buttonStyle} />
    </div>
  );
};

아래와 같이 리랜더링이 일어나지 않는것을 확인할 수 있다.

예시참고 : 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

profile
다양한 경험의 개발자를 꿈꾼다

0개의 댓글