useMemo는 "메모이제이션" 기법을 기반으로 불필요한 연산을 최적화하는 React Hook 입니다.

메모이제이션이란 "기억해두기", "메모해두기"라는 뜻입니다.
프로그래밍에서 동일한 연산을 반복적으로 수행해야 할 경우, 매번 결과값을 새로 계산하면 불필요한 연산 낭비가 발생합니다.

메모지에이션은 불필요한 연산 낭비를 방지하기 위해, 최초 한 번 계산한 결과값을 메모리에 저장해 두고, 동일한 연산이 다시 필요할 때 저장된 값을 바로 반환하는 기법을 의미합니다.

메모이제이션 기법을 사용하면, 최초에 한 번 연산을 수행해 결과값을 저장해 둔 뒤 동일한 연산이 필요할 때 저장된 값을 재사용하기 때문에, 불필요한 반복 연산을 방지하여 프로그래밍 성능 저하를 예방할 수 있습니다.
React에서 useMemo라는 React Hook을 이용하면 메모이제이션 기법을 이용해서 특정 연산에 결과값을 기억해둘 수 있습니다.
ToDo 앱에서는 불필요한 연산이 거의 없기 때문에, 의도적으로 이를 만들어 보겠습니다. 이를 위해 List 컴포넌트 내부에서 현재 Todo 상태를 분석하고 이를 수치로 제공하는 함수를 작성합니다.getAnalyzedData 함수를 만들어줍니다.
import { useState } from "react";
import "./List.css";
import TodoItem from "./TodoItem";
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.toLocaleLowerCase()));
};
const filteredTodos = getFilteredData();
const getAnalyzedData = () => {
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
};
return (
<div className="List">
<h4>Todo List 🌱</h4>
<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;
List 컴포넌트가 리렌더링될 때마다 getAnalyzedData 함수를 호출합니다. 이 함수의 반환값을 구조 분해 할당을 통해 totalCount, doneCount, notDoneCount로 분리해 사용합니다.
import { useState } from "react";
import "./List.css";
import TodoItem from "./TodoItem";
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.toLocaleLowerCase()));
};
const filteredTodos = getFilteredData();
const getAnalyzedData = () => {
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
};
const { totalCount, doneCount, notDoneCount } = getAnalyzedData();
return (
<div className="List">
<h4>Todo List 🌱</h4>
<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;
totalCount, doneCount, notDoneCount의 세 수치를 렌더링하기 위해 div 태그를 추가하고, 해당 값들을 할당하여 화면에 표시합니다.
import { useState } from "react";
import "./List.css";
import TodoItem from "./TodoItem";
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.toLocaleLowerCase()));
};
const filteredTodos = getFilteredData();
const getAnalyzedData = () => {
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
};
const { totalCount, doneCount, notDoneCount } = getAnalyzedData();
return (
<div className="List">
<h4>Todo List 🌱</h4>
<div>Total : {totalCount}</div>
<div>Done : {doneCount}</div>
<div>Not Done : {notDoneCount}</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;

useMemo를 사용하여 getAnalyzedData의 불필요한 연산을 방지해 봅니다.
useMemo를 활용하면 getAnalyzedData에서 수행되는 연산을 메모이제이션하여, 필요할 때만 다시 계산되도록 설정할 수 있습니다. 또한, 특정 조건이 만족될 때에만 결과 값을 재계산하도록 제어할 수도 있습니다.
우선 useMemo를 import 합니다. 그리고 useMemo를 호출합니다. useMemo의 첫 번째 인수로는 콜백 함수를 넣고 두 번째 인수로는 배열을 넣어줍니다.
import { useState, useMemo } from "react";
import "./List.css";
import TodoItem from "./TodoItem";
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.toLocaleLowerCase()));
};
const filteredTodos = getFilteredData();
const getAnalyzedData = () => {
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
};
useMemo(() => {}, []);
const { totalCount, doneCount, notDoneCount } = getAnalyzedData();
return (
<div className="List">
<h4>Todo List 🌱</h4>
<div>Total : {totalCount}</div>
<div>Done : {doneCount}</div>
<div>Not Done : {notDoneCount}</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;
이때 useMemo의 두 번째 인수인 배열은 의존성 배열(deps)입니다. 이는 useEffect의 의존성 배열과 유사한 역할을 합니다. 이전에 라이프사이클에서 useEffect라는 React Hook을 사용했었는데, useEffect는 deps 배열에 포함된 값이 변경될 때 콜백 함수를 다시 실행하는 동작을 수행합니다.
useMemo 역시 의존성 배열(deps)에 포함된 값이 변경되었을 때에만 첫 번째 인수로 전달된 콜백 함수를 다시 실행합니다. 이 콜백 함수가 반환하는 값을 useMemo는 캐싱하여, 그대로 반환합니다.
따라서, 이 콜백 함수 안에 메모이제이션하고자 하는 연산을 작성하면 됩니다. getAnalyzedData 함수의 내용을 그대로 콜백 함수로 옮겨 넣으면 됩니다.
import { useState, useMemo } from "react";
import "./List.css";
import TodoItem from "./TodoItem";
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.toLocaleLowerCase()));
};
const filteredTodos = getFilteredData();
useMemo(() => {
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
}, []);
const { totalCount, doneCount, notDoneCount } = getAnalyzedData();
return (
<div className="List">
<h4>Todo List 🌱</h4>
<div>Total : {totalCount}</div>
<div>Done : {doneCount}</div>
<div>Not Done : {notDoneCount}</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;
useMemo의 반환값이 { totalCount, doneCount, notDoneCount } 객체이므로, 이를 구조 분해 할당을 사용해 totalCount, doneCount, notDoneCount로 받아오는 것이 가능합니다.
import { useState, useMemo } from "react";
import "./List.css";
import TodoItem from "./TodoItem";
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.toLocaleLowerCase()));
};
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,
};
}, []);
return (
<div className="List">
<h4>Todo List 🌱</h4>
<div>Total : {totalCount}</div>
<div>Done : {doneCount}</div>
<div>Not Done : {notDoneCount}</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;
useMemo React Hook은 콜백 함수가 반환하는 값을 그대로 반환해줍니다. 그래서 구조 분해 할당으로 { totalCount, doneCount, notDoneCount } 형태로 받아 볼 수 있습니다.
또한, useMemo는 첫 번째 인수로 전달된 콜백 함수를 두 번째 인수인 deps를 기준으로 메모이제이션합니다. 따라서, deps에 아무것도 전달하지 않으면, 첫 번째 콜백 함수의 연산과 반환은 List 컴포넌트가 최초로 렌더링될 때 단 한 번만 실행됩니다.
그러나 ToDo가 추가되거나 삭제되거나 완료 처리된 결과를 반영하려면, deps에 todos의 상태를 포함해야 합니다.
import { useState, useMemo } from "react";
import "./List.css";
import TodoItem from "./TodoItem";
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.toLocaleLowerCase()));
};
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]);
return (
<div className="List">
<h4>Todo List 🌱</h4>
<div>Total : {totalCount}</div>
<div>Done : {doneCount}</div>
<div>Not Done : {notDoneCount}</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;
이처럼 useMemo React Hook을 사용하면, deps를 활용하여 특정 조건이 만족하지 않을 때에는 원하는 연산을 다시 수행하지 않도록 설정할 수 있습니다.