본문 바로가기
JS/React 강의

[React] 21. useMemo()를 사용해서 object와 array의 재설정 막기

by 박기린 2024. 1. 9.

안녕하세요. 박기린입니다.

 

 

React.memo()와 useCallback()에 이어서, obejct와 array의 재실행/재평가를 막아주는 useMemo()에 대해 알아보겠습니다.

 

 

React.memo 설명글 : https://arnopark.tistory.com/840

useCallback 설명글 : https://arnopark.tistory.com/845

(위 두 글을 읽고, 본 글을 읽는 것을 적극 추천드립니다.)

 

 

 


사전 정의

React.memo()의 구체적인 설명을 위해, 어떠한 앱이 있다고 가정을 하겠습니다.

이 앱은 아래 3개의 파일을 가지고 있습니다.

 

프로그램의 구조
App.js 
- Demo.js
- Button.js

 

// App.js
import React, { useState, useCallback } from 'react';

import './App.css';
import DemoList from './components/Demo/DemoList';
import Button from './components/UI/Button/Button';

function App() {
  const [listTitle, setListTitle] = useState('My List');

const changeTitleHandler = useCallback(() => {
    setListTitle('New Title');
  }, []);

  return (
    <div className="app">
      <DemoList title={listTitle} items={[5, 3, 1, 10, 9]} />
      <Button onClick={changeTitleHandler}>Change List Title</Button>
    </div>
  );
}

export default App;

 

// Button.js
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);

 

// DemoList.js
import React from 'react';

import classes from './DemoList.module.css';

const DemoList = (props) => {
  const { items } = props;
  const sortedList = items.sort((a, b) => a - b);
  console.log('DemoList RUNNING');

  return (
    <div className={classes.list}>
      <h2>{props.title}</h2>
      <ul>
        {sortedList.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default React.memo(DemoList);

 

App.js가 루트 컴포넌트이고, DemoList.js와 Button.js는 자식 컴포넌트입니다.

 

 

 


실행 화면

위 코드들을 실행했을 때의 앱 화면입니다.

 

 

<DemoList title={listTitle} items={[5, 3, 1, 10, 9]} />

App 컴포넌트에서 DemoList 컴포넌트로 props를 전달할 때, item 배열의 숫자가 따로 정렬되어 있지 않습니다.

 

 

 

const sortedList = items.sort((a, b) => a - b);

DemoList 컴포넌트 내부에서, 전달받은 items를 sort()하고, 

 

 

 

그 값을 위처럼 출력하는 구조입니다.

 

 

 

 

 

 

 

그리고 <Change List Title> 버튼을 누르면, "My List"에서 "New Title"로 제목이 변경됩니다.

 

 


문제 확인

<Change List Title> 버튼은 Button.js 컴포넌트입니다. 이 안에는 React.memo()와 useCallback()이 적용되어 있어서, App.js 컴포넌트가 재실행되어도 같이 재실행/재평가되지 않습니다.

 

 

 

반면에, DemoList.js는 React.memo()가 적용되어 있음에도 불구하고 App.js를 따라서 재실행/재평가가 됩니다. 그래서 버튼을 누를 때마다 콘솔에 'DemoList RUNNING'이 찍힙니다.

 

 

 

 

React.memo()를 적용했음에도 DemoList.js가 재실행/재평가되는 이유는 두 가지입니다.

1. DemoList의 title prop이 변경됐기 때문이다.
2. DemoList의 items prop은 array 타입이라서, object type value에 속하기 때문에 React.memo()로는 한계가 있다.
(2번이 이해가 안 된다면, React.memo() 설명글을 읽고 오세요. https://arnopark.tistory.com/840)

 

 

 

  const sortedList = items.sort((a, b) => a - b);

만약 DemoList.js가 계속 재실행된다면, 위의 sort()함수도 계속 재실행될 것입니다. 규모가 작은 array라면 큰 문제가 안 생기겠지만, 규모가 커질수록 분류작업에 시간과 자원이 몇 배는 더 쓰일 것입니다. 결국 성능 저하 문제가 심각해집니다.

그렇기 때문에 재실행/재평가를 막을 필요가 있습니다.

 

 

 

원하는 목표는 이와 같습니다.

1. title prop이 변경되어서 DemoList.js 컴포넌트가 재실행되더라도, sortedList 만큼은 재실행되지 않도록 막는다.
2. 그렇기 위해서 array 타입의 value에도 React.memo()를 적용하고 싶다.

 

위의 두 조건을 충족시켜주는 리액트 훅 함수가 있습니다. 그게 바로 오늘 알아볼 useMemo()입니다.

 

 

 

 


해결책 : useMemo()

before

const sortedList = items.sort((a, b) => a - b);

 

 

after

  const sortedList = useMemo(() => {
    console.log('Items sorted');
    return items.sort((a, b) => a - b);
  }, [items]);

sortedList 코드를 useMemo()를 적용시켜 위와 같이 바꿉니다.

 

 

 

useMemo()의 사용구조

useMemo()의 사용구조는 이러합니다.

useMemo(함수, [의존성 배열])

 

첫 번째 인수로 주어지는 함수의 return 값으로 'React.memo()를 적용시켜서 따로 저장하고 싶은 값'을 넣어줍니다.

위 예시 코드의 경우, sorted된 items array를 return 값으로 넣었습니다.

 

두 번째 인수의 의존성 배열에는, useEffect와 useCallback처럼 변동사항을 체크할 값들을 넣어줍니다.

위 예시 코드의 경우, items array에 변동사항이 생길 때만 useMemo() 안의 함수가 재실행될 것입니다.

(의존성 배열이 뭔지 모르겠다면? : https://arnopark.tistory.com/846?category=1001779)

 

 

 

 

앱 실행 확인 - 실패

useMemo()를 적용시킨 후, 앱을 다시 실행하고 버튼을 눌러봅니다.

그런데 'Items sorted'가 버튼을 누를 때마다 계속 뜹니다. useMemo()가 잘 작동하지 않았습니다.

 

 

 

 

 

왜 오류가 발생했을까요?

이유는 생각보다 간단합니다. items array가 변경됐기 때문입니다.

 

  const sortedList = useMemo(() => {
    console.log('Items sorted');
    return items.sort((a, b) => a - b);
  }, [items]);

useMemo()의 의존성배열에 items를 넣었습니다.

따라서, useMemo() 안의 코드가 재실행됐다는 것은 items가 변경됐다는 것입니다.

 

하지만 또 다시 의문이 생깁니다. DemoList.js 내부의 어떤 곳에서도 (sortedList 내부 함수를 제외하면) items 배열을 수정하는 일이 일어나지 않습니다. 그러면 어디에서 수정이 일어나는 걸까요?

 

 

 

const DemoList = (props) => {
  const { items } = props;
 {...}
}
// App.js 중
<DemoList title={listTitle} items={[5, 3, 1, 10, 9]} />

DemoList.js는 items를 App.js로부터 전달받습니다. 이때 문제가 발생합니다. 바로 Array라는 점 때문입니다. 

 

 

DemoList.js의 sortedItems에는 useMemo()를 적용시켰지만, 정작 App.js에서 건내주는 원본 array에는 useMemo()가 적용되지 않았습니다! 

그래서 App.js가 재실행될 때마다 원본 array 또한 계속 다시 재생성되고, array는 object type value이다보니... 역시나 '===' 비교 연산자가 작동하지 못해서 서로 다른 값을 인식하게 됩니다.

 

 

 

 

해결방안

이를 해결하는 방법도 정말 간단합니다.

App.js에서 전달하는 원본 array에도 똑같이 useMemo()를 적용시켜주면 됩니다.

 

 

  const listItems = useMemo(() => [5, 3, 1, 10, 9], []);

이런 식으로 useMemo()를 적용한 array를 만들어줍니다.

 

 

 

<DemoList title={listTitle} items={listItems} />

그 후, 자식 컴포넌트(DemoList.js)에 prop으로 넘겨주면 문제가 해결됩니다.

 

 

 

 

 

 


수정 후 실행 화면

<Change List Title> 버튼을 누르기 전/후

버튼을 누르면서 listTitle state가 변경됐고, listTitle을 prop으로 전달받는 DemoList.js가 재실행됐습니다. 그래서 콘솔에는 "DemoList RUNNING'이 다시 한 번 기록됐습니다.

하지만 listItems array는 변경된 점이 없기 때문에 배열 재정렬이 다시 실행되지 않았고, 그래서 "items sorted" 콘솔이 다시 찍히지 않았습니다.

즉, useMemo()를 사용해서 array prop에도 React.memo()기능이 정상작동한 것입니다. 이 방법을 이용해서 array 뿐만 아니라, object에도 React.memo()를 적용시킬 수 있습니다.

 

 

 


수정 후 코드 전체

// App.js 
function App() {
  const [listTitle, setListTitle] = useState('My List');

const changeTitleHandler = useCallback(() => {
    setListTitle('New Title');
  }, []);

  const listItems = useMemo(() => [5, 3, 1, 10, 9], []);

  return (
    <div className="app">
      <DemoList title={listTitle} items={listItems} />
      <Button onClick={changeTitleHandler}>Change List Title</Button>
    </div>
  );
}

 

// DemoList.js
const DemoList = (props) => {
  const { items } = props;

  const sortedList = useMemo(() => {
    console.log('Items sorted');
    return items.sort((a, b) => a - b);
  }, [items]); 
  console.log('DemoList RUNNING');

  return (
    <div className={classes.list}>
      <h2>{props.title}</h2>
      <ul>
        {sortedList.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default React.memo(DemoList);

 

 

반응형