안녕하세요. 박기린입니다.
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으로 넘겨주면 문제가 해결됩니다.
수정 후 실행 화면
버튼을 누르면서 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);
'JS > React 강의' 카테고리의 다른 글
[React] 23. 커스텀 훅(Custom Hook)을 만들어 보자 (0) | 2024.02.07 |
---|---|
[React] 22. HTTP 리퀘스트 보내기 (0) | 2024.01.31 |
[React] 20. useEffect와 useCallback에서 의존성 배열을 사용하는 이유 (Dependencies Array 설명) (1) | 2024.01.04 |
[React] 19. useCallback()으로 함수 재생성을 방지하기 (1) | 2024.01.03 |
[React] 18. React.memo() (0) | 2023.12.26 |