Home 06-React.memo를-사용하여-성능-최적화-하기
Post
Cancel

06-React.memo를-사용하여-성능-최적화-하기

강의 링크


리액트는 사용자 인터페이스 구축을 위한 자바스크립트 라이브러리이다.

리액트는 컴포넌트를 효과적으로 구성하여 인터페이스를 구축한다.

업데이트 역시 컴포넌트를 통해 한다.

  • react: 컴포넌트에 신경을 쓴다.
    • props, 상태 또는 컨텍스트가 변경이 되면 이런 것들을 사용하는 컴포넌트 역시 리액트를 통해 변경된다. 재평가
    • 화면에 새로운 것을 표시하는지에 대해 확인합니다
    • 이렇게 화면에 뭔가를 그리려 한다면 리액트는 리액트 DOM에 이를 알린다.
  • react-dom: 새로운 화면과 새로운 컴포넌트가 화면에 보이도록 한다.
    • 리액트가 컴포넌트 트리를 통해 구성한 가상 DOM과 실제 DOM이 일치하도록 조작한다.
    • 리액트가 구성한 컴포넌트의 이전 상태와 트리, 그리고 현재의 상태간의 차이점을 기반으로 변경이 필요할 때만 업데이트 된다.
    • 이전과 현재의 상태를 가상으로 비교한다. (메모리 내부)


1. React.memo()

부모컨포넌트가 변경되어 재평가가 되어 함수를 재실행하면, 자식 컴포넌트 함수도 재실행한다.

즉, 컴포넌트 함수가 재실행 되면 연결된 컴포넌트 함수들도 가상 비교하면 재실행 한다. 이능 성능 이슈를 유발할 수 있다.

프로젝트가 커질 수록 최적화를 해야 하는데 이는 react.memo()로 해결할 수 있다.


1) 기본 개념

1
2
3
4
5
6
7
8
9
10
import React from 'react';

import MyParagraph from './MyParagraph';

const DemoOutput = (props) => {
  console.log('DemoOutput RUNNING');
  return <MyParagraph>{props.show ? 'This is new!' : ''}</MyParagraph>;
};

export default React.memo(DemoOutput);
  • 함수형 컴포넌트에서 작동한다.
  • React.memo(컴포넌트): 인자로 컴포넌트를 넣을 수 있다.
    • 인자로 들어간 컴포넌트에 어떤 props가 입력되는지 확인한다.
    • 해당 props의 값이 바뀐 경우에만 컴포넌트를 재 실행 및 재 평가하게 된다.
  • 즉, 부모 컴포넌트가 변경되어도 해당 컴포넌트의 prosp값이 바뀌지 않으면 컴포넌트 실행은 건너 뛴다.
  • React.memo는 props와 prevProps를 ===로 비교하여 동일한지 확인한다. 그래서 객체, 함수, 배열은 props가 변하지 않아도 같지 않다고 평가하는 이슈가 있다.
    • 이는 자바스크립트 문법 상, 원시 타입이 아닌 참조 타입 데이터에 대해 한정하는 문제이다.
    • 참조타입은 주소를 참조하고 있기 때문에, 같은 주소를 참조하지 않고 있으면 동일하다고 보지 않는다. 참조 동일성


2) 고려 사항

그렇다면, 왜 이걸 모든 컴포넌트에 적용하지 않는걸까?

최적화에는 비용이 따르기 때문이다. 리액트는 기존의 props를 저장할 공간도 필요하고 비교하는 작업이 필요하다.

React.memo()는 컴포넌트를 재평가 하는데 필요한 성능 비용과 props를 비교하는 성능 비용을 서로 맞바꾸는 것이다.

props의 개수와 컴포넌트의 복잡도 그리고 자식 컴포넌트의 숫자에 따라서 효율이 다르기 때문에 React.memo()를 사용하는 것이 무조건 성능이 좋다고 할 수는 없다.

물론, 자식 컴포넌트가 많아서 컴포넌트 트리가 매우 크다면 React.memo는 매우 유용할 수 있다.

이와는 반대로 부모 컴포넌트를 매 번 재 평가할 때마다 컴포넌트의 변화가 있거나 props의 값이 변화하는 경우라면 React.memo()는 큰 의미가 없다.

모든 컴포넌트를 React.memo()로 래핑할 필요는 없다. 그 대신, 컴포넌트 트리에서 잘라낼 수 있는 몇 가지의 주요 컴포넌트 부분을 선택해서 사용하면 된다.


2. useCalllback()

컴포넌트 실행 전반에 걸쳐 함수를 저장할 수 있게 해주는 훅이다.

리액트에게 우리는 이 함수를 저장하고 매번 실행할 때마다 이 함수를 재생성할 필요가 없다는 것을 알린다.

이렇게 되면 동일한 함수 객체가 메모리의 동일한 위치에 저장되므로 비교 작업을 할 수 있다.


1) 기본 개념

함수가 절대 변경되어서는 안된다면 useCallback을 사용해 함수를 저장하면 된다. 즉, 함수를 저장하고 이를 재사용할 수 있다.

함수가 재 정의 되는 비용을 줄여주므로 메모리 누수를 방지할 수 있고, 나아가 불필요하게 다시 렌더링 되는 컴포넌트로 인해 성능이 저하되는 문제를 해결할 수 있다.

하위 컴포넌트(예:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState, useCallback } from 'react';

import Button from './components/UI/Button/Button';
import DemoOutput from './components/Demo/DemoOutput';
import './App.css';

function App() {
  const [showParagraph, setShowParagraph] = useState(false);

  const toggleParagraphHandler = useCallback(() => {
    setShowParagraph((prevShowParagraph) => !prevShowParagraph);
  }, []);
 
  return (
    <div className="app">
      <h1>Hi there!</h1>
      <DemoOutput show={false} />
      <Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
    </div>
  );
}

export default App;
  • useCallback(함수, 의존성 배열)
  • 항상 같은 함수 객체가 사용되게끔 한다.
  • useCallback()을 사용하면 props가 함수인 경우에도 useMemo()를 사용할 수 있다.


2) 의존성 배열

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { useState, useCallback } from 'react';

import Button from './components/UI/Button/Button';
import DemoOutput from './components/Demo/DemoOutput';
import './App.css';

function App() {
  const [showParagraph, setShowParagraph] = useState(false);
  const [allowToggle, setAllowToggle] = useState(false);

  // 의존성 배열로 []를 하게 되면, 함수 내부에서 사용되는 allowToggle 값은 초기 렌더링 때의 값으로 고정된다. 
  const toggleParagraphHandler = useCallback(() => {
    if (allowToggle) {
      setShowParagraph((prevShowParagraph) => !prevShowParagraph);
    }
  }, [allowToggle]);

  const allowToggleHandler = () => {
    setAllowToggle(true);
  };

  return (
    <div className="app">
      <h1>Hi there!</h1>
      <DemoOutput show={showParagraph} />
      <Button onClick={allowToggleHandler}>Allow Toggling</Button>
      <Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
    </div>
  );
}

export default App;
  • 자바스크립트의 함수는 클로저이다.
  • 즉, 함수가 정의되면 함수가 정의될 때 함수 블록에서 사용되는 모든 변수를 잠그게 된다.
  • 만약, 의존성 배열을 따로 추가 하지 않는다면 위의 toggleParagraphHandler함수 내부에서 사용되는 allowToggle변수 값은 false로 고정된다.


3. useMemo()

useCallback이 함수에 대한 것을 저장하듯, UseMemo는 모든 종류의 데이터를 저장할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { useMemo } from 'react';

import classes from './DemoList.module.css';

const DemoList = (props) => {
  const { items } = props;

  const sortedList = useMemo(() => {
    console.log('Items sorted');
    return items.sort((a, b) => a - b);
  }, [items]); 
  console.log('DemoList RUNNING');

  return (
    <div className={classes.list}>
      <h2>{props.title}</h2>
      <ul>
        {sortedList.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default React.memo(DemoList);
  • useMemo(함수, 의존성 배열)

  • 이 함수가 저장하고 싶은 것을 반환한다.

  • useMemo()는 물론 데이터 재계산과 같은 성능 집약적 작업 때문에 데이터를 저장해야 할 필요도 있지만 이런 경우가 아니면 별로 사용하지 않는다.

  • useMemo()를 사용해 데이터를 저장하면 이는 메모리를 사용하는 것이고 이런 함수 저장 또한 일정 성능을 사용하는 것을 잊으면 안된다.


This post is licensed under CC BY 4.0 by the author.