성장, 그리고 노력

부족하더라도 어제보다 더 잘해지자. 노력은 절대 배신하지 않는다.

React

[React] 리액트 (리)렌더링, React.memo

제이콥(JACOB) 2020. 1. 31. 00:55

React는 어떻게 작동할까?

먼저 아래 있는 앱과 부모, 자식 컴포넌트로 구성된 예제를 보자.

// App.js

import React, { useEffect } from "react";
import { Parent } from "./Parent";
import { Child } from "./Child";

function App() {
  console.log("자식 렌더링 시작");

  useEffect(() => {
    console.log("자식 리렌더링!");
  });

  return (
    <Parent>
      <Child />
    </Parent>
  );
}

export default App;
더보기
// 부모 컴포넌트(Parent.js)
import React, { useEffect, useState } from "react";

export const Parent = ({ children }) => {
  console.log("부모 렌더링 시작");
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    console.log("부모 리렌더링!");
  });

  return (
    <div>
      <h1>부모</h1>
      <button onClick={() => setCounter(counter + 1)}>
        부모 버튼 {counter}
      </button>
      {children}
    </div>
  );
};
// 자식 컴포넌트(Child.js)
import React, { useEffect } from "react";

export const Child = () => {
  console.log("자식 렌더링 시작");

  useEffect(() => {
    console.log("자식 리렌더링!");
  });

  return <div>자식</div>;
};

 

 위 예제는 리액트 앨리먼트(Element)의 생성 순서 및 리렌더링 흐름를 알 수 있다. 실제로 실행해보면, 아래와 같은 결과가 콘솔에 나온다.

1. 렌더(render)함수는 가상돔(React Virtual DOM) 계층에서 아래와 같이 순서대로 호출(called)된다.

* 리액트 앨리먼트의 렌더 순서
앱(App) -> 부모(Parent) -> 자식(Child)

2. 리액트 앨리먼트는 계층(hierarchy)의 맨 아래부터 반대 순서로 마운트(mount)된다.

* 리액트 앨리먼트의 마운트 순서
자식(Child) -> 부모(Parent) -> 앱(App) 

여기서 확인해야 되는 요점 중 하나는 부모 요소는 자식 요소를 생성하는 요소가 아니라는 점이다. 자식은 children prop으로 부모에게 전달 된다. 이것이 위에서 확인하고도 잘 와닿지 않는다면 부모 요소에 있는 버튼을 눌러보자.

 버튼을 아무리 클릭해도 자식은 리렌더링되지 않고 부모만 렌더링이 되는 것을 확인할 수 있을 것이다. 그렇다면 하위 요소는 어떤 것에 의해 다시 렌더링 될까? 정답은 바로 앱(App)에 의해 리렌더링된다. 

 이제 몬가 리렌더링에 대한 궁금증이 해소될거 같기도 하지만, 앱의 규모가 크거나 정확히 확인하고 싶을 때가 있을것이다. 그때는 리액트 개발자 도구를 통해 어떤 것에 의해 렌더링 되는지 확인할 수 있다.

 

Components Re-renders

"컴포넌트의 상태(state)가 변하면, 컴포넌트의 리렌더링을 발생(trigger)시킨다"

 우리는 내가 만든 리액트앱이 점점 커지면 슬슬 관심을 갖게되는 것이 바로 "컴포넌트가 언제 다시 렌더링이 되냐?"일 것이다.  물론 위에서 설명한 논리로 대부분 설명이 되겠지만, 요약하여 정리하면 아래와 같다.

 

  • 자신의 상태(state)가 변경될 때
  • 부모 컴포넌트가 리렌더링될 때
  • 자신이 전달받은 props가 변경될 때
  • forceUpdate 함수가 실행될 때

 실제 프로덕션에서 실행되는 앱은 거대한 트리처럼 수 많은 컴포넌트들이 부모-자식 관계로 복잡하게 얽혀있다. 거기서 만약 불필요한(내가 의도하지 않았고, 다시 렌더링 될 필요가 없는 요소) 렌더링이 일어난다면 이를 방지해 줘야 한다. 그래서 사용하는 방법 중에 하나가 React.memo이다.

 

React.memo

 만약 클래스형 컴포넌트를 사용한다면 React.PureComponent의 shouldComponentUpdate라는 라이프사이클을 사용하면 된다. 하지만 함수형 컴포넌트를 사용한다면, 기존 라이프사이클 메서드를 사용할 수 없기 때문에 React.memo라는 함수를 사용한다. 

 

 참고로 React.memo는 고차 컴포넌트(HOC)이다. 고차 컴포넌트는 어떤 컴포넌트를 가져와서 새로운 컴포넌트를 리턴하는 함수를 말한다. 

const EnhancedComponent = higherOrderComponent(WrappedComponent);

 

 React.memo의 사용법은 간단하다. 컴포넌트를 만들고 나서 감싸주기만 하면 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 되지 않게 할 수 있다. (props를 메모리에 저장하고 동일한 입력이 반복되서 발생하면 캐시된 출력을 반환한다.)

위에 예제 약간 수정

 위의 예제를 다시 가져오고 약간 수정하여 memo를 적용하기 전과 후를 비교해 보겠다. 예제 코드는 아래 더보기를 클릭하면 볼 수 있다.

더보기
// App.js
import React from "react";
import { Parent } from "./Parent";
import { Child } from "./Child";

function App() {
  return (
    <Parent>
      <Child />
    </Parent>
  );
}

export default App;


// Parent.js
import React, { useState } from "react";

export const Parent = ({ children }) => {
  const [counter, setCounter] = useState(0);

  return (
    <div>
      <h1>부모</h1>
      <button onClick={() => setCounter(counter + 1)}>
        부모 버튼 {counter}
      </button>
      <hr />
      {children}
    </div>
  );
};


// Child.js
import React, { useState } from "react";
import { ChildChild } from "./ChildChild";

export const Child = () => {
  const [inputText, setInputText] = useState("");

  const handleChange = event => {
    setInputText(event.target.value);
  };

  return (
    <>
      <div>자식</div>
      <input style={{ marginBottom: "0.5rem" }} onChange={handleChange} />
      <ChildChild />
    </>
  );
};


// ChildChild.js
import React, { useState } from "react";

export const ChildChild = () => {
  const [inputText, setInputText] = useState("");

  const handleChange = event => {
    setInputText(event.target.value);
  };

  return (
    <>
      <div>자식의 자식</div>
      <input onChange={handleChange} />
    </>
  );
};

 위 코드를 실행하고 "자식"에 아무거나 입력해 보자. 

리렌더링이 될때마다 하이라이트가 생긴다.

"자식"의 state가 바뀌기 때문에 자식이 리렌더링되고, "자식의 자식"은 부모 컴포넌트가 리렌더링 되기 때문에 아무것도 안했지만, 같이 리렌더링이 된다. 그리고 "자식" 컴포넌트가 모두 리렌더링 되면서 자식과 자식의 자식이라는 고정된 텍스트까지 리렌더링된다.

 

이제 리렌더링 최적화를 위해 React.memo를 사용하여 불필요한 렌더링을 개선해 보자. 아래와 같이 "자식 컴포넌트"와 "자식의 자식 컴포넌트"를 감싸면 된다.

// Child.js
export const Child = React.memo(() => {
   // 생략...
});

// ChildChild.js
export const ChildChild = React.memo(() => {
  // 생략 ...
});

그 다음에 다시 한번 "자식"에 아무런 데이터나 입력해 보자.

 어려운 부분은 없다. 다만 주의해야할 점을 리액트 공식 문서에서 이렇게 언급하고 있다.

This method only exists as a performance optimization.  Do not rely on it to “prevent” a render, as this can lead to bugs.
> React.memo를 성능 최적화 목적으로 사용해야지, 렌더링을 방지할 목적으로 사용하면 안된다. 버그를 만들 수 있다.
반응형

'React' 카테고리의 다른 글

React + Styled-components로 구현해본 아코디언(Accordion)  (1) 2020.07.26
CSS in 리액트 - Inline Style  (0) 2020.02.01
react-intl 적용하기  (1) 2020.01.11
DOM과 Virtual DOM  (2) 2019.12.13
[React Hooks] useMemo 사용하기  (0) 2019.12.10