현재 리스트를 렌더링하고 있는데, 무작위로 입력된 배열은 간단한 내장 정렬 메소드를 사용하여 목록을 정렬하고 있다.
import React, { useState, useCallback, useMemo } 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;
import React from 'react';
import classes from './DemoList.module.css';
const DemoList = (props) => {
//정렬 메소드
const sortedList = props.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);
지금은 정렬할 요소가 많지 않지만, 만약에 요소가 엄청나게 많다고 해보자.
그러면 이런 정렬 메소드가 있는 경우, 전체 컴포넌트가 재평가될 때 마다 매번 정렬하는 코드를 재실행하고 싶지는 않을 것이다.
위와 같이 React.memo()를 통해 DemoList를 감싸주면 불필요한 동작을 막을 수 있을 것 같다.
버튼을 클릭하면 {props.title}이 바뀌었기 때문에 DemoList가 재실행되는건 당연하다. props가 바뀌었기 때문에 React.memo()는 정상적으로 작동한다. (문제 X)
또한 현재 부모 컴포넌트가 재렌더링되었기 때문에 DemoList도 재렌더링되는 것이기도 하다. 왜냐하면 부모 컴포넌트가 실행될 때마다 부모컴포넌트에서 [5, 3, 1, 10, 9]
배열을 지속적으로 새로 만들기 때문이다.
배열도 함수처럼 객체이고, 객체는 참조값이다. 같은 배열처럼 보이지만 메모리 안에서는 다른 위치를 가지는 다른 배열이다.
위 두 가지 이유 때문에 현재 DemoList는 리빌드되기에 타당한 이유를 가지고 있다.
하지만 정렬은 컴포넌트에서 수행가능한 대표적인 성능 집약적 작업 중 하나이다. 바뀌지 않는 저 배열을 지속적으로 새로 만들기는 원하지 않는다면, 2의 문제는 어떻게 하면 해결할 수 있을까?
useMemo() 훅을 사용하면 된다.
import React, { useMemo } from 'react';
import classes from './DemoList.module.css';
const DemoList = (props) => {
//객체 구조분해할당으로 props.items 배열을 가져온다.
const { items } = props;
const sortedList = useMemo(() => {
console.log('Items sorted');
//첫번째 인자는 저장하고 싶은 데이터를 반환하는 함수
return items.sort((a, b) => a - b);
//두 번째 인자는 의존성 배열로, 의존성 배열을 통해 저장된 값에 변경사항이 생기면 업데이트됨
//즉 items 배열에 새로운 값이 추가되거나 변경되면 다시 재정렬됨
}, [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);
const sortedList = useMemo(() => return items.sort((a, b) => a - b);}, [items]);
이렇게만 하면 여전히 2번 문제는 해결되지 않는다. App이 재실행될 때마다 props.item으로 가는 배열이 새롭게 생성되고 있기 때문이다. 따라서 그 부분에도 useMemo()를 사용하여 감싸주자.
import React, { useState, useCallback, useMemo } 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');
}, []);
//useMemo(함수의 리턴값으로 기존 배열을 넣고, 바뀌는게 없이 하드코딩되어 있으니 종속배열은 비워둠)
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>
);
}
export default App;
이렇게 해주면 잘 작동한다.
useMemo 덕분에 이제 더 이상 배열이 재정렬되지는 않는다.
useMemo는 useCallback에 비하면 사용빈도는 낮다. 함수를 기억하는 것이 훨씬 더 도움이 되고, 데이터를 기억할 일보다 빈도수가 더 많기 때문이다.
물론 데이터 재계산과 같은 성능 집약적 작업 때문에 데이터를 저장해야할 필요도 있지만 이런 경우가 아니라면 별로 사용하지 않는다.
useMemo를 사용하여 데이터를 저장하면, 메모리를 사용하여 저장하는 것이다.
따라서 useMemo는 사용하는 모든 값에 다 사용하는 훅은 아니다. 위의 경우처럼 뭔가를 정렬하는 경우, 이후 컴포넌트 업데이트에서 불필요한 정렬을 막아야 할 때 useMemo를 적절히 사용하도록 하자.
뭐든지 정답은 없다. 앱이 느려지거나 개선해야할 때 이런 훅을 이용하여 최적화 가능한 부분을 찾아 볼 수 있다.