[React] TodoList Refactoring

gimmari·2024년 8월 24일
0

📝 React

목록 보기
11/24

리팩토링 일명 '파일 세분화하기'를 해보았다.

일단 기존 파일은 src/component/TodoList.jsx 하나에 App.jsx의 file tree.

이제 component 폴더 아래

  1. TodoContainer - 상태를 중앙 집중 관리

  2. TodoForm - 제출 받는 폼

  3. TodoList - 리스트를 렌더링

  4. TodoItem - 각 아이템 하나를 렌더링

이런 파일 4개를 세분화 refactoring하겠다.


→ 일단 기존 코드에서 간결하고 직관적이게 최적화 작업을 거쳤다.

import { SAMPLE_TODOS } from "../../constants/sample-todos";
import React, { useState } from "react";

const TodoContainer = () => {
  const [todos, setTodos] = useState(SAMPLE_TODOS);
  const [newTodo, setNewTodo] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault(); 

    if (!newTodo.trim()) {
      return; 
    }
    const newtodoObj = {
      id: crypto.randomUUID(),
      text: newTodo,
      completed: false,
    };
    setTodos([newtodoObj, ...todos]);
    setNewTodo("");
  };

  const handleInputChange = (e) => {
    setNewTodo(e.target.value);
  };

  const toggleCompleted = (id) =>
    setTodos((prevTodos) =>
      prevTodos.map((todo) =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );


  const handleDelete = (id) =>
    setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={newTodo}
          onChange={handleInputChange}
          placeholder="Enter a new todo"
        />
        <button type="submit">Add Todo</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleUpdate(todo.id)}
            />
            {todo.text}
            {todo.completed ? <p>완료됨</p> : <p>진행중</p>}
            <button onClick={() => handleDelete(todo.id)}>삭제</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TodoContainer;

🔍 상세 설명 (1)

handleSubmit 함수에서 newtodoObj 추가 방법 변경

→ 이전 코드:

setTodos([
  ...todos,
  { id: crypto.randomUUID(), text: newTodo, completed: false },
]);

→ 변경된 코드:

const newtodoObj = {
  id: crypto.randomUUID(),
  text: newTodo,
  completed: false,
};
setTodos([newtodoObj, ...todos]);

newtodoObj를 먼저 정의한 후, setTodos에서 이를 사용해 todos 배열 앞에 새 항목을 추가했습니다. 이전 코드에서는 새로 추가된 할 일이 배열의 마지막에 위치했지만, 변경된 코드에서는 새 할 일이 배열의 맨 앞에 위치합니다. 이로 인해 할 일이 추가될 때마다 최신 항목이 리스트의 맨 위에 나타납니다.

🔍 상세 설명 (2)

toggleCompleted 함수의 변경

→ 이전 코드:

const handleUpdate = (id) => {
  const updatedTodos = todos.map((todo) => {
    if (todo.id === id) {
      return {
        ...todo,
        completed: !todo.completed,
      };
    } else {
      return todo;
    }
  });
  setTodos(updatedTodos);
};

→ 변경된 코드:

const toggleCompleted = (id) =>
  setTodos((prevTodos) =>
    prevTodos.map((todo) =>
      todo.id === id
        ? { ...todo, completed: !todo.completed }
        : todo
    )
  );

함수 이름이 handleUpdate에서 toggleCompleted로 변경되었습니다.
setTodos 함수 내에서 이전 상태(prevTodos)를 사용하여 map 함수를 통해 해당 id의 completed 상태를 반전시키도록 했습니다.

🔍 상세 설명 (3)

handleDelete 함수의 단축

→ 이전 코드:

const handleDelete = (id) => {
  const updatedTodos = todos.filter((todo) => todo.id !== id);
  setTodos(updatedTodos);
};

→ 변경된 코드:

const handleDelete = (id) =>
  setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));

handleDelete 함수가 더 간결하게 작성되었습니다. filter 메소드를 사용해 조건에 맞지 않는 요소를 제거한 후, 바로 setTodos를 통해 상태를 업데이트합니다. 이전 상태(prevTodos)를 사용하여 동일한 작업을 수행합니다.

🔍 상세 설명 (4)

함수 호출 이름 변경 (handleUpdate -> toggleCompleted)

→ 이전 코드:

onChange={() => handleUpdate(todo.id)}

→ 변경된 코드:

onChange={() => toggleCompleted(todo.id)}

handleUpdate 함수 호출이 toggleCompleted로 변경되었습니다. 이로 인해 할 일의 completed 상태를 반전시키는 함수 이름이 더 직관적으로 변경되었습니다.


이제 이 코드를 4개의 파일로 리팩토링 할 것이다.

TodoForm

import React, { useState } from "react";

const TodoForm = ({ addTodos }) => {
  const [newTodo, setNewTodo] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault(); 

    if (!newTodo.trim()) {
      return; 
    }
    const newtodoObj = {
      id: crypto.randomUUID(),
      text: newTodo,
      completed: false,
    };
    // setTodos([newtodoObj, ...todos]);
    addTodos(newtodoObj);
    
    setNewTodo("");
  };

  const handleInputChange = (e) => {
    setNewTodo(e.target.value);
  };
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={newTodo}
        onChange={handleInputChange}
        placeholder="Enter a new todo"
      />
      <button type="submit">Add Todo</button>
    </form>
  );
};

export default TodoForm;

setTodos([newtodoObj, ...todos]);→ addTodos(newtodoObj);

TodoForm 컴포넌트는 addTodos라는 새로운 함수를 props로 받아옵니다. TodoForm은 더 이상 todos나 setTodos를 필요로 하지 않으며, addTodos 함수만을 호출. 기존의 setTodos가 정의되어 있지 않아 새로 선언하고 TodoForm이 받아옴.

❓ 리팩토링의 주요 목표는 단일 책임 원칙(SRP)을 따르는 것입니다. 즉, TodoForm 컴포넌트는 할 일을 추가하는 입력 폼의 역할만 담당하며, 상태 업데이트는 부모 컴포넌트가 관리하도록 변경되었습니다. 이렇게 하면 코드의 재사용성과 유지보수성이 향상됩니다.


TodoItem

const TodoItem = ({ todo, toggleCompleted, handleDelete }) => {
  return (
    <li key={todo.id}>
      <p
        style={{
          textDecoration: todo.completed ? "line-through" : "none",
        }}
      >
        {todo.text} -{" "}
        {todo.completed ? <span>완료됨</span> : <span>진행중</span>}
      </p>
      <button onClick={() => toggleCompleted(todo.id)}>
        {todo.completed ? "취소" : "왼료"}
      </button>
      <button onClick={() => handleDelete(todo.id)}>삭제</button>
    </li>
  );
};

export default TodoItem;

화면에 렌더링 될 TodoItem을 보여준다


TodoList

import TodoItem from "./TodoItem";

const TodoList = ({ todos, toggleCompleted, handleDelete }) => {
  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          toggleCompleted={toggleCompleted}
          handleDelete={handleDelete}
        />
      ))}
    </ul>
  );
};

export default TodoList;

TodoItem에서 정보를 받아와서 map함수를 통해 리스트로 뿌려주고 화면에 렌더링.


TodoContainer

import { SAMPLE_TODOS } from "../../constants/sample-todos";
import React, { useState } from "react";
import TodoForm from "./TodoFOrm";
import TodoList from "./TodoList";

const TodoContainer = () => {
  const [todos, setTodos] = useState(SAMPLE_TODOS);

  const addTodos = (newTodoObj) => setTodos([newTodoObj, ...todos]);

  const toggleCompleted = (id) =>
    setTodos((prevTodos) =>
      prevTodos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );

  // 1. Todo 항목을 삭제하는 함수 정의
  const handleDelete = (id) =>
    // 2. 선택된 항목을 제외한 새로운 배열 생성
    setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));

  return (
    <div>
      <TodoForm addTodos={addTodos} />
      <TodoList
        todos={todos}
        toggleCompleted={toggleCompleted}
        handleDelete={handleDelete}
      />
    </div>
  );
};

export default TodoContainer;

addTodos 함수를 TodoContainer 컴포넌트에서 선언하고, 이를 TodoForm 컴포넌트에 prop으로 전달하여, TodoForm에서 이 함수를 호출함으로써 새로운 할 일 객체를 부모 컴포넌트(TodoContainer)로 전달하는 방식.

  • handleDelete 함수:
const handleDelete = (id) =>
  setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
prevTodos.filter: todos 배열에서 주어진 id를 제외한 새로운 배열을 생성합니다.
  • toggleCompleted 함수:
const toggleCompleted = (id) =>
  setTodos((prevTodos) =>
    prevTodos.map((todo) =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  );
toggleCompleted: 이 함수는 특정 id를 가진 할 일의 완료 상태를 토글(반전)합니다.
setTodos: 상태를 업데이트하는 함수로, 이전 상태 prevTodos를 받아서 새로운 배열을 반환합니다.
prevTodos.map: todos 배열의 각 항목을 순회하면서, 주어진 id와 일치하는 항목의 completed 상태를 반전시킵니다.
삼항 연산자: todo.id === id가 참이면 completed 상태를 반전시키고, 그렇지 않으면 기존의 todo 객체를 그대로 반환합니다.
profile
김마리의 개발.로그

0개의 댓글