17 분 소요


[state, useRef, axios를 활용한 Todolist 만들기 - chap3:axios↗️]에 이어 작성하는 글입니다.


1. Context API

1.1 TodoContext.jsx

src > context > TodoContext.jsx 생성

import { createContext, useState, useEffect } from "react";
import { v4 as uuid } from "uuid";
import axios from "axios";

export const TodoContext = createContext();

export const TodoContextProvider = ({ children }) => {
  const [todos, setTodos] = useState([]);

  // 초기 할 일 데이터 가져오기
  useEffect(() => {
    const fetchTodos = async () => {
      try {
        const res = await axios.get(
          `${process.env.REACT_APP_SERVER_URL}/todos`
        );
        setTodos(res.data);
      } catch (error) {
        console.error("에러가 발생하였습니다.");
      }
    };
    fetchTodos();
  }, []);

  // 할 일 추가
  const addTodo = (newTodo) => {
    setTodos((prevTodos) => [
      ...prevTodos,
      { ...newTodo, id: uuid(), isDone: false },
    ]);
  };

  // 할 일 삭제
  const deleteTodo = (id) => {
    setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
  };

  // 할 일 완료 상태 토글
  const toggleTodo = (id) => {
    setTodos((prevTodos) =>
      prevTodos.map((todo) =>
        todo.id === id ? { ...todo, isDone: !todo.isDone } : todo
      )
    );
  };

  // 할 일 수정
  const updateTodo = async (updatedTodo) => {
    try {
      await axios.put(
        `${process.env.REACT_APP_SERVER_URL}/todos/${updatedTodo.id}`,
        updatedTodo
      );
      setTodos((prevTodos) =>
        prevTodos.map((todo) =>
          todo.id === updatedTodo.id ? updatedTodo : todo
        )
      );
    } catch (error) {
      console.error("할 일을 수정하는 중 오류가 발생했습니다:", error);
    }
  };

  // 할 일 정렬
  const sortTodos = (order) => {
    if (order === "asc") {
      setTodos((prevTodos) =>
        [...prevTodos].sort(
          (a, b) => new Date(a.deadline) - new Date(b.deadline)
        )
      );
    } else if (order === "desc") {
      setTodos((prevTodos) =>
        [...prevTodos].sort(
          (a, b) => new Date(b.deadline) - new Date(a.deadline)
        )
      );
    }
  };

  return (
    <TodoContext.Provider
      value={{
        todos,
        setTodos,
        addTodo,
        deleteTodo,
        toggleTodo,
        updateTodo,
        sortTodos,
      }}
    >
      {children}
    </TodoContext.Provider>
  );
};


1.2 Home.jsx

import TodoForm from "../TodoForm";
import TodoList from "../TodoList";
import TodoSort from "../TodoSort";
import styled from "styled-components";
import { TodoContextProvider } from "context/TodoContext";

const Home = () => {
  return (
    <TodoContextProvider>
      <StHomeLayout>
        <StH1>TodoList</StH1>

        <TodoForm />
        <TodoSort />
        <TodoList />
      </StHomeLayout>
    </TodoContextProvider>
  );
};

export default Home;

const StHomeLayout = styled.main`
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 100vh;
`;

const StH1 = styled.h1`
  font-size: 3rem;
  font-weight: bold;
  padding: 3rem;
`;


1.3 TodoForm.jsx

import axios from "axios";
import { TodoContext } from "context/TodoContext";
import { useContext } from "react";
import styled from "styled-components";
import { v4 as uuid } from "uuid";

const TodoForm = () => {
  const { addTodo } = useContext(TodoContext);

  const onSubmit = async (e) => {
    e.preventDefault();

    const title = e.target.title.value;
    const content = e.target.content.value;
    const deadline = e.target.deadline.value;

    if (!title || !content || !deadline) {
      alert("값을 입력하세요");
      return;
    }

    try {
      const res = await axios.post(
        `${process.env.REACT_APP_SERVER_URL}/todos`,
        {
          id: uuid(),
          title,
          content,
          isDone: false,
          deadline,
        }
      );
      addTodo(res.data);

      // 입력폼 초기화
      e.target.title.value = "";
      e.target.content.value = "";
      e.target.deadline.value = "";
    } catch (error) {
      console.error("추가 에러가 발생하였습니다.", error);
    }
  };

  return (
    <StTodoForm onSubmit={onSubmit}>
      <input type="text" name="title" placeholder="제목" />
      <input type="text" name="content" placeholder="내용" />
      <input type="date" name="deadline" />
      <button>추가</button>
    </StTodoForm>
  );
};

export default TodoForm;

const StTodoForm = styled.form`
  padding: 3rem;
`;


1.4 TodoList.jsx

import { useContext } from "react";
import TodoItem from "./TodoItem";
import { TodoContext } from "context/TodoContext";
import styled from "styled-components";

const TodoList = () => {
  const { todos } = useContext(TodoContext);

  const workingTodos = todos.filter((todo) => !todo.isDone);
  const doneTodos = todos.filter((todo) => todo.isDone);

  return (
    <>
      <StH2>미완료</StH2>
      {workingTodos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
      <StH2>할 일 완료</StH2>
      {doneTodos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </>
  );
};

export default TodoList;

const StH2 = styled.h2`
  font-size: 2rem;
  padding: 3rem;
`;


1.5 TodoItem.jsx

import axios from "axios";
import { TodoContext } from "context/TodoContext";
import { useContext, useState } from "react";
import styled from "styled-components";

const TodoItem = ({ todo }) => {
  const { deleteTodo, toggleTodo, updateTodo } = useContext(TodoContext);

  const [edit, setEdit] = useState(null);

  // 할 일 삭제 함수
  const handleDeleteButton = async (id) => {
    const deleteConfirm = window.confirm("정말 삭제하시겠습니까?");
    if (deleteConfirm) {
      try {
        await axios.delete(`${process.env.REACT_APP_SERVER_URL}/todos/${id}`);
        deleteTodo(id);
        alert("삭제되었습니다.");
      } catch (error) {
        console.error("할 일을 삭제하는 중 오류가 발생했습니다:", error);
      }
    }
  };

  // 할 일을 수정 모드로 변경하는 함수
  const handleEditButton = (todo) => {
    setEdit(todo);
  };

  // 할 일 수정 완료 함수
  const handleUpdateButton = () => {
    updateTodo(edit);
    alert("수정되었습니다.");
    setEdit(null);
  };

  // 할 일의 완료 상태 토글 함수
  const isDoneOnToggleButton = async (id) => {
    try {
      await axios.put(`${process.env.REACT_APP_SERVER_URL}/todos/${id}`, {
        ...todo,
        isDone: !todo.isDone,
      });
      toggleTodo(id);
    } catch (error) {
      console.error(
        "할 일의 완료 상태를 토글하는 중 오류가 발생했습니다:",
        error
      );
    }
  };

  // 날짜 형식을 한국어로 변환
  const formattedDate = new Date(todo.deadline).toLocaleDateString("ko-KR", {
    year: "2-digit",
    month: "long",
    day: "numeric",
    weekday: "long",
  });

  return (
    <StTodoBox>
      {edit && todo.id === edit.id ? (
        <ul>
          <input
            type="text"
            name="text"
            value={edit.title}
            onChange={(e) => setEdit({ ...edit, title: e.target.value })}
          />
          <input
            type="text"
            name="content"
            value={edit.content}
            onChange={(e) => setEdit({ ...edit, content: e.target.value })}
          />
          <input
            type="date"
            name="deadline"
            value={edit.deadline}
            onChange={(e) => setEdit({ ...edit, deadline: e.target.value })}
          />
          <button onClick={() => handleDeleteButton(todo.id)}>삭제</button>
          <button onClick={handleUpdateButton}>수정 완료</button>
        </ul>
      ) : (
        <ul>
          <li>제목: {todo.title}</li>
          <li>내용: {todo.content}</li>
          <li>마감일: {formattedDate}</li>
          <button onClick={() => handleDeleteButton(todo.id)}>삭제</button>
          <button onClick={() => handleEditButton(todo)}>수정</button>
          <button onClick={() => isDoneOnToggleButton(todo.id)}>
            {todo.isDone ? "할 일 취소" : "할 일 완료"}
          </button>
        </ul>
      )}
    </StTodoBox>
  );
};

export default TodoItem;

const StTodoBox = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  padding: 1rem 4rem;
  border: 1px solid black;
`;


1.6 TodoSort.jsx

import { TodoContext } from "context/TodoContext";
import { useContext } from "react";

const TodoSort = () => {
  const { sortTodos } = useContext(TodoContext);

  const onChangeSortOrder = (e) => {
    const nextSortOrder = e.target.value;
    sortTodos(nextSortOrder);
  };

  return (
    <div>
      <select onChange={onChangeSortOrder}>
        <option value="asc">오름차순</option>
        <option value="desc">내림차순</option>
      </select>
    </div>
  );
};

export default TodoSort;



2. Redux

2.0 파일 구조


2.1 index.js

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import store from "./redux/config/configStore";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

reportWebVitals();


2.2 todos.js - 모듈 생성

// import uuid from "react-uuid";
import shortid from "shortid";

// 액션 객체 type value 상수로 지정
const ADD_TODO = "ADD_TODO";
const DELETE_TODO = "DELETE_TODO";
const SWITCH_TODO = "SWITCH_TODO";
const UPDATE_TODO = "UPDATE_TODO";

// action creator 생성
export const addTodo = (title, body) => ({
  type: ADD_TODO,
  payload: {
    id: shortid.generate(),
    title,
    body,
    isDone: false,
  },
});

export const deleteTodo = (id) => ({
  type: DELETE_TODO,
  payload: id,
});

export const switchTodo = (id) => ({
  type: SWITCH_TODO,
  payload: id,
});

export const updateTodo = (id, title, body) => ({
  type: UPDATE_TODO,
  payload: { id, title, body },
});

// 초기 상태 값
const initialState = [
  {
    id: shortid.generate(),
    title: "타이틀",
    body: "내용",
    isDone: false,
  },
  {
    id: shortid.generate(),
    title: "타이틀2",
    body: "내용2",
    isDone: true,
  },
];

// 리듀서
const todos = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TODO:
      return [...state, action.payload];

    case DELETE_TODO:
      return state.filter((item) => item.id !== action.payload);

    case SWITCH_TODO:
      return state.map((item) =>
        item.id === action.payload ? { ...item, isDone: !item.isDone } : item
      );

    case UPDATE_TODO:
      const { id, title, body } = action.payload;
      return state.map((item) =>
        item.id === id ? { ...item, title, body } : item
      );

    default:
      return state;
  }
};

export default todos;


2.3 configStore.js - 리듀서 연결

import { createStore } from "redux";
import { combineReducers } from "redux";
import todos from "../modules/todos";

const rootReducer = combineReducers({
  todos,
});

const store = createStore(rootReducer);

export default store;


2.5 Router.js

라우터 설정

import { BrowserRouter, Routes, Route } from "react-router-dom";
import Detail from "../pages/Detail";
import Home from "../pages/Home";

const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/:id" element={<Detail />} />
      </Routes>
    </BrowserRouter>
  );
};

export default Router;


2.6 Home.jsx

import TodoArea from "../components/todo/TodoArea";
import TodoList from "../components/todo/TodoList";

const Home = () => {
  return (
    <div>
      <TodoArea />
      <TodoList isActive={true} />
      <TodoList isActive={false} />
    </div>
  );
};

export default Home;


2.7 TodoArea.jsx

// src/components/TodoArea.jsx
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../../redux/modules/todos";

const TodoArea = () => {
  const dispatch = useDispatch();
  const [title, setTitle] = useState("");
  const [body, setBody] = useState("");

  const handleAddTodo = () => {
    if (title.trim() === "" || body.trim() === "") {
      alert("제목과 내용을 입력하세요.");
      return;
    }
    dispatch(addTodo(title, body));
    setTitle("");
    setBody("");
  };

  return (
    <div>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="제목"
      />
      <input
        value={body}
        onChange={(e) => setBody(e.target.value)}
        placeholder="내용"
      />
      <button onClick={handleAddTodo}>입력</button>
    </div>
  );
};

export default TodoArea;


2.8 TodoList.jsx

import React from "react";
import { useSelector } from "react-redux";
import TodoItem from "./TodoItem";

const TodoList = ({ isActive }) => {
  const todos = useSelector((state) => state.todos);

  return (
    <div>
      <h1>{isActive ? "투두 리스트" : "던 리스트"}</h1>
      {todos
        .filter((todo) => todo.isDone !== isActive)
        .map((todo) => (
          <TodoItem key={todo.id} todo={todo} />
        ))}
    </div>
  );
};

export default TodoList;


2.9 TodoItem.jsx

import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { updateTodo, deleteTodo, switchTodo } from "../../redux/modules/todos";
import { Link } from "react-router-dom";

const TodoItem = ({ todo }) => {
  const dispatch = useDispatch();
  const [isEditing, setIsEditing] = useState(false);
  const [newTitle, setNewTitle] = useState(todo.title);
  const [newBody, setNewBody] = useState(todo.body);

  const handleEditToggle = () => {
    setIsEditing(!isEditing);
  };

  const handleSave = () => {
    dispatch(updateTodo(todo.id, newTitle, newBody));
    setIsEditing(false);
  };

  const handleDelete = () => {
    dispatch(deleteTodo(todo.id));
  };

  const handleSwitch = () => {
    dispatch(switchTodo(todo.id));
  };

  return (
    <div>
      {isEditing ? (
        <>
          <input
            type="text"
            value={newTitle}
            onChange={(e) => setNewTitle(e.target.value)}
          />
          <textarea
            value={newBody}
            onChange={(e) => setNewBody(e.target.value)}
          />
          <button onClick={handleSave}>저장</button>
          <button onClick={handleEditToggle}>취소</button>
          <Link to={`/detail/${todo.id}`}>상세</Link>
        </>
      ) : (
        <>
          <p>제목: {todo.title}</p>
          <p>내용: {todo.body}</p>
          <button onClick={handleEditToggle}>수정</button>
          <button onClick={handleDelete}>삭제</button>
          <button onClick={handleSwitch}>
            {todo.isDone ? "취소" : "완료"}
          </button>
          <Link to={`/${todo.id}`}>상세</Link>
        </>
      )}
    </div>
  );
};

export default TodoItem;


2.10 Detail.jsx

import { useSelector, useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { deleteTodo } from "../redux/modules/todos";

const Detail = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const dispatch = useDispatch();

  const todos = useSelector((state) => state.todos);
  const todo = todos.find((item) => item.id === id);

  if (!todo) {
    // Handle case where todo is not found
    return <p>Todo not found</p>;
  }

  const handleDelete = () => {
    dispatch(deleteTodo(id));
    navigate("/");
  };

  return (
    <div>
      <h1>상세 보기</h1>
      <p>
        <strong>제목:</strong> {todo.title}
      </p>
      <p>
        <strong>내용:</strong> {todo.body}
      </p>
      <p>
        <strong>완료 여부:</strong> {todo.isDone ? "완료" : "미완료"}
      </p>
      <button onClick={() => navigate(-1)}>뒤로 가기</button>
      <button onClick={handleDelete}>삭제</button>
    </div>
  );
};

export default Detail;



3. Zustand

  • 베이스 코드 : Context API
  • Provider 가 존재하지 않는다.

3.1 Zustand 설치

yarn add zustand


3.2 src/store/useTodos.jsx

import create from "zustand";
import { v4 as uuid } from "uuid";
import axios from "axios";

const useTodoStore = create((set) => ({
  todos: [],
  setTodos: (todos) => set({ todos }),
  fetchTodos: async () => {
    try {
      const res = await axios.get(`${process.env.REACT_APP_SERVER_URL}/todos`);
      set({ todos: res.data });
    } catch (error) {
      console.error("에러가 발생하였습니다.");
    }
  },
  addTodo: (newTodo) =>
    set((state) => ({
      todos: [...state.todos, { ...newTodo, id: uuid(), isDone: false }],
    })),
  deleteTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== id),
    })),
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, isDone: !todo.isDone } : todo
      ),
    })),
  updateTodo: async (updatedTodo) => {
    try {
      await axios.put(
        `${process.env.REACT_APP_SERVER_URL}/todos/${updatedTodo.id}`,
        updatedTodo
      );
      set((state) => ({
        todos: state.todos.map((todo) =>
          todo.id === updatedTodo.id ? updatedTodo : todo
        ),
      }));
    } catch (error) {
      console.error("할 일을 수정하는 중 오류가 발생했습니다:", error);
    }
  },
  sortTodos: (order) => {
    set((state) => {
      const sortedTodos = [...state.todos].sort((a, b) => {
        return order === "asc"
          ? new Date(a.deadline) - new Date(b.deadline)
          : new Date(b.deadline) - new Date(a.deadline);
      });
      return { todos: sortedTodos };
    });
  },
}));

export default useTodoStore;


3.3 Home.jsx

import TodoForm from "../TodoForm";
import TodoList from "../TodoList";
import TodoSort from "../TodoSort";
import styled from "styled-components";

const Home = () => {
  return (
    <StHomeLayout>
      <StH1>TodoList</StH1>
      <TodoForm />
      <TodoSort />
      <TodoList />
    </StHomeLayout>
  );
};

export default Home;

const StHomeLayout = styled.main`
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 100vh;
`;

const StH1 = styled.h1`
  font-size: 3rem;
  font-weight: bold;
  padding: 3rem;
`;


3.4 TodoForm.jsx

import axios from "axios";
import useTodoStore from "../store/useTodos";
import styled from "styled-components";
import { v4 as uuid } from "uuid";

const TodoForm = () => {
  /* 1. 구조 분해 할당으로 가져오기
  const { addTodo } = useTodoStore(); */

  // 2. 상태 선택 함수로 가져오기
  const addTodo = useTodoStore((state) => state.addTodo);

  const onSubmit = async (e) => {
    e.preventDefault();

    const title = e.target.title.value;
    const content = e.target.content.value;
    const deadline = e.target.deadline.value;

    if (!title || !content || !deadline) {
      alert("값을 입력하세요");
      return;
    }

    try {
      const res = await axios.post(
        `${process.env.REACT_APP_SERVER_URL}/todos`,
        {
          id: uuid(),
          title,
          content,
          isDone: false,
          deadline,
        }
      );
      addTodo(res.data);

      // 입력폼 초기화
      e.target.title.value = "";
      e.target.content.value = "";
      e.target.deadline.value = "";
    } catch (error) {
      console.error("추가 에러가 발생하였습니다.", error);
    }
  };

  return (
    <StTodoForm onSubmit={onSubmit}>
      <input type="text" name="title" placeholder="제목" />
      <input type="text" name="content" placeholder="내용" />
      <input type="date" name="deadline" />
      <button>추가</button>
    </StTodoForm>
  );
};

export default TodoForm;

const StTodoForm = styled.form`
  padding: 3rem;
`;


3.5 TodoItem.jsx

import axios from "axios";
import useTodoStore from "../store/useTodos";
import { useState } from "react";
import styled from "styled-components";

const TodoItem = ({ todo }) => {
  const deleteTodo = useTodoStore((state) => state.deleteTodo);
  const toggleTodo = useTodoStore((state) => state.toggleTodo);
  const updateTodo = useTodoStore((state) => state.updateTodo);

  const [edit, setEdit] = useState(null);

  const handleDeleteButton = async (id) => {
    const deleteConfirm = window.confirm("정말 삭제하시겠습니까?");
    if (deleteConfirm) {
      try {
        await axios.delete(`${process.env.REACT_APP_SERVER_URL}/todos/${id}`);
        deleteTodo(id); // 서버 요청이 성공하면 상태 업데이트
        alert("삭제되었습니다.");
      } catch (error) {
        console.error("할 일을 삭제하는 중 오류가 발생했습니다:", error);
      }
    }
  };

  const handleEditButton = (todo) => {
    setEdit(todo);
  };

  const handleUpdateButton = () => {
    updateTodo(edit);
    alert("수정되었습니다.");
    setEdit(null);
  };

  const isDoneOnToggleButton = async (id) => {
    try {
      await axios.put(`${process.env.REACT_APP_SERVER_URL}/todos/${id}`, {
        ...todo,
        isDone: !todo.isDone,
      });
      toggleTodo(id);
    } catch (error) {
      console.error(
        "할 일의 완료 상태를 토글하는 중 오류가 발생했습니다:",
        error
      );
    }
  };

  const formattedDate = new Date(todo.deadline).toLocaleDateString("ko-KR", {
    year: "2-digit",
    month: "long",
    day: "numeric",
    weekday: "long",
  });

  return (
    <StTodoBox>
      {edit && todo.id === edit.id ? (
        <ul>
          <input
            type="text"
            name="text"
            value={edit.title}
            onChange={(e) => setEdit({ ...edit, title: e.target.value })}
          />
          <input
            type="text"
            name="content"
            value={edit.content}
            onChange={(e) => setEdit({ ...edit, content: e.target.value })}
          />
          <input
            type="date"
            name="deadline"
            value={edit.deadline}
            onChange={(e) => setEdit({ ...edit, deadline: e.target.value })}
          />
          <button onClick={() => handleDeleteButton(todo.id)}>삭제</button>
          <button onClick={handleUpdateButton}>수정 완료</button>
        </ul>
      ) : (
        <ul>
          <li>제목: {todo.title}</li>
          <li>내용: {todo.content}</li>
          <li>마감일: {formattedDate}</li>
          <button onClick={() => handleDeleteButton(todo.id)}>삭제</button>
          <button onClick={() => handleEditButton(todo)}>수정</button>
          <button onClick={() => isDoneOnToggleButton(todo.id)}>
            {todo.isDone ? "할 일 취소" : "할 일 완료"}
          </button>
        </ul>
      )}
    </StTodoBox>
  );
};

export default TodoItem;

const StTodoBox = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  padding: 1rem 4rem;
  border: 1px solid black;
`;


3.6 TodoList.jsx

import useTodoStore from "../store/useTodos";
import TodoItem from "./TodoItem";
import styled from "styled-components";
import { useEffect } from "react";

const TodoList = () => {
  const todos = useTodoStore((state) => state.todos);
  const fetchTodos = useTodoStore((state) => state.fetchTodos);

  useEffect(() => {
    fetchTodos();
  }, [fetchTodos]);

  if (!Array.isArray(todos)) {
    return <div>Loading...</div>;
  }

  const workingTodos = todos.filter((todo) => !todo.isDone);
  const doneTodos = todos.filter((todo) => todo.isDone);

  return (
    <>
      <StH2>미완료</StH2>
      {workingTodos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
      <StH2>할 일 완료</StH2>
      {doneTodos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </>
  );
};

export default TodoList;

const StH2 = styled.h2`
  font-size: 2rem;
  padding: 3rem;
`;


3.7 TodoSort.jsx

import useTodoStore from "../store/useTodos";

const TodoSort = () => {
  const sortTodos = useTodoStore((state) => state.sortTodos);

  const onChangeSortOrder = (e) => {
    const nextSortOrder = e.target.value;
    sortTodos(nextSortOrder);
  };

  return (
    <div>
      <select onChange={onChangeSortOrder}>
        <option value="asc">오름차순</option>
        <option value="desc">내림차순</option>
      </select>
    </div>
  );
};

export default TodoSort;



4. Jotai

베이스 코드 : Context API

4.1 jotai 설치

yarn add jotai


4.2 App.jsx

기본적으로 Jotai는 전역 상태를 사용하지만, Provider를 통해 상태를 특정 범위에서만 사용할 수 있다.

import Router from "./shared/Router";
import { Provider } from "jotai";

function App() {
  return (
    <Provider>
      <Router />
    </Provider>
  );
}

export default App;


4.3 src/store/todoAtom.js

import { atom, useAtom } from "jotai";
import { useEffect } from "react";
import axios from "axios";
import { v4 as uuid } from "uuid";

// Atoms
const todosAtom = atom([]);

// Actions
const addTodoAtom = atom(
  (get) => get(todosAtom),
  (get, set, newTodo) => {
    set(todosAtom, [
      ...get(todosAtom),
      { ...newTodo, id: uuid(), isDone: false },
    ]);
  }
);

const deleteTodoAtom = atom(
  (get) => get(todosAtom),
  (get, set, id) => {
    set(
      todosAtom,
      get(todosAtom).filter((todo) => todo.id !== id)
    );
  }
);

const toggleTodoAtom = atom(
  (get) => get(todosAtom),
  (get, set, id) => {
    set(
      todosAtom,
      get(todosAtom).map((todo) =>
        todo.id === id ? { ...todo, isDone: !todo.isDone } : todo
      )
    );
  }
);

const updateTodoAtom = atom(
  (get) => get(todosAtom),
  async (get, set, updatedTodo) => {
    try {
      await axios.put(
        `${process.env.REACT_APP_SERVER_URL}/todos/${updatedTodo.id}`,
        updatedTodo
      );
      set(
        todosAtom,
        get(todosAtom).map((todo) =>
          todo.id === updatedTodo.id ? updatedTodo : todo
        )
      );
    } catch (error) {
      console.error("할 일을 수정하는 중 오류가 발생했습니다:", error);
    }
  }
);

const sortTodosAtom = atom(
  (get) => get(todosAtom),
  (get, set, order) => {
    if (order === "asc") {
      set(
        todosAtom,
        [...get(todosAtom)].sort(
          (a, b) => new Date(a.deadline) - new Date(b.deadline)
        )
      );
    } else if (order === "desc") {
      set(
        todosAtom,
        [...get(todosAtom)].sort(
          (a, b) => new Date(b.deadline) - new Date(a.deadline)
        )
      );
    }
  }
);

// Fetch initial todos
const useFetchTodos = () => {
  const [, setTodos] = useAtom(todosAtom);

  useEffect(() => {
    const fetchTodos = async () => {
      try {
        const res = await axios.get(
          `${process.env.REACT_APP_SERVER_URL}/todos`
        );
        setTodos(res.data);
      } catch (error) {
        console.error("에러가 발생하였습니다.");
      }
    };
    fetchTodos();
  }, [setTodos]);
};

export {
  todosAtom,
  addTodoAtom,
  deleteTodoAtom,
  toggleTodoAtom,
  updateTodoAtom,
  sortTodosAtom,
  useFetchTodos,
};


4.4 Home.jsx

useFetchTodos 훅을 사용

import TodoForm from "../TodoForm";
import TodoList from "../TodoList";
import TodoSort from "../TodoSort";
import styled from "styled-components";
import { useFetchTodos } from "store/todoAtom";

const Home = () => {
  useFetchTodos();

  return (
    <StHomeLayout>
      <StH1>TodoList</StH1>
      <TodoForm />
      <TodoSort />
      <TodoList />
    </StHomeLayout>
  );
};

export default Home;

const StHomeLayout = styled.main`
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 100vh;
`;

const StH1 = styled.h1`
  font-size: 3rem;
  font-weight: bold;
  padding: 3rem;
`;


4.5 TodoForm.jsx

import axios from "axios";
import { useAtom } from "jotai";
import styled from "styled-components";
import { addTodoAtom } from "store/todoAtom";
import { v4 as uuid } from "uuid";

const TodoForm = () => {
  const [, addTodo] = useAtom(addTodoAtom);

  const onSubmit = async (e) => {
    e.preventDefault();

    const title = e.target.title.value;
    const content = e.target.content.value;
    const deadline = e.target.deadline.value;

    if (!title || !content || !deadline) {
      alert("값을 입력하세요");
      return;
    }

    try {
      const res = await axios.post(
        `${process.env.REACT_APP_SERVER_URL}/todos`,
        {
          id: uuid(),
          title,
          content,
          isDone: false,
          deadline,
        }
      );
      addTodo(res.data);

      // 입력폼 초기화
      e.target.title.value = "";
      e.target.content.value = "";
      e.target.deadline.value = "";
    } catch (error) {
      console.error("추가 에러가 발생하였습니다.", error);
    }
  };

  return (
    <StTodoForm onSubmit={onSubmit}>
      <input type="text" name="title" placeholder="제목" />
      <input type="text" name="content" placeholder="내용" />
      <input type="date" name="deadline" />
      <button>추가</button>
    </StTodoForm>
  );
};

export default TodoForm;

const StTodoForm = styled.form`
  padding: 3rem;
`;


4.6 TodoList.jsx

import { useAtom } from "jotai";
import TodoItem from "./TodoItem";
import { todosAtom } from "store/todoAtom";
import styled from "styled-components";

const TodoList = () => {
  const [todos] = useAtom(todosAtom);

  const workingTodos = todos.filter((todo) => !todo.isDone);
  const doneTodos = todos.filter((todo) => todo.isDone);

  return (
    <>
      <StH2>미완료</StH2>
      {workingTodos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
      <StH2>할 일 완료</StH2>
      {doneTodos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </>
  );
};

export default TodoList;

const StH2 = styled.h2`
  font-size: 2rem;
  padding: 3rem;
`;


4.7 TodoItem.jsx

import axios from "axios";
import { useAtom } from "jotai";
import styled from "styled-components";
import { deleteTodoAtom, toggleTodoAtom, updateTodoAtom } from "store/todoAtom";
import { useState } from "react";

const TodoItem = ({ todo }) => {
  const [, deleteTodo] = useAtom(deleteTodoAtom);
  const [, toggleTodo] = useAtom(toggleTodoAtom);
  const [, updateTodo] = useAtom(updateTodoAtom);

  const [edit, setEdit] = useState(null);

  const handleDeleteButton = async (id) => {
    const deleteConfirm = window.confirm("정말 삭제하시겠습니까?");
    if (deleteConfirm) {
      try {
        await axios.delete(`${process.env.REACT_APP_SERVER_URL}/todos/${id}`);
        deleteTodo(id);
        alert("삭제되었습니다.");
      } catch (error) {
        console.error("할 일을 삭제하는 중 오류가 발생했습니다:", error);
      }
    }
  };

  const handleEditButton = (todo) => {
    setEdit(todo);
  };

  const handleUpdateButton = () => {
    updateTodo(edit);
    alert("수정되었습니다.");
    setEdit(null);
  };

  const isDoneOnToggleButton = async (id) => {
    try {
      await axios.put(`${process.env.REACT_APP_SERVER_URL}/todos/${id}`, {
        ...todo,
        isDone: !todo.isDone,
      });
      toggleTodo(id);
    } catch (error) {
      console.error(
        "할 일의 완료 상태를 토글하는 중 오류가 발생했습니다:",
        error
      );
    }
  };

  const formattedDate = new Date(todo.deadline).toLocaleDateString("ko-KR", {
    year: "2-digit",
    month: "long",
    day: "numeric",
    weekday: "long",
  });

  return (
    <StTodoBox>
      {edit && todo.id === edit.id ? (
        <ul>
          <input
            type="text"
            name="text"
            value={edit.title}
            onChange={(e) => setEdit({ ...edit, title: e.target.value })}
          />
          <input
            type="text"
            name="content"
            value={edit.content}
            onChange={(e) => setEdit({ ...edit, content: e.target.value })}
          />
          <input
            type="date"
            name="deadline"
            value={edit.deadline}
            onChange={(e) => setEdit({ ...edit, deadline: e.target.value })}
          />
          <button onClick={() => handleDeleteButton(todo.id)}>삭제</button>
          <button onClick={handleUpdateButton}>수정 완료</button>
        </ul>
      ) : (
        <ul>
          <li>제목: {todo.title}</li>
          <li>내용: {todo.content}</li>
          <li>마감일: {formattedDate}</li>
          <button onClick={() => handleDeleteButton(todo.id)}>삭제</button>
          <button onClick={() => handleEditButton(todo)}>수정</button>
          <button onClick={() => isDoneOnToggleButton(todo.id)}>
            {todo.isDone ? "할 일 취소" : "할 일 완료"}
          </button>
        </ul>
      )}
    </StTodoBox>
  );
};

export default TodoItem;

const StTodoBox = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  padding: 1rem 4rem;
  border: 1px solid black;
`;


4.8 TodoSort.jsx

import { useAtom } from "jotai";
import { sortTodosAtom } from "store/todoAtom";

const TodoSort = () => {
  const [, sortTodos] = useAtom(sortTodosAtom);

  const onChangeSortOrder = (e) => {
    const nextSortOrder = e.target.value;
    sortTodos(nextSortOrder);
  };

  return (
    <div>
      <select onChange={onChangeSortOrder}>
        <option value="asc">오름차순</option>
        <option value="desc">내림차순</option>
      </select>
    </div>
  );
};

export default TodoSort;


카테고리:

업데이트:

댓글남기기