10.2) useMemo와 연산 최적화
"메모이제이션"기법을 기반으로 불 필요한 연산을 최적화 하는 리액트 훅
특정 연산의 결과 값을 기억해 둘 수 있다.
자매품: useCallback
메모이제이션(Memoization)? 기억해두기, 메모해두기
import "./List.css";
import TodoItem from "./TodoItem";
import { useMemo, useState } from "react";
const List = ({ todos, onUpdate, onDelete }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getFilteredData = () => {
if (search === "") {
return todos;
}
return todos.filter((todo) =>
todo.content.toLowerCase().includes(search.toLowerCase())
);
};
const filteredTodos = getFilteredData();
const { totalCount, doneCount, notDoneCount } = useMemo(() => {
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
}, [todos]);
// [] 의존성 배열 : deps가 바뀔 때 callback함수 실행
// callback함수가 반환하는 값을 그대로 다시 반환해준다.
// callback함수 내에는 memoization하고 싶은 함수를 넣으면 된다.
return (
<div className="List">
<h4>Todo List 🌱</h4>
<div>
<div>total: {totalCount}</div>
<div>done: {doneCount}</div>
<div>notDone: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요"
/>
<div className="todos_wrapper">
{filteredTodos.map((todo) => {
return (
<TodoItem
key={todo.id}
{...todo}
onUpdate={onUpdate}
onDelete={onDelete}
/>
);
})}
</div>
</div>
);
};
export default List;
10.3) React.memo와 컴포넌트 렌더링 최적화
React.memo란? 컴포넌트를 인수로 받아, 최적화된 컴포넌트를 만들어 반환
import "./TodoItem.css";
import { memo } from "react";
const TodoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input
onChange={onChangeCheckbox}
readOnly
checked={isDone}
type="checkbox"
/>
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
export default memo(TodoItem);
가지고 있는 props가 변화할 때만 리렌더링
하지만 지금은 계속 리렌더링 왜?
memo는 얕은 비교를 하기 때문에(같은 값이 더라도 다른 곳에서 리렌더링할 때 객체의 주소값이 바뀌어서, 다른 값이라고 판단해버림..)
props가 바뀌었다고 판단..
onUpdate, onDelete 처럼 객체 타입의 값을 props로 받고 있는 컴포넌트는 memo 메서드를 적용하기만 해서는 최적화가 이루어지지 않는다.
방법1. 앱 컴포넌트에서 이 함수들 자체를 메모이제이션해서 리렌더링 되더라도 다시 생성되지 않게 방지하는 방법..
-> 다음시간에 배울 useCallback이라는 훅을 배워야 하기 때문에 다음에 이용해보도록 함..
방법2. memo의 두번째 인수로 callback 함수를 추가로 전달해서 최적화 기능을 커스터마이징하는 방법..
보통은 이 callback함수를 생략하지만, 이렇게 전달을 해주면 memo메서드는 부모 컴포넌트가 리렌더링 될 때마다 이 컴포넌트의 props가 바뀌었는지 스스로 판단하는게 아니라!
대신에 이 callback 함수의 매개변수로 과거의 props인 prev props와 현재의 props인 next props를 전달해줘서 이 함수의 반환 값에 따라 props가 바뀌었는지, 안바뀌었는지 판단..
T -> props가 바뀌지 않음 -> 리렌더링 X
F -> props가 바뀜 -> 리렌더링 O
import "./TodoItem.css";
import { memo } from "react";
const TodoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input
onChange={onChangeCheckbox}
readOnly
checked={isDone}
type="checkbox"
/>
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
export default memo(TodoItem, (prevProps, nextProps) => {
// 반환값에 따라, Props가 바뀌었는지 안바뀌었는지 판단
// T -> Props 바뀌지 않음 -> 리렌더링 X
// F -> Props 바뀜 -> 리렌더링 O
if (prevProps.id !== nextProps.id) return false;
if (prevProps.isDone !== nextProps.isDone) return false;
if (prevProps.content !== nextProps.content) return false;
if (prevProps.date !== nextProps.date) return false;
return true;
});
10.4) useCallback과 함수 재생성 방지
React의 memo 메서드를 사용하여 Props가 바뀌었는지 안바뀌었는지의 유무를 기본적으로는 얕은 비교로 판단하기 때문에 이런 함수와 같은 onUpadate, onDelete 함수같은 객체 타입의 값을 props로 전달해줄 때에는 제대로 된 최적화가 이루어지지 않아서
별도로 추가적인 callback함수를 전달해서 일일이 하나하나 props의 값이 바뀌엇는지 비교를 해줬어야 했다.
그런데 최적화를 위해 컴포넌트 memo메서드를 적용할 때마다 이렇게 모든 props를 일일이 다 비교해야한다면 상당히 불편하다.
게다가 props의 이름이 바뀌면 callback함수 안에도 바꿔줘야하고, 새로운 props가 추가되면 새로운 조건을 추가해줘야 한다.
export default memo(TodoItem, (prevProps, nextProps) => {
// 반환값에 따라, Props가 바뀌었는지 안바뀌었는지 판단
// T -> Props 바뀌지 않음 -> 리렌더링 X
// F -> Props 바뀜 -> 리렌더링 O
if (prevProps.id !== nextProps.id) return false;
if (prevProps.isDone !== nextProps.isDone) return false;
if (prevProps.content !== nextProps.content) return false;
if (prevProps.date !== nextProps.date) return false;
return true;
});
그래서 onUpdate와 onDelete가 다시 생성되지 않도록 최적하는게 더 좋은 방법일 수 있다.
그걸 가능하게 해주는게 useCallback인 React Hook이다.
App.jsx
import { useCallback, useReducer, useRef, useState } from "react";
import "./App.css";
import Editor from "./components/Editor";
import Header from "./components/Header";
import List from "./components/List";
const mockData = [
{
id: 0,
isDone: false,
content: "React 공부하기",
date: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "빨래하기",
date: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: "노래 연습하기",
date: new Date().getTime(),
},
];
function reducer(state, action) {
switch (action.type) {
case "CREATE":
return [action.data, ...state];
case "UPDATE":
return state.map((item) =>
item.id === action.targetId ? { ...item, isDone: !item.isDone } : item
);
case "DELETE":
return state.filter((item) => item.id !== action.targetId);
default:
return state;
}
}
function App() {
const [todos, dispatch] = useReducer(reducer, mockData);
const idRef = useRef(3);
const onCreate = (content) => {
dispatch({
type: "CREATE",
data: {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
},
});
};
const onUpdate = useCallback((targetId) => {
dispatch({
type: "UPDATE",
targetId: targetId,
});
}, []);
// useCallback: 함수를 메모이제이션한다!라고 생각하면 된다.
// useCallback(() => []) // 첫번째인수: 함수를 그대로 반환,
// 두번째 인수: 아무것도 적혀있지 않다면, 컴포넌트 마운트 이후에는 생성되지 않는다.
const onDelete = useCallback((targetId) => {
dispatch({
type: "DELETE",
targetId: targetId,
});
}, []);
return (
<div className="App">
{/* <Exam></Exam> */}
<Header />
<Editor onCreate={onCreate} />
<List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
TodoItem.jsx
import "./TodoItem.css";
import { memo } from "react";
const TodoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input
onChange={onChangeCheckbox}
readOnly
checked={isDone}
type="checkbox"
/>
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
export default memo(TodoItem); // 👈 다시 이렇게 돌려 놓아도 최적화는 충분?하다
// export default memo(TodoItem, (prevProps, nextProps) => {
// // 반환값에 따라, Props가 바뀌었는지 안바뀌었는지 판단
// // T -> Props 바뀌지 않음 -> 리렌더링 X
// // F -> Props 바뀜 -> 리렌더링 O
// if (prevProps.id !== nextProps.id) return false;
// if (prevProps.isDone !== nextProps.isDone) return false;
// if (prevProps.content !== nextProps.content) return false;
// if (prevProps.date !== nextProps.date) return false;
// return true;
// });
💡 최적화는 언제, 어떤 것을 하는지 중요한 기준이 된다.
1. 언제 하면 좋은가?
리액트 앱을 최적화할 때는 하나의 프로젝트를 거의 완성한 상태에서 최적화를 한다.
그래서 항상 기능을 구현 완성 후 최적화는 한다.
2. 어떤 것이 최적화의 대상인가?
모든 것 X,
최적화가 꼭 필요할 것 같은 연사느 함수, 컴포넌트에만
사소한 컴포넌트에는 잘 사용하지 않는다.
무거운 컴포넌트에 사용한다.
'공부 > frontend' 카테고리의 다른 글
useReducer (0) | 2025.01.31 |
---|---|
라이프 사이클(useEffect) (1) | 2025.01.31 |
React 프로젝트 1.카운터 앱 (0) | 2025.01.30 |
React.js 입문 (0) | 2025.01.30 |
React.js 개론 (1) | 2025.01.30 |