React 알아가기 (8)

삔아·2023년 6월 2일
0
post-thumbnail

해당 내용은 https://www.udemy.com/course/best-react/ 강의를 들으며 정리하고 스스로 공부 한 내용을 기록 하였습니다.

지난 포스팅에서 출력 결과가 바뀌는 것이 없음에도 불구하고 모든 자식 컴포넌트를 재실행하는 것은 낭비라고 말했는데, 이번 포스팅에서 불필요한 재평가를 방지하는 방법인 React.memo() 에 대해 알아보도록 하자.

About React.memo

React.memo 는 최적화 도구다. 이는 함수형 컴포넌트을 최적화 할 수 있는데 이 때, 클래스 기반의 컴포넌트 에는 작동하지 않는다.

React.memo 는 인자로 들어간 컴포넌트에 어떤 props가 입력되는지 확인하고 입력되는 모든 props의 신규 값을 확인한 뒤 이를 기존의 props의 값과 비교하도록 리액트에게 전달한다.
그리고 props의 값이 바뀐 경우에만 컴포넌트를 재실행 및 재평가 한다.
이 때, 부모 컴포넌트가 변경 되었지만 그 컴포넌트의 props 값이 바뀌지 않았다면 컴포넌트 실행을 건너뛴다.

React.memo(Component, [equal(prevProps, nextProps)]);

props 혹은 props의 객체를 비교 할 때 얕은 비교를 사용하게 되는데, 비교방식을 수정하고 싶다면 두번째 매개변수에 비교함수를 만들어 넣어주면 된다.

equal(prevProps, nextProps) 함수는 prevProps와 nextProps가 같다면 true를 반환할 것이다.

불필요한 재렌더링을 피하기 위해 최적화가 이루어지고 있는데, 왜 모든 컴포넌트에 React.memo 를 적용하지 않는지에 대한 의문도 들 것이다.

답은 간단하다. 최적화에는 비용이 따르기 때문이다.

React.memo 는 App에 변경이 발생할 때 마다 이 컴포넌트로 이동하여 기존 props의 값과 새로운 값을 비교한다.

즉, 리액트가 두가지 작업을 할 수 있어야 한다.
1. 먼저 기존의 props 값을 저장할 공간이 필요하고,
2. 비교하는 작업 또한 필요하다는 것이다.

이 각각의 작업은 개별적인 성능 비용이 필요하게 되기 때문에 어떤 컴포넌트를 최적하느냐에 따라 성능 효율이 달라지게 된다.

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

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

만약 자식 컴포넌트가 많아서 컴포넌트 트리가 매우 크거나 컴포넌트 트리의 상위에 위치 해있다면 전체 컴포넌트 트리에 대한 쓸데없는 재렌더링을 막을 수 있기 때문에 이런 상황에서는 매우 유용하게 쓰이기도 할 것이다.

그러나 부모 컴포넌트를 매번 재평가 할 때 마다 컴포넌트의 변화가 있거나 props의 값이 변화 할 수 있는 경우라면 컴포넌트의 재렌더링이 어떻게든 필요하기 때문에 React.memo 는 크게 의미를 갖지 못할 것 이다.

💡 즉 모든 컴포넌트를 React.memo 로 래핑 할 필요는 없다.

그 대신, 컴포넌트 트리에서 잘라낼 수 있는 몇가지 주요 컴포넌트 부분을 선택해서 사용하면 된다.

React.memo 를 이용하여 다른 예시를 확인해보자. 이는 Button 컴포넌트 이다.

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.js
...
// 이 때, changeTitleHandler 는 함수 이다.
<Button onClick={changeTitleHandler}>Change List Title</Button>
...

이렇게 사용 하였을 때, Button 컴포넌트를 React.memo 로 래핑하였음에도 불구하고 Button RUNNING 콘솔이 자꾸 찍히는 것을 확인 할 수 있다.

이는 props의 값이 계속 바뀌고 있다는 뜻인데, 위 코드를 확인하여 알 수 있듯이 props로 넘겨주는 것은 onClick 으로 넘겨주고 있는 changeTitleHandler 함수 뿐이다.

왜 이런 현상이 일어나게 되는걸까?

이 App 컴포넌트는 리액트에 의해 호출되는 함수로 일반적인 자바스크립트 함수처럼 재실행 된다.
일반 함수처럼 실행 된다는 말은 모든 코드가 다시 실행 된다는 의미다.

버튼에 전달되는 함수는 매번 재생성 된다. App 함수의 모든 렌더링 또는 모든 실행 사이클 에서 완전히 새로운 함수다.

코드가 다시 실행되면서, 새로운 함수도 다시 만들어진다. 재실행 하기 전의 함수와 재실행 한 후의 함수는 같은 함수가 아닌 같은 기능을 하는 새로운 함수 인 것이다.

그러면 React.memo 가 잘 작동되는 컴포넌트와 그렇지 않은 컴포넌트의 차이는 무엇일까.

바로 위에서 말한 부분에서 정답이 있다.

props 혹은 props의 객체를 비교 할 때 얕은 비교를 사용한다.

이 메서드가 최종적으로 하는 일은 props의 값을 확인하고, 현재의 props와 직전 값인 previousProps 를 비교하는 것인데,

예를 들어 props.onClick === props.previous.onClick // false

실제로 내부적으로 이렇게 쓰이는 것은 아니다. 그저 예를 들어 이런 식이라고 생각하면 되는데,

props.show의 이전 값을 확인하고 현재의 값과 비교하는데 이 작업은 일반 비교 연산자를 통해 한다.

만약 이 값이 원시값이라면 이게 true가 나올 것이다. ( 자바스크립트의 원시 값: number, string, null, undefiend, boolean, bigint, symbol )

그러나 배열이나 객체, 함수를 비교하면 달라진다.

[1, 2, 3] === [1, 2, 3] // false

사람의 눈에는 같아보이지만 자바스크립트에서 이 둘은 같지 않다.

이 부분에 대한 자세한 내용은 Reference vs Primitive Values 참고하면 좋다.

💡 자바스크립트에서 함수는 하나의 객체에 불과하다.

이 App 함수가 실행될 때 마다 새로운 함수 객체가 생성이 되고 함수 객체가 onClick 에 전달이 되는데, 이렇게 되면 버튼 컴포넌트는 props.onClickprops.previous.onClick을 비교하는 셈이 된다.

그리고 두 함수 객체는 같은 내용을 가지고 있다 해도 앞에 말했듯이 자바스크립트에서 둘을 비교하면 동일하지 않게 된다. 이러한 작동방식 때문에 React.memo 는 값이 변경되었다고 인식하게 된다.

그러면 React.memo 는 props를 통한 객체나 배열 또는 함수를 가져오는 컴포넌트에는 사용할 수 없는 것인가 라는 의문점이 또다시 들게 된다.
물론 해결방법 역시 있다.

profile
Frontend 개발자 입니다, 피드백은 언제나 환영 입니다

0개의 댓글