React.memo

김민기·2022년 12월 4일
0

React

목록 보기
8/8

HOC

React에서 제공하는 React.memo는 HOC, 고차 컴포넌트다. 고차 컴포넌트란, 어떤 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환해주는 함수다.

memo의 필요성

리액트에서 컴포넌트는 Props, 또는 State에 의해서 자주 렌더링된다. 컴포넌트가 복잡한 로직을 포함하고 있다면 전체적인 성능 저하에 영향을 미친다.

따라서 불필요한 렌더링을 최소화 해주는 것이 리액트 컴포넌트의 성능 최적화에 도움이된다.

사실상 리액트 자체의 성능이 좋기 때문에 리렌더링 횟수에 대해서 크게 고민할 필요는 없지만 앱의 크기가 커질 수록 또 하나의 컴포넌트에서 처리하는 로직이 무거울 수록 성능에 부담을 줄 수 있다.

이전에는 useMemo, useCallback을 사용해서 Memoization을 통해 불필요하게 반복되는 계산을 막거나 렌더링 될 때마다 불필요하게 다시 함수 객체가 생성되는 것을 막는 방법에 대해 배웠다.

이번에는 React에서 제공하는 HOC, memo를 사용해서 컴포넌트의 성능 최적화를 만들어본다.

React.memo

React.memo는 오직 Props 변화에만 의존하는 최적화 방법이다.

React.memo는 인자로 전달 받은 컴포넌트의 Prop Check를 통해서 Props의 변화가 있는지 확인하고 변화가 있다면 컴포넌트를 렌더링 하고 변화가 없을 경우 기존에 렌더링된 내용을 재사용한다.

주의할점은 props에만 의존한다는 것이고 컴포넌트 내부에서 useState, useReducer, useContext와 같은 훅을 사용해서 상태관리를 한다면 props의 변화와 관계없이 다시 렌더링 될 수 있다.

컴포넌트의 렌더링

기본적으로 부모 컴포넌트에서 렌더링이 발생하면, 자식 컴포넌트 또한 렌더링된다.

const Parent = () => {
  ...
  return (
    <div>
    	<Child/>
    </div>
  )
}

Parent 컴포넌트에서 상태가 변경된다면 Parent 컴포넌트는 리렌더링이 발생되고 Child 컴포넌트 또한 리렌더링 된다. 만약 Parent 컴포넌트에서 Child 컴포넌트로 특정 props를 전달하는데, 전달해주는 props이 변경되지 않는다면 Child 컴포넌트는 다시 렌더링될 필요가 없다. 불필요한 리렌더링이 발생한다는 것이다
React.memo를 사용하면 이런 불필요한 리렌더링을 막을 수 있게된다.

적용해보기

// Parent Component
const { useState } = require("react");
const { default: Child } = require("./Child");

function App() {
  const [parentAge, setParentAge] = useState(0);
  const [childAge, setChildAge] = useState(0);

  const incrementParentAge = () => {
    setParentAge(parentAge + 1);
  };

  const incrementChildAge = () => {
    setChildAge(childAge + 1);
  };
  console.log("Parent Component Rendered!");

  return (
    <div style={{ border: "2px solid navy", padding: "10px" }}>
      <h1> 👨‍👩‍👦 Parent </h1>
      <p>age: {parentAge}</p>
      <button onClick={incrementParentAge}>Parent Age increase</button>
      <button onClick={incrementChildAge}>Child Age increase</button>
      <Child name={"Hong"} age={childAge} />
    </div>
  );
}

export default App;
// Child Component
import { memo } from "react";

const Child = ({ name, age }) => {
  console.log("Child Component Rendered");
  return (
    <div style={{ border: "2px solid powderblue", padding: "10px" }}>
      <h3> 👶Child </h3>
      <p>name: {name} </p>
      <p>age: {age} </p>
    </div>
  );
};

export default memo(Child);

memo를 사용하는 방법은 간단하다 react에서 memo를 import 하고 컴포넌트를 감싸면 끝

만약 memo를 사용하지 않는다면 부모 컴포넌트인 App에서 부모의 나이를 증가시킬 경우 상태가 변하기 때문에 다시 렌더링되고 자식 컴포넌트의 프롭으로 전달되는 상태는 변경되지 않았음에도 렌더링 된다. 하지만 memo를 사용했기 때문에 부모의 나이가 증가한다고 해서 자식 컴포넌트까지 렌더링 되지는 않게된다.

memo를 통해 최적화된 Child 컴포넌트는 렌더링될 상황에 놓일 때마다 Prop Check를 한다. 컴포넌트가 받는 Props에 변화가 있을 때만 렌더링을 허락해주고 만약 변화가 없다면 렌더링을 하지 않고 이전에 렌더링된 컴포넌트 결과를 재사용한다.

useMemo와 같이 사용하기

memo를 사용해서 props이 변화하지 않으면 컴포넌트가 리렌더링되지 않도록 만들었다. 하지만 객체 타입의 값을 전달할 경우 문제가 발생한다.

// Parent
const { useState, useMemo } = require("react");
const { default: Child } = require("./Child");

function App() {
  const [parentAge, setParentAge] = useState(0);

  const incrementParentAge = () => {
    setParentAge(parentAge + 1);
  };

  console.log("Parent Component Rendered!");
//  const name = { lastName: "Hong", firstName: "KillDong" };
	const name = useMemo(()=>({ lastName: "Hong", firstName: "KillDong" }),[]);
  return (
    <div style={{ border: "2px solid navy", padding: "10px" }}>
      <h1> 👨‍👩‍👦 Parent </h1>
      <p>age: {parentAge}</p>
      <button onClick={incrementParentAge}>Parent Age increase</button>
      <Child name={name} />
    </div>
  );
}

export default App;

주석으로 처리한 name 객체를 Child 컴포넌트에 전달했을 때, name 객체는 변경되지 않는 것처럼 보이지만 name이라는 변수에는 객체의 메모리 주소를 저장하고 있고 App 컴포넌트가 리렌더링 될 때 새롭게 객체가 할당되면서 메모리 주소가 변경되기 때문에 props에 변화가 있다고 여기고 Child 컴포넌트가 리렌더링 된다.

하지만 useMemo를 사용해서 name 변수에 할당되는 객체 값을 캐싱해두었고, 의존성 배열이 비어있기 때문에 처음 렌더링되었을 때 값을 그대로 사용한다. 때문에 App 컴포넌트가 리렌더링 되더라도 name 객체를 다시 할당하지 않기 때문에 props의 변화가 없는 것으로 여겨져서 Child 컴포넌트가 리렌더링 되지 않는다.

useCallback과 같이 사용하기

// Parent
const { useState } = require("react");
const { default: Child } = require("./Child");

function App() {
  console.log("Parent Component Rendered");
  const [parentAge, setParentAge] = useState(0);

  const incrementParentAge = () => {
    setParentAge(parentAge + 1);
  };

//  const tellMe = () => {
//    console.log("I will Kill you");
//  };

	const tellMe = useCallback(() => {
    console.log("I Love you");
  }, []);

  return (
    <div style={{ border: "2px solid navy", padding: "10px" }}>
      <h1> 👨‍👩‍👦 Parent </h1>
      <p>age: {parentAge}</p>
      <button onClick={incrementParentAge}>Parent Age increase</button>
      <Child name={"Hong"} tellMe={tellMe} />
    </div>
  );
}

export default App;

이번에는 props으로 함수를 전달하는 상황이다. 함수 또한 자바스크립트에서는 객체이기 때문에 초기화가 이루어질 때마다 변수에는 메모리 주소가 할당된다. 마찬가지로 리렌더링 될 때마다 새로운 함수 객체가 할당되기 때문에 props으로 전달하는 함수는 바뀌는 것으로 인식되어 Child 컴포넌트 또한 리렌더링 된다. 하지만 useCallback을 사용해서 함수 객체를 캐싱해 두었기 때문에 App 컴포넌트가 리렌더링 된다고 해서 Child 컴포넌트가 리렌더링 되지는 않는다.

0개의 댓글