Language/React.js

[React] TodoList 프로젝트 : 최적화 (Optimization), useMemo, useCallBack

JJcoding 2024. 10. 21. 09:54

최적화(Optimization)

  • 웹 서비스의 성능을 개선하는 모든 행위를 일컫는다.
  • 아주 단순한 것부터 아주 어려운 방법까지 매우 다양하다.
  • 일반적인 최적환 방법에는 서버의 응답속도 개선 / 이미지, 폰트, 코드 파일 등의 정적 파일 로딩 개선 / 불필요한 네트워크 요청 줄임 등이 있다.
  • React App 내부의 최적화 방법에는 컴포넌트 내부의 불필요한 연산 방지 / 컴포넌트 내부의 불필요한 함수 재생성 방지 / 컴포넌트의 불필요한 리렌더링 방지 등이 있다.

useMemo

  • ‘메모이제이션’(Memoization) 기법을 기반으로 불필요한 연산은 최적화하는 React Hook이다.
  • Memoization이란? 반복적으로 수행되는 동일한 연산이 있을 때 최초 한 번의 연산을 메모리에 저장해놓았다가 결과값이 필요할 때마다 꺼내주는 기법을 말한다.

List.jsx

  • Todo List에서 사용했던 List.jsx에 할일의 갯수를 파악하는 함수를 추가하여 useMemo를 사용해보자.
import "./List.css";
import TodoItem from "./TodoItem";
import { useState, useMemo } 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(() => {
    console.log("getAnalyzedData 호출");
    const totalCount = todos.length;
    const doneCount = todos.filter((todo) => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;

    return {
      totalCount,
      doneCount,
      notDoneCount,
    };
  }, [todos]);
  // 의존성배열 : deps

  //const { totalCount, doneCount, notDoneCount } = getAnalyzedData();

  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;
  • useMemo의 의존성 배열을 사용하여 todos가 업데이트 될 때에만 동작하도록 설정할 수 있다.


React.memo

  • 컴포넌트를 인수로 받아, 최적화된 컴포넌트를 만들어 반환한다.
  • MemoizedComponent는 부모가 리렌더링 되어도 자신이 받는 props가 바뀌지 않으면 리렌더링이 발생하지 않는다. 그래서 불필요한 리렌더링을 줄여 최적화 할 수 있다.

Header.jsx

import "./Header.css";
import { memo } from "react";

const Header = () => {
  return (
    <div className="Header">
      <h3>오늘은🐿️💛</h3>
      <h1>{new Date().toDateString()}</h1>
    </div>
  );
};

export default memo(Header);

  • 체크 박스에 체크를 할 때 불필요하게 Header도 렌더링 되는 모습을 볼 수 있다. 이 때 memo를 이용하면 이런 불필요한 리렌더링을 줄일 수 있다.

  • memo 적용 전 리액트 개발자 도구로 확인 했을 때 리렌더링 되는 모습 ( 헤더와 밑에 입력창 사이에 선이 있음 )

  • memo 적용 후 리액트 개발자 도구로 확인 했을 때 리렌더링 되지 않는 모습 ( 선이 사라짐 )
  • 같은 방식으로 TodoItem도 각 아이템이 따로 리렌더링 되도록 memo 설정을 해보자.

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} 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);

  • 그런데 memo를 적용해도 모든 아이템이 여전히 같이 리렌더링 되는 것을 볼 수 있다. 왤까?
    • → state가 변화했으니 TodoItem의 부모 컴포넌트인 App 컴포넌트가 리렌더링 되는데, 이 때 App 컴포넌트 안에 있는 onUpdate, onDelete 와 같은 함수가 매번 재생성되어 TodoItem에 전달된다.
    • memo는 기본적으로 얕은 비교(===) 를 하는데 ‘객체 타입’인 이 함수들을 비교하면 (주솟값이 매번 달라져서) 기존과 다른 함수라고 인식하기 때문에, 모든 아이템들이 props가 변경된 것으로 인식하며 리렌더링 되는 것이다.
  • 해결방법은 2가지 이다.

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} checked={isDone} type="checkbox" />
      <div className="content">{content}</div>
      <div className="date">{new Date(date).toLocaleDateString()}</div>
      <button onClick={onClickDeleteButton}>삭제</button>
    </div>
  );
};

// 고차 컴포넌트 (HOC)
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;
});
  • 첫 번째 방법은 콜백함수를 활용하여 최적화 기능을 커스터마이징 해주는 것이다.

  • 이제 이렇게 내가 선택한 컴포넌트만 리렌더링 되는 것을 볼 수 있다.
  • 두 번째 방법은 useCallBack이라는 리액트 훅을 이용한 방법이다.


UseCallBack - 불필요한 함수 재생성 방지하기

App.jsx

import "./App.css";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";
import { useRef, useReducer, useCallback } from "react";

const mockDate = [
 ...생략
];

function reducer(state, action) {
 ...생략
}

function App() {
  const [todos, dispatch] = useReducer(reducer, mockDate);
  const idRef = useRef(3);

  const onCreate = useCallback((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,
    });
  }, []);

  const onDelete = useCallback((targetId) => {
    dispatch({
      type: "DELETE",
      targetId: targetId,
    });
  }, []);

  return (
    <div className="App">
      <Header />
      <Editor onCreate={onCreate} />
      <List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
    </div>
  );
}

export default App;

  • 첫 번째 방법처럼 매번 props를 일일이 비교하는 것은 비효율적이다. props의 이름이 바뀌기라도 한다면? NO~ 따라서 useCallback 함수로 애초에 함수가 한 번만 생성되도록 설정한다.
  • 위 코드 처럼 두번째 인자를 빈배열로 설정하면, 함수는 마운트 될 때 한 번만 생성이 되고 업데이트(리렌더링) 시에는 생성되지 않는다.


최적화는 언제, 어떤 것을 하는게 좋을까?

  • 언제 - 기능이 완성되고 난 후 최적화를 해야한다. 최적화 후 기능을 추가하거나 수정하려면 예상치 못한 오류를 만날 수 있다.
  • 어떤 것 - 최적화가 꼭 필요한 곳에만 한다. memo와 같은 메서드도 당연히 연산이 필요하고 자원이 소비되는 기능이기에 단순하게 UI를 렌더링하는 곳에 사용한다면 효율적이지 못하다. 함수가 많아서 코드가 무거운 컴포넌트라던가, 유저에 행동에 따라서 경우의 수가 많아질 수 있는 컴포넌트 같은 곳에 최적화를 해야한다.

 

출처 : 한입 크기로 잘라먹는 리액트