[React] useMemo - 불 필요한 연산 방지

Ahnzi·2024년 11월 27일

useMemo

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

메모이제이션

메모이제이션이란 "기억해두기", "메모해두기"라는 뜻입니다.

프로그래밍에서 동일한 연산을 반복적으로 수행해야 할 경우, 매번 결과값을 새로 계산하면 불필요한 연산 낭비가 발생합니다.

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

메모이제이션 기법을 사용하면, 최초에 한 번 연산을 수행해 결과값을 저장해 둔 뒤 동일한 연산이 필요할 때 저장된 값을 재사용하기 때문에, 불필요한 반복 연산을 방지하여 프로그래밍 성능 저하를 예방할 수 있습니다.

React에서 useMemo라는 React Hook을 이용하면 메모이제이션 기법을 이용해서 특정 연산에 결과값을 기억해둘 수 있습니다.

불필요한 연산 만들기

ToDo 앱에서는 불필요한 연산이 거의 없기 때문에, 의도적으로 이를 만들어 보겠습니다. 이를 위해 List 컴포넌트 내부에서 현재 Todo 상태를 분석하고 이를 수치로 제공하는 함수를 작성합니다.getAnalyzedData 함수를 만들어줍니다.

  • totalCount : ToDo 아이템의 갯수
  • doneCount : 전체 ToDo 중 완료된 ToDo 갯수
  • notDoneCount : 완료되지 않은 ToDo 갯수
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 사용하기

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를 활용하여 특정 조건이 만족하지 않을 때에는 원하는 연산을 다시 수행하지 않도록 설정할 수 있습니다.

profile
운동하는 개발자 Ahnzi 입니다.

0개의 댓글