React. memo() (feat. useMemo, useCallback)

RumbleBi·2022년 7월 19일
0

React

목록 보기
8/9
post-thumbnail

React.memo() 는 언제 사용할까?

React.memo는 React에서 제공하는 고차 컴포넌트(HOC)이다. 부모 컴포넌트에서 자식 컴포넌트로 props를 보내게 될 때 props check를 통해 props 의 값이 변화가 있는지 확인하게 된다. 변화가 있으면 렌더링이 일어나고 아니라면 기존의 값을 그대로 재사용한다.

React.memo() 사용시 유의할 점

값을 Memoization 하는 것 자체가 메모리상에 저장을 해 둔 상태에서 필요할 때 꺼내와 쓰는 개념이기 때문에 무분별한 사용은 오히려 메모리 사용량을 늘려 최적화되지 않고 오히려 성능이 떨어질 수 있기 때문이다.

이러한 경우에 사용하도록 하자.

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

React.memo 는 오직 props check를 통해 props의 변화에만 의존하는 최적화 방법이다. 만약 useState, useContext, useReducer와 같이 상태를 변경하는 훅을 사용하면 props의 변화가 없어도 렌더링이 일어난다.

예제 코드

// App.js
import React, { useState } from "react";
import Child from "./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("부모 컴포넌트 렌더링");

  return (
    <div style={{ border: "2px solid gray", padding: "10px" }}>
      <h1>부모</h1>
      <p>age : {parentAge}</p>
      <button onClick={incrementParentAge}>부모 나이 증가</button>
      <button onClick={incrementChildAge}>자식 나이 증가</button>
      <Child name={"홍길동"} age={7} />
    </div>
  );
}

export default App;
// Child.js
import React from "react";

const Child = ({ name, age }) => {
  console.log("자녀 컴포넌트가 렌더링");
  return (
    <div style={{ border: "2px solid blue", padding: "10px" }}>
      <h3>자녀</h3>
      <p>name: {name}</p>
      <p>age: {age}</p>
    </div>
  );
};

export default Child;

위 코드에서 부모 나이 증가 버튼을 클릭하게 된다면, 아래의 사진과 같이 자식 컴포넌트도 다시 렌더링이 일어난다.

지금 코드의 경우 자식 컴포넌트에 전달해준 props는 값이 변하지 않기 때문에 React.memo를 활용하면 자식 컴포넌트의 렌더링을 막을 수 있다.

// Child.js
import React, { memo } from "react";

const Child = ({ name, age }) => {
  console.log("자녀 컴포넌트가 렌더링");
  return (
    <div style={{ border: "2px solid blue", padding: "10px" }}>
      <h3>자녀</h3>
      <p>name: {name}</p>
      <p>age: {age}</p>
    </div>
  );
};

export default memo(Child);

적용할 컴포넌트에 고차함수를 사용하여 memo로 감싸주면 적용이 된다.

위의 사진과 같이 자식 컴포넌트에는 렌더링이 일어나지 않는 것을 알 수 있다.

React.memo는 하나의 함수이며 인자로 컴포넌트를 받아 최적화된 컴포넌트로 반환시켜준다. 여기서 props check가 작동하고 값을 확인하여 변하지 않았다면 값을 재활용 하게 되는 것이다.

useMemo, useCallback을 사용하여 더욱 최적화된 컴포넌트를 만들기

// App.js
import React, { useState } from "react";
import Child from "./Child";

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

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

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

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

  return (
    <div style={{ border: "2px solid gray", padding: "10px" }}>
      <h1>부모</h1>
      <p>age : {parentAge}</p>
      <button onClick={incrementParentAge}>부모 나이 증가</button>
      <Child name={name} />
    </div>
  );
}

export default App;

이러한 경우 자식 컴포넌트에도 렌더링이 일어날까?

객체 타입은 참조 타입으로써 메모리 주소를 참조하기 때문에 렌더링이 일어난다면 App 컴포넌트에서 새로 호출이 된다는 것이고 즉 모든 변수들이 초기화가 일어난다는 것이다. 여기서 객체 타입은 메모리 주소가 변경되므로 값은 변경되지 않았지만, React에서는 변경되었다고 판단하여 자식 컴포넌트에도 렌더링이 일어나는 것이다.

이를 해결하는 방법은 3가지가 있다.

구조분해할당으로 개별 변수 값을 지정하기

// App.js

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

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

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

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

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

  return (
    <div style={{ border: "2px solid gray", padding: "10px" }}>
      <h1>부모</h1>
      <p>age : {parentAge}</p>
      <button onClick={incrementParentAge}>부모 나이 증가</button>
      <Child lastName={lastName} firstName={firstName} />
    </div>
  );
}

export default App;
// Child.js
import React, { memo } from "react";

const Child = ({ lastName, firstName }) => {
  console.log("자녀 컴포넌트가 렌더링");
  return (
    <div style={{ border: "2px solid blue", padding: "10px" }}>
      <h3>자녀</h3>
      <p>: {lastName}</p>
      <p>이름: {firstName}</p>
    </div>
  );
};

export default memo(Child);

이렇게 구조분해할당으로 개별 변수 값을 넣어준다면 메모리를 참조하지 않기 때문에 자식 컴포넌트의 렌더링이 일어나지 않게 된다.

useMemo() 를 활용하여 해결하기

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

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

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

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

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

  return (
    <div style={{ border: "2px solid gray", padding: "10px" }}>
      <h1>부모</h1>
      <p>age : {parentAge}</p>
      <button onClick={incrementParentAge}>부모 나이 증가</button>
      <Child name={name} />
    </div>
  );
}

export default App;

useCallback을 활용하여 해결하기

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

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

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

  const Calling = () => {
    console.log("Calling");
  };
  console.log("부모 컴포넌트 렌더링");

  return (
    <div style={{ border: "2px solid gray", padding: "10px" }}>
      <h1>부모</h1>
      <p>age : {parentAge}</p>
      <button onClick={incrementParentAge}>부모 나이 증가</button>
      <Child name={"홍길동"} Calling={Calling} />
    </div>
  );
}

export default App;

자바스크립트에서는 함수도 하나의 객체이므로 위의 useMemo의 상황과 같이 매번 메모리 주소가 변경되기 때문에 useCallback을 사용하여 불필요한 렌더링을 막을 수 있다.

const Calling = useCallback(() => {
    console.log("Calling");
  }, []);

useCallback을 사용하여 Calling 변수의 메모리 주소를 기억하여 렌더링이 일어난다 하더라도 값이 변하지 않는 한 렌더링이 일어나지 않을 것이다.

profile
기억보다는 기록하는 개발자

0개의 댓글