[React] React.memo()를 알아보자.

ZenTechie·2023년 5월 15일
0

React

목록 보기
2/3

이번에는 React.memo()가 무엇이고 어떤 역할을 하는지 코드를 통해 알아보자.

전체적인 구조

App.js

import React, { useState } 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);

  console.log("APP RUNNING!");
  
  const toggleParagraphHandler = () => {
    setShowParagraph(prevShowParagraph => !prevShowParagraph);
  }

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

export default App;

Button.js

import React from 'react';

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

const Button = (props) => {
  console.log("BUTTON RUNNING!");
  return (
    <button
      type={props.type || 'button'}
      className={`${classes.button} ${props.className}`}
      onClick={props.onClick}
      disabled={props.disabled}
    >
      {props.children}
    </button>
  );
};

export default Button;

DemoOutput.js

import React from "react";

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

export default DemoOutput;

실행 화면

먼저 초기 화면은 다음과 같다.

컴포넌트들 간의 관계

  • 부모 : App.js
  • 자식 : Button.js, DemoOutput.js

작동 방식

먼저, 초기 렌더링 시에 다음과 같이 콘솔에 출력된다.

"APP RUNNING!"
"DEMOOUTPUT RUNNING!"
"BUTTON RUNNING!"

자 이제 버튼을 눌러보자. 콘솔에 어떻게 출력이 될까?

"APP RUNNING!"
"DEMOOUTPUT RUNNING!"
"BUTTON RUNNING!"

똑같이 출력이된다..! 원래대로라면 "DEMOOUTPUT RUNNING"은 출력되면 안된다.
props로 전달되는 show의 값이 변하지 않기 때문이다.

그런데 왜 출력이 되는걸까?

App 함수는 상태가 변경되었기 때문에 재실행된다. App 내부에는 뭐가 있는가?
당연히 반환문이 있고 이것은 JSX 코드를 반환한다.
여기에 있는 모든 JSX 요소들은 결국 컴포넌트 함수에 대한 함수 호출과 같다.

그러니까, DemoOutput 컴포넌트에 대한 함수를 호출하고 Button 컴포넌트에 대한 함수를 출력한다.

부모 컴포넌트들이 변경되었고 자식 컴포넌트는 부모 컴포넌트의 일부이기 때문에, 자식 컴포넌트들 역시 다시 실행되고 재평가된다.

결국, props의 값은 상관이 없다는 의미이다.

기억해야 될 것은, 부모 컴포넌트가 재실행되면 이의 모든 자식 컴포넌트들 역시 재실행, 재평가 된다.


그렇다면, 모든 자식 컴포넌트들을 재실행, 재평가하면 낭비가 아닐까?

간단한 어플리케이션에서는 전혀 문제가 되지 않지만, 더 큰 어플리케이션이라면 최적화가 필요하다.
따라서, 우리는 특정한 상황일 경우에만 자식 컴포넌트가 재실행하도록 리액트에 지시를 해야한다.
(여기서 특정한 상황은 props가 변경되었을 때를 의미한다.)

그렇다면, 어떻게 하면 위와 같이 행동하도록 명령할 수 있을까?

React.memo()를 사용하자.

일단, 특정 상황에서만 재실행되도록 하고자 하는 자식 컴포넌트를 지정한 뒤에 React.memo()로 감싸주면 된다.

코드로 살펴보자.

DemoOutput.js

import React from "react";

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

export default React.memo(DemoOutput);

컴포넌트를 export하는 행에 React.memowrap을 하면 된다.

결과는 다음과 같다.

"APP RUNNING!"
"BUTTON RUNNING!"

짜잔, "DEMOOUTPUT RUNNING!" 문구가 출력되지 않았다. 성공적이다!

이렇듯 React.memo를 사용하면 특정 상황에만 컴포넌트가 재실행, 재평가되도록 할 수 있다.
하지만 염두해야할 것은, React.memo함수형 컴포넌트에만 가능하다는 것이다.
클래스 기반의 컴포넌트의 경우 React.memo작동하지 않는다.

ex. 클래스 기반의 DemoOutput.js

class DemoOutput extends Component {
  render () {
    return ( ... );
  }
}

export default DemoOutput;

React.memo의 역할

그렇다면 React.memo의 역할은 무엇인가?
React.memo인자로 들어간 컴포넌트어떤 props가 입력되는지 확인하고, 입력되는 모든 props의 최신(신규)값을 확인한 뒤 이를 기존의 props의 값과 비교하도록 리액트에게 전달한다.

그리고 props값이 바뀐 경우에만 컴포넌트를 재실행 및 재평가하게 된다.
그리고 부모 컴포넌트가 변경되었지만 자식 컴포넌트의 props 값이 바뀌지 않았다면, 자식 컴포넌트의 실행은 건너뛴다.

또한, 위의 상황에서 자식 컴포넌트안의 자식 컴포넌트실행이 되지 않는다.

생기는 의문점..

그렇다면 React.memo를 사용하면 불필요한 낭비를 막을 수 있는데,

왜 모든 컴포넌트에 적용하지 않을까?

그 이유는, 최적화 또한 비용이 따르기 때문이다.
React.memo는 자식 컴포넌트를 가지는 부모 컴포넌트에 변경이 발생할 때 마다 자식 컴포넌트로 이동하여 기존 props 값과 새로운 값을 비교하게 한다.

그렇다면 리액트가 2가지 작업을 할 수 있어야 한다.

  1. 먼저 기존의 props 값을 저장할 공간이 필요하다.
  2. 또한, props비교하는 작업도 필요하다.

이 각각의 작업은 개별적인 성능 비용이 필요하다.
따라서, 성능의 효율어떤 컴포넌트를 최적화하느냐에 따라 달라진다.

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

그리고 이는 props의 개수컴포넌트의 복잡도, 그리고 자식 컴포넌트의 개수에 따라 달라지므로 어느 쪽의 비용이 더 높다고는 판단하기가 어렵다.

만약, 자식 컴포넌트가 많아서 컴포넌트 트리가 매우 크다면, React.memo매우 유용하게 쓰인다. 그리고 컴포넌트 트리의 상위에 위치해있다면 전체 컴포넌트 트리에 대한 쓸데없는 재렌더링을 막을 수 있다.

(필독) 많은 개발자들이 실수하는 것!!!

예시 코드에서 버튼이 변경될 일이 없다는 건 자명하기 때문에, 이를 재평가하는 것은 가치가 없다.
그렇다면, 이번에는 Button.jsReact.memo를 적용해보자.

import React from 'react';

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

const Button = (props) => {
  console.log("BUTTON RUNNING!");
  return (
    <button
      type={props.type || 'button'}
      className={`${classes.button} ${props.className}`}
      onClick={props.onClick}
      disabled={props.disabled}
    >
      {props.children}
    </button>
  );
};

export default React.memo(Button);

결과는 어떻게 될까?

"APP RUNNING!"
"BUTTON RUNNING!"

예상과는 다르게 "BUTTON RUNNING!"이 출력된다. 왜 이럴까?

출력이 된다는 말은 Buttonprops 값이 계속 바뀐다는 뜻이다.
<Button>을 살펴보면 onClick이라는 props 하나밖에 없다.

실행되는 함수는 항상 같은 것인데, memo가 동작하지 않는 이유리액트에서 흔하게 발생하는 오류 중 하나이기 때문이다.

App 컴포넌트는 어쨌든 간에 하나의 함수이기 때문에, 일반적인 자바스크립트 함수처럼 재실행된다.
이때 조금 다른 것은, 함수가 사용자가 아닌 리액트에 의해 호출된다는 것이다.
그렇지만 여전히 일반 함수처럼 실행되는데, 이 말은 즉슨 App 내부의 모든 코드가 다시 실행된다는 의미이다.

버튼에 전달되는 함수도 매번 재생성된다. 이는 App 함수의 모든 렌더링, 또는 모든 실행 사이클에서 완전히 새로운 함수로 취급된다.(재사용하지 않음 ❌)

왜 새로운 함수로 취급되냐하면, 매번 다시 만드는 상수이기 때문이다.

분명 App 내부의 모든 코드가 다시 실행된다고 했다.
그렇다면 이 상수도 새로 만들어지고 이 말은 새로운 함수가 만들어진다는 의미이다.

기능은 똑같지만, 이전과는 다른 새로운 함수인 것이다.

어? 그렇다면 아까 DemoOutput의 props에 전달된 false도 재생성되는 것 아닌가?
그러면 React.memo가 왜 동작하는걸까? false와 함수가 다른 점은 뭘까?

자, falseboolean 타입이고 자바 스크립트에서 boolean 값은 원시 값이라고 칭한다.

예시 코드에서 React.memoprops의 값을 확인하고 props.show를 직전의 값인 props.previous.show와 비교한다. (실제로 이렇게 작동하는 것 아님 ❌, 설명을 위해...)

이 비교는 일반 비교 연산자를 통해 진행한다. 그렇기 때문에 false === false 로 비교해서 true가 나오기 때문에 이 둘은 같은 값이다.
(기술적으로는 서로 다른 boolean이지만, 이렇게 비교가 가능하다.)

하지만 배열이나 객체, 함수를 비교한다면 살짝은 달라진다.

자바스크립트에서 함수하나의 객체에 불과하다.
따라서, App 함수가 실행될 때 마다 새로운 함수 객체가 생성이 되고 이 함수 객체가 DemoOutputonClick props로 전달이 된다.

이렇게 되면, props.onClickprops.previous.onClick을 비교하게 되는 것이다.
이 두 함수 객체가 완전히 똑같은 기능을 하더라도 자바스크립트에서는 절대 동일하지 않다.

이러한 자바스크립트의 동작 방식 때문에, React.memo를 사용해도 값이 변경되었다고 인식하는 것이다.

정말 정말 중요한 개념이다. 꼭 알아두자!
그렇다면, React.memo를 사용해도 객체나 배열 또는 함수에는 사용할 수가 없는걸까..?

요약

React.memo를 사용하면 자식 컴포넌트의 불필요한 재렌더링을 막을 수 있다!
하지만 최적화는 비용이 따르기 때문에 항상 적용하지 않는다.

자식 컴포넌트의 변화가 정말 불필요한지, 프로젝트 규모 등을 고려해서 적절하게 사용해야 한다.

또한, 배열, 객체, 함수에서는 React.memo는 동작하지 않는다.

profile
데브코스 진행 중.. ~ 2024.03

0개의 댓글