[React다루는기술] 11. React TODO List


리액트를 다루는 기술(개정판)을 참고한 리액트 TODO App 예제 코드이다. 설렁설렁 하기 좋다.


1. TODO APP

react-deal-todo-app


App.js

import React, { useState, useRef, useCallback } from "react";
import TodoApp from "./components/TodoApp/TodoApp";
import TodoInsert from "./components/TodoInsert/TodoInsert";
import TodoList from "./components/TodoList/TodoList";

const App = () => {
  const [todos, setTodos] = useState([
    { id: 1, text: "리액트 취미", checked: true },
    { id: 2, text: "자바 마지막 회독", checked: true },
    { id: 3, text: "준비 및 진행 상태 체크", checked: false },
  ]);

  const idx = useRef(4);

  //TODO 추가
  const onAddTodo = useCallback(
    (text) => {
      const todo = {
        id: idx.current,
        text,
        checked: false,
      };
      setTodos(todos.concat(todo));
      idx.current += 1;
    },
    [todos]
  );

  //TODO 삭제
  const onRemoveTodo = useCallback(
    (id) => {
      setTodos(todos.filter((todo) => todo.id !== id));
    },
    [todos]
  );

  const onToggle = useCallback(
    (id) => {
      setTodos(
        todos.map((todo) =>
          todo.id === id ? { ...todo, checked: !todo.checked } : todo
        )
      );
    },
    [todos]
  );

  return (
    <TodoApp>
      <TodoInsert onAddTodo={onAddTodo} />
      <TodoList todos={todos} onRemoveTodo={onRemoveTodo} onToggle={onToggle} />
    </TodoApp>
  );
};

export default App;


TodoApp.js

import "./TodoApp.scss";

const TodoApp = ({ children }) => {
  return (
    <div className="TodoApp">
      <div className="app-title">일정 관리</div>
      <div className="content">{children}</div>
    </div>
  );
};

export default TodoApp;


TodoInsert.js

import { useState, useCallback } from "react";
import { MdAdd } from "react-icons/md";
import "./TodoInsert.scss";

const TodoInsert = ({ onAddTodo }) => {
  //onAddTodo 는 App.js에서 useCallback
  const [value, setValue] = useState("");

  const onChange = useCallback((e) => {
    setValue(e.target.value);
  }, []); // 두 번째 매개변수로 전달된 배열에 따라 언제 useEffect를 실행할지 결정됩

  const onSubmit = useCallback(
    (e) => {
      onAddTodo(value);
      setValue("");
      //submit 브라우저 새로고침 방어
      e.preventDefault();
    },
    [onAddTodo, value]
  );

  return (
    <form className="TodoInsert" onSubmit={onSubmit}>
      <input
        placeholder="할 일을 입력하세요."
        value={value}
        onChange={onChange}
      />
      <button type="submit">
        <MdAdd />
      </button>
    </form>
  );
};

export default TodoInsert;


TodoList.js

import TodoListItem from "../TodoListItem/TodoListItem";
import "./TodoList.scss";

const TodoList = ({ todos, onRemoveTodo, onToggle }) => {
  return (
    <div className="TodoList">
      {todos.map((todo) => (
        <TodoListItem
          todo={todo}
          key={todo.id}
          onRemoveTodo={onRemoveTodo}
          onToggle={onToggle}
        />
      ))}
    </div>
  );
};

export default TodoList;


TodoListItem.js

import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from "react-icons/md";
import cn from "classnames";
import "./TodoListItem.scss";

const TodoListItem = ({ todo, onRemoveTodo, onToggle }) => {
  const { id, text, checked } = todo;

  return (
    <div className="TodoListItem">
      <div className={cn("checkbox", { checked })} onClick={() => onToggle(id)}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
      <div className="remove" onClick={() => onRemoveTodo(id)}>
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};

export default React.memo(TodoListItem);



2. 성능최적화로 React.memo 적용 및 useCallback 불변성

TodoListItem.js

성능 최적화를 위해 컴포넌트의 리렌더링을 방지로 React.memo 적용한 것이다.
그러면 todo, onRemove, onToggle 를 리렌더링을 하지 않는다.

export default React.memo(TodoListItem);


App.js

useCallback hook에서 todos 기존 데이터가 변경시마다 함수가 생성되는 것을 방지하기 위해 두번째 배열 [todos] 제거하면서 불변성으로 처리하기 위해 데이터 비교를 위해 기쥰 데이터를 직접 수정하지 않고 새로운 객체를 만들어 필요한 부분만 수정하게끔 수정한 코드이다. 불변성이 지켜지지 않으면 값이 변경된것을 감지 못하기 때문에 React.memo 에서 비교를 못할 수 있다.

//TODO 추가
const onAddTodo = useCallback(
  (text) => {
    const todo = {
      id: idx.current,
      text,
      checked: false,
    };
    setTodos((todos) => todos.concat(todo));
    idx.current += 1;
  },
  [] //[todos] 불변성을 위해 제거
);

//TODO 삭제
const onRemoveTodo = useCallback(
  (id) => {
    setTodos((todos) => todos.filter((todo) => todo.id !== id));
  },
  [] //[todos] 불변성을 위해 제거
);

const onToggle = useCallback(
  (id) => {
    setTodos((todos) =>
      todos.map((todo) =>
        todo.id === id ? { ...todo, checked: !todo.checked } : todo
      )
    );
  },
  [] //[todos] 불변성을 위해 제거
);



3. useState 성능 문제

useState는 React에서 상태를 관리하는 Hook 중 하나로, 함수형 컴포넌트에서 상태를 추가하고 업데이트할 수 있게 해주나 데이터가 많은 경우에는 몇 가지 단점이 발생한다.

  1. 성능 문제:
    • useState는 단순한 값 변경을 감지하는 데 사용되기 때문에, 데이터가 많고 복잡할수록 성능에 영향을 미칠 수 있습니다. 이는 컴포넌트가 다시 렌더링될 때마다 모든 상태를 비교하고 업데이트해야 하기 때문입니다.
  2. 가독성과 유지보수:
    • 많은 상태가 하나의 객체에 담겨 있다면, 코드의 가독성과 유지보수가 어려워질 수 있습니다. 어떤 상태가 변경되었는지 알아내기 어려워질 수 있습니다.
  3. 렌더링 최적화 어려움:
    • useState는 단순한 값 비교를 통해 상태 업데이트를 감지하기 때문에, 객체나 배열과 같은 복잡한 데이터 구조의 경우 얕은 비교를 하게 됩니다. 이는 때로는 필요한 최적화를 수행하기 어렵게 만들 수 있습니다.
  4. 불필요한 렌더링:
    • 상태가 많고 컴포넌트가 자주 렌더링되는 경우, 불필요한 렌더링이 발생할 수 있습니다. 상태가 변경되지 않았더라도 모든 상태를 확인하고 렌더링이 발생하기 때문입니다.
  5. 분리 어려움:
    • 모든 상태를 하나의 useState로 관리하는 경우, 관련된 상태를 개별적으로 추적하고 업데이트하기 어려울 수 있습니다. 논리적으로 관련된 상태들을 여러 개의 useState로 분리하는 것이 더 좋을 수 있습니다.

이러한 이유로, 데이터가 많은 경우에는 useState를 적절히 활용하여 상태를 분리하고 최적화하는 것이 중요합니다. 더 복잡한 상태 관리가 필요한 경우에는 useReducer나 상태 관리 라이브러리(예: Redux)를 고려할 수도 있다.

아래 수정된 App.js 컴포넌트는 useState 의 함수형 업데이트를 사용하는 대신 useReducer를 사용헤서 onToggle과 onRemove가 새로 생기는 것을 방지한다.

import React, { useState, useRef, useCallback, useReducer } from "react";
import TodoApp from "./components/TodoApp/TodoApp";
import TodoInsert from "./components/TodoInsert/TodoInsert";
import TodoList from "./components/TodoList/TodoList";

function createDummyData() {
  return [
    { id: 1, text: "리액트 취미", checked: true },
    { id: 2, text: "자바 마지막 회독", checked: true },
    { id: 3, text: "준비 및 진행 상태 체크", checked: false },
  ];
}
function todoReducer(todos, action) {
  switch (action.type) {
    case "INSERT":
      return todos.concat(action.todo);
    case "REMOVE":
      return todos.filter((todo) => todo.id !== action.id);
    case "TOGGLE":
      return todos.map((todo) =>
        todo.id === action.id ? { ...todo, checked: !todo.checked } : todo
      );
    default:
      return todos;
  }
}

const App = () => {
  //useState는 단순한 값 변경을 감지하는 데 사용되기 때문에,
  //데이터가 많고 복잡할수록 성능에 영향을 미칠 수 있디
  //이는 컴포넌트가 다시 렌더링될 때마다 모든 상태를 비교하고 업데이트해야 하기 때문이다.
  // const [todos, setTodos] = useState([
  //   { id: 1, text: "리액트 취미", checked: true },
  //   { id: 2, text: "자바 마지막 회독", checked: true },
  //   { id: 3, text: "준비 및 진행 상태 체크", checked: false },
  // ]);

  //useReducer 3번째 인자는 컴포넌트 맨 처음 렌더링시에만 호출되는 option 인자다.
  const [todos, dispatch] = useReducer(todoReducer, undefined, createDummyData);

  const idx = useRef(4);

  //TODO 추가
  const onAddTodo = useCallback(
    (text) => {
      const todo = {
        id: idx.current,
        text,
        checked: false,
      };
      //setTodos((todos) => todos.concat(todo));
      dispatch({ type: "INSERT", todo });
      idx.current += 1;
    },
    [] //[todos] 성능을 위해 제거
  );

  //TODO 삭제
  const onRemoveTodo = useCallback(
    (id) => {
      //setTodos((todos) => todos.filter((todo) => todo.id !== id));
      dispatch({ type: "REMOVE", id });
    },
    [] //[todos] 성능을 위해 제거
  );

  const onToggle = useCallback(
    // (id) => {
    //   setTodos((todos) =>
    //     todos.map((todo) =>
    //       todo.id === id ? { ...todo, checked: !todo.checked } : todo
    //     )
    //   );
    // },
    (id) => {
      dispatch({ type: "TOGGLE", id });
    },
    [] //[todos] 성능을 위해 제거
  );

  return (
    <TodoApp>
      <TodoInsert onAddTodo={onAddTodo} />
      <TodoList todos={todos} onRemoveTodo={onRemoveTodo} onToggle={onToggle} />
    </TodoApp>
  );
};

export default App;



react-deal-book-img