React 렌더링과 성능향상(Memoization)

  1. 개요
  2. 렌더링이란?
    1. 리액트의 렌더링 과정
    2. 리렌더링이란?
      1. 렌더링 제외 조건
  3. 성능향상
    1. 메모이제이션
      1. React.memo
      2. useMemo
      3. useCallback
  4. 맺음

개요

리액트에서의 렌더링과 리렌더링에 대해서 자세히 알아본 정보를 공유하고, Memoization으로 사용하는 useCallback, useMemo 사용의 작은팁? 을 소개하고자합니다.

리액트에서 렌더링 과정을 정확히 알고자한다면 브라우저의 렌더링 과정과 Virtual Dom에 대해서 정확히 알아야 하지만 이번 포스트에서는 컴포넌트의 렌더링(React에서의 렌더링)에 대해서 작성하겠습니다.


렌더링이란?

렌더링을 쉽게 말하면 사용자에게 보여지는 UI 를 업데이트 하는 과정이라고 할 수 있습니다.

리액트렌더링 이미지

리액트의 렌더링은 공식문서에 의하면 두가지가 있습니다.

  • 엘리먼트 렌더링 : 엘리먼트의 변경된 부분만을 DOM 업데이트
  • 컴포넌트 렌더링 : 리액트가 컴포넌트를 실행하여 엘리먼트를 반환합니다.

여기서 중요한 부분이 리액트에 의해 컴포넌트가 호출되고 컴포넌트는 엘리먼트를 반환한 다음 DOM에 반영된다는 것입니다.

DOM을 업데이트하는 과정에서 컴포넌트의 상태 변경을 추적하고 업데이트하는 재조정(Reconciliation) 과정,
React Element Tree를 비교하여 변경사항을 찾는 Diffing 과정을 거칩니다.

React 16 이전까지는 재조정하는 방식의 한계로 React 16에 등장한 새로운 재조정 알고리즘 Fiber을 사용하여 재조정(Reconciliation)을 수행합니다. Fiber 에 대해서 따로 포스팅 하도록 하겠습니다.


리액트의 렌더링 과정

리액트의 렌더링은 3가지 단계를 거칩니다.

  1. Trigger : 고객의 주문을 주방에 전달
  2. Rendering : 주방에서 주문을 준비
  3. Commit : 손님에게 주문(결과)을 전달

리액트 렌더링 과정

1단계 : Trigger

컴포넌트가 렌더링되는 두가지 경우 가 트리거가 됩니다.

  1. 초기 렌더링
  2. 컴포넌트의 상태가 업데이트됨(부모 컴포넌트 포함)

2단계 : Render

Trigger 이후 리액트는 컴포넌트를 호출하여 화면에 표시할 항목을 결정합니다.
이 과정은 재귀적으로 컴포넌트 트리를 탐색합니다.

초기 렌더링시 root 컴포넌트를 호출하고 DOM 노드를 생성합니다.
컴포넌트의 상태 업데이트의 경우 해당 컴포넌트를 호출하여 변경된 정보를 찾고(재조정 과정) 다음 단계로 넘어갑니다.

3단계 : Commit

컴포넌트를 Rendering(호출)한 뒤 React는 DOM을 업데이트합니다.

초기 렌더링 시 DOM API의 appendChild() 를 호출하여 Root 컴포넌트DOM node를 생성합니다.
리렌더링의 경우 최소한의 작업으로 변경된 사항을 DOM에 업데이트합니다.


리렌더링이란?

리렌더링이란 렌더링 제외 기준을 만족하지 못할 경우에 발생하는 렌더링이라고 할 수 있을 것 같습니다.

렌더링 제외 조건

  1. 컴포넌트가 이미 마운트 되어있음
  2. 변경된 Props가 없음
  3. 컴포넌트에서 사용되는 Context 값 중 변경된 사항이 없음
  4. 컴포넌트 자체에서 업데이트를 예약하지않음(Fiber와 연관되어 예약이라고 표기하였습니다.)(state의 변경이 없음)

앞선 렌더링과정 중 render 과정에서 리액트는 재귀적으로 컴포넌트 트리를 탐색하여 컴포넌트를 렌더링합니다 이때 렌더링 제외조건을 만족하지 못하는 컴포넌트들이 호출되어 다시 렌더링 되는 것을 리렌더링이라고 합니다.

3번 Context의 변경사항이 없음 은 Context Api의 값을 참조하는 컴포넌트들이 값이 변경되었을 때 다시 렌더링 과정을 거칩니다. 이는 많은 상태관리 라이브러리에서 사용하는 방식입니다.


성능향상

렌더링이란 결국 변경사항을 DOM에 반영하여 UI 업데이트를 하는 과정이라서 결국에는 많이 발생할 수 밖에 없는 것같습니다.
이러한 렌더링 과정 중 불필요한 과정을 줄여서 컴포넌트 렌더링 성능을 향상 시킬 수 있습니다.

메모이제이션

대표적으로 리액트에서 제공하는 Hooks메모이제이션 HookuseMemouseCallback을 사용한 최적화 방법이 있습니다. 이 Hooks의 사용과 예시를 확인하고, 오해?를 알아보고자 합니다.

그리고 고차함수인 React.memo 또한 알아보겠습니다.

:clipboard: 예제를 준비하였는데 모든 예제는 1초마다 Parent의 state가 변경되는 예제입니다.
자식 컴포넌트는 각각 메모이제이션이 된것 과 안된 컴포넌트로 구성되어있습니다.

모든 자식 컴포넌트는 자신이 다시 렌더링이 될때마다 숫자가 증가합니다.


React.memo

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

React.memo는 고참함수로 첫번째 인수로 컴포넌트 타입을 받고,
두번째 인수로 props 의 변경을 확인하는 함수를 전달하여 리액트의 기본적인 props 비교(shallow equality (얕은 비교))를 변경 할 수 있습니다.

See the Pen 1 by upupxlhb-the-styleful (@upupxlhb-the-styleful) on CodePen.


  • 첫번째 컴포넌트 : React.memo 미사용
  • 두번째 컴포넌트 : React.memo 사용

const Child = ({ title }) => {
  const count = React.useRef(0);

  return (
    <div className="black-tile memo">
      {title}
      <p>update : {count.current++}</p>
    </div>
  );
};

const MemoChild = React.memo(Child, (prevProps, nextProps) => {
  return prevProps.title === nextProps.title;
});

useMemo

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

객체나 복잡한 계산의 결과 등의 value메모이제이션 하여 반환합니다.

  • 첫번째 인수로 값을 반환하는 함수를 전달
  • 두번째 인수로 배열을 전달합니다. 이 배열은 의존성을 부여하는 것으로 배열로 전달된 값이 변경되면 다시 메모이제이션 하여 값을 반환합니다. 만약 배열을 전달하지 않는다면 매번 다시 메모이제이션 값을 반환합니다.

See the Pen 2 by upupxlhb-the-styleful (@upupxlhb-the-styleful) on CodePen.


  • 첫번째 컴포넌트 : useMemo 사용, React.memo 미사용
  • 두번째 컴포넌트 : useMemo 사용, React.memo 사용
const Child = ({ value, title }) => {
  const count = React.useRef(0);
  const memoResult = React.useMemo(() => value * 100, [value]);

  return (
    <div className="black-tile memo">
      {title}
      <p>update : {count.current++}</p>
      <p>props value * 100 : {memoResult}</p>
    </div>
  );
};

const MemoChild = React.memo(Child);

useCallback

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

메모이제이션Callback 함수를 반환합니다.

  • 첫번째 인수로 callback 함수를 전달
  • 두번째 인수로 배열을 전달합니다. 이 배열은 의존성을 부여하는 것으로 배열로 전달된 값이 변경되면 다시 메모이제이션 하여 값을 반환합니다. 만약 배열을 전달하지 않는다면 매번 다시 메모이제이션 함수를 반환합니다.

See the Pen 3 by upupxlhb-the-styleful (@upupxlhb-the-styleful) on CodePen.


  • 첫번째 컴포넌트 : useCallback 사용, React.memo 미사용
  • 두번째 컴포넌트 : useCallback 사용, React.memo 사용
  const memoHandleClick = React.useCallback(handleClick, []);
  <Child title={"useCallback"} onClick={memoHandleClick}></Child>
  <MemoChild
    title={"useCallback and React.memo"}
    onClick={memoHandleClick}
  ></MemoChild>

React.memo와 함께 사용

여기서 작은 팁을 하나 드리고 싶습니다.
useCallbackuseMemo를 사용하면 callback 함수와 값에 대해서 메모이제이션이 되어 불필요한 자원을 낭비하지않아도 된다고 설명을 드렸는데요 여기서 하나 더 나아가 React.memo를 함께 사용해주시면 Props의 변경사항이 있을 경우 리렌더링이 발생하기때문에 더욱 효과적일 것입니다.

useCallback


맺음

이렇게 React에서 Rendering 이란 무엇인지와 과정을 알아보았습니다.
메모이제이션을 사용하여 성능향상을 도모할 수 있었지만, 반대로 성능저하를 야기할 수 있습니다.
메모이제이션을 통해 메모리에 해당 정보를 저장하기때문인데요. 무분별하게 메모이제이션을 하게 된다면
오히려 하지않았을 때 보다 성능이 저하 될 수 있습니다. 역시 뭐든지 적당히 적절히가 중요한가봐요.


Reference

  • https://velog.io/@mogulist/understanding-react-rerender-easily
  • https://velog.io/@eunbinn/when-does-react-render-your-component
  • https://ko.legacy.reactjs.org/docs/hooks-reference.html
  • https://ko.legacy.reactjs.org/docs/components-and-props.html
  • https://ko.legacy.reactjs.org/docs/reconciliation.html

:phone: 글에 대한 긍정적, 부정적 모든 피드백을 환영합니다. 말씀주시고 싶은 내용은 댓글이나 메일로 알려주시면 감사합니다!

© 2023.04 All rights reserved.

Powered by Hydejack v9.1.6