4 분 소요

1. React.memo란

리액트에서는 부모 컴포넌트가 렌더링되면 모든 자식 컴포넌트들도 렌더링된다.

부모 컴포넌트로부터 넘겨받는 props가 업데이트 될 때만 렌더시킬 순 없을까? 그럼 더 효율적일 텐데…

이때 리액트에서 제공하는 React.memo를 사용하면 된다.

React.memo는 리액트에서 제공하는 고차 컴포넌트(HOC, Higher Order Component)이다. 고차 컴포넌트란, 어떤 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환해주는 함수를 말한다. React.memo라는 고차 컴포넌트에 어떤 컴포넌트를 인자로 넣으면 UI와 기능 면에서는 똑같지만 보다 최적화된 컴포넌트를 반환해준다.

최적화된 컴포넌트는 렌더링이 되어야 할 상황에 놓일 때마다 prop check를 통해, 자신이 받는 props에 변화가 있는지 없는지 확인하게 된다. 변화된 내용이 없다면 렌더링하지 않고 기존의 내용을 재사용한다.

React.memo는 꼭 필요할 때만 사용해야 한다! 왜냐하면 컴포넌트를 메모아이즈 할 때 렌더링된 내용을 어딘가에 저장해두어야 하기 때문에 메모리가 필요하기 때문이다. 따라서,

  1. 컴포넌트가 같은 props로 자주 렌더링될 때,
  2. 컴포넌트가 렌더링될 때마다 복잡한 로직을 처리해야 할 때 사용한다.


기억해야 할 점은, React.memo는 오직 props 변화에만 의존하는 최적화 방법이라는 것이다.

따라서 useState, useReducer, useContext와 같은 상태와 관련된 훅을 사용한다면 props에 변화가 없더라도 state나 context가 변할 때마다 다시 렌더될 것이다.




2. 예제 1. React.memo 사용해보기

Parent 컴포넌트

import Child from "./Child";
import { useState } from "react";

const Parent = () => {
  const [parentAge, setParentAge] = useState(0);
  const [childAge, setChildAge] = useState(0);

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

  const incrementChildAge = () => {
    setChildAge(childAge + 1);
  };

  console.log("부모 컴포넌트 렌더링!");

  return (
    <div style=>
      <h1>부모</h1>
      <p>age: {parentAge}</p>
      <button onClick={incrementParentAge}>부모나이 증가</button>
      <button onClick={incrementChildAge}>자식나이 증가</button>
      <Child name={"홍길동"} age={childAge} />
    </div>
  );
};

export default Parent;


Child 컴포넌트

const Child = ({ name, age }) => {
  console.log("자녀 컴포넌트 렌더링!");

  return (
    <div style=>
      <h3>자녀</h3>
      <p>name: {name}</p>
      <p>age: {age}</p>
    </div>
  );
};

export default Child;

이때 부모나이 증가 버튼을 눌러, 자식 컴포넌트에는 업데이트된 것이 아무것도 없는데도 Child 컴포넌트까지 모두 다시 렌더링된다. Child 컴포넌트는 props로 받는 name과 age가 변경되지 않는다면 다시 렌더될 필요가 없다.

이때 React.memo를 사용해주면, props에 업데이트 사항이 있을 때에만 자식 컴포넌트가 렌더링 된다. memo 사용 방법은 매우 쉽다.

  1. memo를 import 해주고
  2. HOC인 memo의 인자에 자식 컴포넌트를 넣어준다.


import { memo } from "react";

const Child = ({ name, age }) => {
  console.log("자녀 컴포넌트 렌더링!");

  return (
    <div style=>
      <h3>자녀</h3>
      <p>name: {name}</p>
      <p>age: {age}</p>
    </div>
  );
};

export default memo(Child);

memo를 사용해주면, 부모 컴포넌트에서 parentAge 스테이트가 변경되더라도, 그 스테이트가 프롭스로 자식 컴포넌트에 넘겨지지 않기 때문에 자식 컴포넌트는 렌더되지 않는다. 부모 컴포넌트만 새로 렌더되고, 자식 컴포넌트는 렌더되기 직전에 prop check를 통해 프롭스에 변화가 있는지 확인하게 된다. 프롭스에 변화가 없다면 렌더하지 않고 기존에 저장해둔 값을 그대로 사용한다.




3. 예제 2. useMemo, useCallback과 함께 사용해보기

3.1. props로 객체를 넘겨줄 때 (useMemo)

Parent 컴포넌트

import Child from "./Child";
import { useState } from "react";

const Parent = () => {
  const [parentAge, setParentAge] = useState(0);

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

  console.log("부모 컴포넌트 렌더링!");

  const name = {
    lastName: "",
    firstName: "길동",
  };

  return (
    <div style=>
      <h1>부모</h1>
      <p>age: {parentAge}</p>
      <button onClick={incrementParentAge}>부모나이 증가</button>
      <Child name={name} />
    </div>
  );
};

export default Parent;


Child 컴포넌트

import { memo } from "react";

const Child = ({ name }) => {
  console.log("자녀 컴포넌트 렌더링!");

  return (
    <div style=>
      <h3>자녀</h3>
      <p>: {name.lastName}</p>
      <p>이름: {name.firstName}</p>
    </div>
  );
};

export default memo(Child);

React.memo를 통해 컴포넌트 최적화를 했음에도 불구하고, parentAge 스테이트만 변경되었을 뿐인데 Child 컴포넌트에게 넘겨준 name이 변하지 않았는데도 부모와 자녀 컴포넌트가 같이 렌더된다.

그 이유는 name이 객체이기 때문이다. 객체는 원시값과는 달리 변수에 저장될 때 그대로 저장되는 것이 아니라 객체가 저장된 메모리의 주소가 저장된다. 컴포넌트 함수가 새로 렌더될 때 함수 안에 있는 모든 변수들은 초기화되기 때문에, 객체를 값으로 갖는 name 변수도 초기화되는 것이다. 즉, 부모 컴포넌트가 렌더될 때마다 새로운 객체가 만들어지게 되고, 만들어진 객체들은 각각 다른 메모리 주소에 저장된다. 따라서 렌더링 될 때마다 name 변수 안에는 다른 메모리 주소가 들어가게 되고, React.memo가 prop check를 할 때 변화를 감지하게 되는 것이다.


그럼 useMemo를 통해 name 오브젝트를 메모이제이션 해주자.

import Child from "./Child";
import { useState, useMemo } from "react";

const Parent = () => {
  const [parentAge, setParentAge] = useState(0);

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

  console.log("부모 컴포넌트 렌더링!");

  const name = useMemo(() => {
    return {
      lastName: "",
      firstName: "길동",
    };
  }, []);

  return (
    <div style=>
      <h1>부모</h1>
      <p>age: {parentAge}</p>
      <button onClick={incrementParentAge}>부모나이 증가</button>
      <Child name={name} />
    </div>
  );
};

export default Parent;

React.memo와 useMemo를 같이 사용하면, props로 전달 받는 값이 객체여도 새롭게 자식 컴포넌트가 렌더되는 것을 막아줄 수 있다.


3.2. props로 함수를 넘겨줄 때 (useCallback)

Parent 컴포넌트

import Child from "./Child";
import { useState } from "react";

const Parent = () => {
  const [parentAge, setParentAge] = useState(0);

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

  console.log("부모 컴포넌트 렌더링!");

  const tellMe = () => {
    console.log("길동아 사랑해");
  };

  return (
    <div style=>
      <h1>부모</h1>
      <p>age: {parentAge}</p>
      <button onClick={incrementParentAge}>부모나이 증가</button>
      <Child name={"홍길동"} tellMe={tellMe} />
    </div>
  );
};

export default Parent;


Child 컴포넌트

import { memo } from "react";

const Child = ({ name, tellMe }) => {
  console.log("자녀 컴포넌트 렌더링!");

  return (
    <div style=>
      <h3>자녀</h3>
      <p>이름: {name}</p>
      <button onClick={tellMe}>엄마  사랑해?</button>
    </div>
  );
};

export default memo(Child);

3.1.의 경우와 마찬가지로, 부모나이 증가 버튼이 눌렸을 때, React.memo로 최적화를 해주었음에도 불구하고, 프롭스로 받아온 name과 tellMe가 변경되지 않았음에도 두 컴포넌트가 모두 렌더링된다.

그 이유는 함수도 객체이기 때문이다. tellMe라는 함수는 사실 tellMe라는 변수 안에 함수 객체가 할당되어 있고, 직접 저장되는 게 아니라 메모리 주소로 저장된다. 컴포넌트 함수가 새로 호출되면, 즉 리렌더가 일어나면, 함수 내부의 모든 변수는 초기화되기 때문에 tellMe 변수도 초기화되고, 새롭게 생성된 함수 객체의 메모리가 tellMe 변수에 저장된다.


이때는 React.memo와 useCallback을 함께 사용해주면 된다.

import Child from "./Child";
import { useCallback, useState } from "react";

const Parent = () => {
  const [parentAge, setParentAge] = useState(0);

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

  console.log("부모 컴포넌트 렌더링!");

  const tellMe = useCallback(() => {
    console.log("길동아 사랑해");
  }, []);

  return (
    <div style=>
      <h1>부모</h1>
      <p>age: {parentAge}</p>
      <button onClick={incrementParentAge}>부모나이 증가</button>
      <Child name={"홍길동"} tellMe={tellMe} />
    </div>
  );
};

export default Parent;

React.memo와 useCallback을 같이 사용하면, props로 전달 받는 값이 함수여도 새롭게 자식 컴포넌트가 렌더되는 것을 막아줄 수 있다.


📌 별코딩님의 React.memo로 컴포넌트 최적화하기 (ft. useMemo, useCallback)를 참고했습니다!

댓글남기기