본문 바로가기
Front-End/React

[ React ] React 기본 ⑦ 최적화 - React.memo

by 2CHAE._.EUN 2022. 10. 22.

[ 컴포넌트를 재사용하기 위한 React.memo ]

 

컴포넌트가 자신과 관련 없는 부모 컴포넌트에 의해 리랜더되어 자신도 업데이트가 된다면 성능상에 문제가 된다.

낭비를 방지하기 위해 자식 컴포넌트에 조건을 걸어놔야 한다. 그러면 부모 컴포넌트가 state가 업데이트되는 등

리랜더링이 되었을 때 업데이트 조건에 따라 일치하는 자식 컴포넌트만 같이 렌더링이 된다.

 

React.memo 기능을 통해 함수형 컴포넌트에게 업데이트 조건을 걸 수 가 있다.

 

* 리액트 공식 문서 확인하기

https://ko.reactjs.org/docs/getting-started.html

 

시작하기 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

React.memo는 고차 컴포넌트이다.

 

* 고차 컴포넌트( HOC, Higher Order Component ) : 컴포넌트 로직을 재사용 하기 위한 리액트의 고급 기술

→ 고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수이다. 

( 함수를 호출해서 매개변수로 컴포넌트를 전달하면 더 좋아진 컴포넌트를 반환하는 기능이다. )

 

const MyComponent = React.memo(function MyComponent(props) {
  /* props를 사용하여 렌더링 */
});

 

React.memo는 함수이므로 함수처럼 호출이 되고 매개변수로 컴포넌트를 전달하게 되면 더 좋아진 새로운 컴포넌트를

반환하게 된다. 새로운 컴포넌트에게 똑같은 props를 바뀐 것 처럼 전달할지라도 똑같기 때문에 다시 컴포넌트를

리랜더링하지 않는다. 

React.memo()에 리랜더링되지 않았으면 하는 컴포넌트를 전달해준다면 props가 바뀌지 않는 이상 리랜더링하지

않은 강화된 새로운 컴포넌트를 돌려준다. 

* 자기 자신의 state가 변경이 된다면 리랜더링된다. 

 


[ 컴포넌트를 재사용하는 코드 작성해보기 ]

 

OptimizeTest.js는 컴포넌트를 재사용하는 실습용으로 사용한다. 

 

OptimizeText.js

import { useState } from "react";

const TextView = ({ text }) => {
  return <div>{text}</div>;
};

const CountView = ({ count }) => {
  return <div>{count}</div>;
};

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [text, setText] = useState("");

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count</h2>
        <CountView count={count} />
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>
      <div>
        <h2>text</h2>
        <TextView text={text} />
        <input value={text} onChange={(e) => setText(e.target.value)}></input>
      </div>
    </div>
  );
};

export default OptimizeTest;

 

리랜더링이 일어났을 때 Props을 확인해보기 

import { useEffect, useState } from "react";

const TextView = ({ text }) => {
  useEffect(() => {
    console.log(`text update : ${text}`);
  });
  return <div>{text}</div>;
};

const CountView = ({ count }) => {
  useEffect(() => {
    console.log(`count update : ${count}`);
  });
  return <div>{count}</div>;
};

 

useEffect의 두번째 파라미터에 아무것도 전달해주지 않은 경우는 컴포넌트가 업데이트가 되는 순간을 제어하는 것이다.

버튼을 클릭해서 count를 증가시키거나 input에 값을 입력할 경우, 부모 컴포넌트인 OptimizeTest의 count와 text

state가 변경이 되어 자식 컴포넌트인 CountView와 TextView가 랜더링이 일어나게 되어 콘솔이 출력된다.

 

 

이런 경우는 낭비가 발생한다. 왜냐하면 TextView는 test state를 부모 컴포넌트로부터 props로 받아서 랜더만 하고

있을 뿐인데 count state가 변경될 때도 같이 랜더가 되어 성능이 낭비되기 때문이다.

→ 컴포넌트 재사용 기능을 사용해서 해결할 수 있다.

 


[ 컴포넌트를 재사용할 수 있는 고차컴포넌트 React.memo 사용하기 ]

 

import React, { useEffect, useState } from "react";

const TextView = React.memo(({ text }) => {
  useEffect(() => {
    console.log(`text update : ${text}`);
  });
  return <div>{text}</div>;
});

const CountView = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`count update : ${count}`);
  });
  return <div>{count}</div>;
});

 

TextView 컴포넌트를 React.memo로 감싸주게 되면 prop인 text가 변경되지 않으면 절대로 랜더링이 일어나지 않는다.

 

 

CountView 컴포넌트에도 똑같이 적용해주면 된다. 그러면 text state만 변경될 경우는 CountView 컴포넌트는

랜더링이 되지 않아 강화된 컴포넌트로 바뀌게 된다.

 


[ 이전의 prop과 이후의 prop에 따른 결과 출력하기 ① ]

 

import React, { useEffect, useState } from "react";

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({ count: 1 });

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>Counter A</h2>
        <button onClick={() => setCount(count)}>A Button</button>
      </div>
      <div>
        <h2>Counter B</h2>
        <button onClick={() => setObj({ count: obj.count })}>B button</button>
      </div>
    </div>
  );
};

export default OptimizeTest;

 

A button을 클릭하면 원래랑 똑같은 값으로 바뀌는 버그 같은 상태 변화를 일으킨다. 

B button은 클릭하면 똑같은 count 프로퍼티를 할당한다. B 버튼을 클릭하면 setObj가 일어나면서 새로운 객체를

값으로 할당한다. count에는 obj.count를 넣어줌으로써 똑같은 값을 가진 객체를 할당하게 된다. 

 

import React, { useEffect, useState } from "react";

const CounterA = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`counter A : ${count}`);
  });

  return <div>{count}</div>;
});

const CounterB = React.memo(({ obj }) => {
  useEffect(() => {
    console.log(`counter B : ${obj.count}`);
  });
  return <div>{obj.count}</div>;
});

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({ count: 1 });

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        <button onClick={() => setCount(count)}>A Button</button>
      </div>
      <div>
        <h2>Counter B</h2>
        <CounterB obj={obj} />
        <button onClick={() => setObj({ count: obj.count })}>B button</button>
      </div>
    </div>
  );
};

export default OptimizeTest;

 

Counter A에서는 리랜더링이 일어났을 때의 count 값을 확인할 수 있고, Counter B에서는 리랜더링이 일어났을 때,

obj props의 count 프로퍼티의 값을 확인할 수 있다. Counter A, Counter B 둘다 React.memo를 통해서

강화된 컴포넌트로 만들어준다.

 

 Counter A의 결과로는 setCount에 count 값을 그대로 전달해줘서 기본값인 1이 유지된다.

1에서 1로 바뀌는 것은 변경된다고 볼 수 없기 때문에 실제로 버튼을 눌러봐도 콘솔에는 아무것도 확인할 수 없다.

React.memo 기능을 사용하기 때문에 Counter A도 랜더가 일어나지 않고, Counter B도 랜더가 일어나지 않는다.

 

반면에 Counter B의 결과는 setObj로 상태 변화를 주도하긴 하지만 count : obj.count에서 count는 사실상 1로

setCount를 하는 격이다. 그렇기 때문에 아무 일도 안일어날 것  같지만 B 버튼을 클릭하면 콘솔에 값이 출력된다.

 

 

콘솔에 출력이 됐다는 것은 리랜더링이 일어났다는 의미이다. 콘솔에 출력이 되는 이유는 props로 전달된

obj가 객체이기 때문이다. 자바스크립트에서는 객체를 비교할 때는 얕은 비교를 하기 때문에 이러한 문제가 발생한다.

 


[ 객체를 비교하는 방법 ]

 

let a = { count: 1 };
let b = { count: 1 };

if (a === b) {
  console.log("같다");
} else {
  console.log("다르다.");
}

 

a와 b의 값은 다르다고 판별이 된다. 자바스크립트는 객체, 함수, 배열 같은 비원시 타입의 자료형을 비교할 때,

값에 의한 비교가 아니라 주소에 의한 비교인 얕은 비교를 하기 때문이다.

 

a와 b 변수에 각각 객체를 할당해서 생성하게 되면 객체들은 생성되자마자 고유한 메모리 주소를 가지게 된다.

얕은 주소는 객체의 값을 비교하는 것이 아니라 두 객체가 같은 주소에 있는지를 비교하기 때문에 설령 값이

같을 지라도 다르다고 판단하게 된다.

 

let a = { count: 1 };
let b = a;

if (a === b) {
  console.log("같다");
} else {
  console.log("다르다.");
}

 

변수 b에 a의 값을 대입을 시킨다면 메모리 상에서는 b 변수가 a 변수와 같은 객체를 가리키게 된다.

 

function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);

 

 

React.memo의 두번째 인자로 areEqual이라는 함수를 받고 있다. areEqual 함수는 이전의 prop과 이후의 prop을

파라미터로 전달을 받아 두 prop이 동일한 값을 가지고 있다면 true를 반환하고, 아니라면 false를 반환한다.

즉, areEqual은 비교 함수로 사용이 되기 때문에 값이 동일해서 true를 반환하면 리랜더링을 안하고, 값이 서로 달라

false를 반환하면 리랜더링을 하도록 구현할 수 있다.

 


[ 이전의 prop과 이후의 prop에 따른 결과 출력하기 ② ]

 

Counter B 컴포넌트에서 obj prop에 대해 얕은 비교를 하지 않도록해서 랜더링 최적화를 한다.

 

const areEqual = ( prevProps, nextProps) =>{
    return true // 이전 prop과 현재 prop이 같음 -> 리랜더링 안함
    return false // 이전 prop과 현재 prop이 다름 -> 리랜더링 수행
}

 

const areEqual = ( prevProps, nextProps) =>{
    if(prevProps.obj.count === nextProps.obj.count){
        return true;
    }
    return false;
}

const MemoizedCounterB = React.memo(CounterB, areEqual);

 

areEqual 함수는 React.memo의 비교함수로써 작용을 하게 된다. 결론적으로 CounterB는 areEqual 함수의 판단에

따라서 리랜더링을 할지 말지 결정을 하게 되는 컴포넌트가 된다.

 

import React, { useEffect, useState } from "react";

const CounterA = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`counter A : ${count}`);
  });

  return <div>{count}</div>;
});

const CounterB = ({ obj }) => {
  useEffect(() => {
    console.log(`counter B : ${obj.count}`);
  });
  return <div>{obj.count}</div>;
};

const areEqual = (prevProps, nextProps) => {
  if (prevProps.obj.count === nextProps.obj.count) {
    return true;
  }
  return false;
};

const MemoizedCounterB = React.memo(CounterB, areEqual);

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({ count: 1 });

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        <button onClick={() => setCount(count)}>A Button</button>
      </div>
      <div>
        <h2>Counter B</h2>
        <MemoizedCounterB obj={obj} />
        <button onClick={() => setObj({ count: obj.count })}>B button</button>
      </div>
    </div>
  );
};

export default OptimizeTest;

 

MemoizedCounterB 컴포넌트에 obj를 prop으로 넘겨주게 된다면 count가 계속 1이기 때문에 변화가 없어 true를 

반환하기 때문에 MemoizedCounterB 컴포넌트는 리랜더링이 일어나지 않는다.

 

* areEqual 함수 다르게 작성하기

const areEqual = (prevProps, nextProps) => {
  return prevProps.obj.count === nextProps.obj.count;
};