본문 바로가기
React

Todo앱을 만들어 보자 -5-

by tinker_coder 2024. 9. 28.

지난 시간에 이어서 todo추가하기 기능을 완성하고 추가한 todo를 메인화면에 표시하는 것을 시작하기 전에 먼저 전체적인 테마 색상을 설정하도록 할 건데 색상의 조합은 아래 사이트에서 참고하였다. 해당사이트에서 마음에 드는 색상조합을 찾은 뒤 색상코드를 복사하면 된다.

https://2colors.colorion.co/

 

Two Color Combinations

Two color combination palettes

2colors.colorion.co

two color combinations

빨간동그라미 버튼을 클릭하면 복사할 수 있다.

 

마음에 드는 색조합을 찾았으면 우리가 만든 Todo앱에 적용시킨다. 다음과 같이 Root.js를 수정한다.

import { RecoilRoot } from "recoil";
import App from "./App";
import { HashRouter } from "react-router-dom";
import { createTheme, ThemeProvider } from "@mui/material";

function Root() {
  const theme = createTheme({
    palette: {
      primary: {
        main: "#990011",
        contrastText: "#FCF6F5",
        background: {
          default: '#FCF6F5',  // 기본 배경색 설정
          paper: '#ffffff',     // 카드 등 컴포넌트의 배경색 설정
        },
      },
    },
  });
  return (
    <>
      <RecoilRoot>
        <HashRouter>
          <ThemeProvider theme={theme}>
            <App />
          </ThemeProvider>
        </HashRouter>
      </RecoilRoot>
    </>
  );
}

export default Root;

createTheme는 mui에서 제공하는 테마를 생성하는 함수다. 해당 함수로 테마를 생성한 뒤에 primary의 main색상과 contrastText색을 바꿔주고 background컬러도 바꿔주었다. 해당 테마를 앱에 적용시키기 위해서는 app을 ThemeProvider태그로 감싸야한다.

 

기존에 작성했던 코드와 새로 변경할 코드를 비교하는게 어렵다면 아래 사이트를 이용하는 것도 도움이 된다.

https://www.diffchecker.com

 

Diffchecker - Compare text online to find the difference between two text files

Diffchecker will compare text to find the difference between two text files. Just paste your files and click Find Difference!

www.diffchecker.com

예시로 이전포스팅까지의 Root.js와 현재 포스팅의 Root.js를 diffchecker로 비교하였다.

https://www.diffchecker.com/sYexaJDn/

 

Diffchecker - Compare text online to find the difference between two text files

Diffchecker will compare text to find the difference between two text files. Just paste your files and click Find Difference!

www.diffchecker.com

diffchecker

한눈에 알아보기 편하니 해당 사이트를 북마크에 추가해 두는 편이 도움이 될 거라 생각한다. 

그리고 추가로 Header의 NavLink의 Css를 살짝 수정해준다.

import { AppBar, Toolbar } from "@mui/material";
import { hover } from "@testing-library/user-event/dist/hover";
import { NavLink, useLocation } from "react-router-dom";

export default function Header() {
  const location = useLocation();
  return (
    <>
      <div>
        <AppBar position="static">
          <Toolbar>
            <ul className="flex flex-1 justify-between">
              <li className="text-xl font-bold">logo</li>
              <li>
                <span className="text-xl font-bold">Happy Note</span>
              </li>
              <li>
                {location.pathname == "/main" && (
                  <NavLink to="/write" className="hover:font-bold hover:underline">할일 추가</NavLink>
                )}
                {location.pathname == "/write" && (
                  <NavLink to="/main" className="hover:font-bold hover:underline">다음에 할래요</NavLink>
                )}
              </li>
            </ul>
          </Toolbar>
        </AppBar>
      </div>
    </>
  );
}

자잘한 수정은 끝났고 본격적으로 메인화면에 표시해 줄 리스트에 대한 컴포넌트들을 생성해 보자.

 

component의 폴더 안에 TodoListItem.js파일을 생성해준 뒤 다음과 같이 코드를 써준다.

import { Chip, Divider } from "@mui/material";
export default function TodoListItem({todo}) {
  console.log(JSON.stringify(todo.id));
  return (
    <>
      <li className="mt-5 mx-20  text-4xl">
        <div className="flex flex-col gap-3">
          <div className="flex gap-2">
            <Chip label={`번호 : ${todo.id}`} variant="filled" color="primary" />
            <Chip label={todo.regDate} variant="outlined" color="primary"/>
          </div>
          <div className="rounded-lg shadow-md p-2 flex items-baseline gap-3">
            <i className="fa-solid fa-check text-[#dcdcdc]" />
            <Divider
              orientation="vertical"
              variant="middle"
              flexItem
              sx={{ background:'#dcdcdc', width:'3px'}}
            />
            <div className="hover:text-[#990011] whitespace-pre-wrap leading-relaxed flex-grow items-center">
              {todo.content}
            </div>
          </div>
        </div>
      </li>
    </>
  );
}

mui의 chip과 didvider를 사용해서 간단하게 디자인하였다.

https://mui.com/material-ui/react-chip/

 

React Chip component - Material UI

Chips are compact elements that represent an input, attribute, or action.

mui.com

https://mui.com/material-ui/react-divider/

 

React Divider component - Material UI

The Divider component provides a thin, unobtrusive line for grouping elements to reinforce visual hierarchy.

mui.com

List 안에 들어갈 Item컴포넌트를 완성했으니 이제 item을 넣어줄 list 컴포넌트를 생성해 준다.

component의 폴더 안에 TodoList.js파일을 생성해준 뒤 다음과 같이 코드를 써준다.

import { useTodoState } from "../states";
import TodoListItem from "./TodoListItem";
export default function TodoList() {
  const todoState = useTodoState();
  const todoData = todoState.todoData;
  return (
    <>
      <ul>
        {todoData.todos.map((todo, index)=> {
          return <TodoListItem todo={todo} key={todo.id}/>
        })}
      </ul>
    </>
  );
}

map함수를 이용하여 TodoListItem을 생성해 주었다.

 

이제 메인페이지에 해당 리스트 컴포넌트를 적용시켜 보자. MainPage.js파일을 다음과 같이 수정해 준다.

import TodoList from "../components/TodoList";
import TodosEmpty from "../components/TodosEmpty";
import { useTodoState } from "../states";

function MainPage() {
  const todoState = useTodoState();
  const todoData = todoState.todoData;
  const todosEmpty = todoData.todos.length == 0;

  if (todosEmpty) {
    return <TodosEmpty />;
  }

  return (
    <>
      <TodoList />
    </>
  );
}

export default MainPage;

todosEmpty는 todoData의 todos가 비어있을 때만 표시되게끔 변경하였고 MainPage의 리턴에는 TodoList를 적용하였다. 적용된 화면을 보기 위해서 Todo를 추가하고 메인화면으로 이동하면 다음과 같이 정상적으로 리스트가 표기되는 것을 확인할 수 있다.

MainPage

이로써 할 일 추가 후 메인페이지에 리스트를 표시해 주는 기능은 모두 완성되었으나 테스트를 위해 할 일 추가 시 일일이 날짜를 지정해 주는 것이 귀찮아 기본값으로 오늘 날짜를 넣어주기로 했다. 물론 변경 가능하다. 현재 날짜와 시간을 구해 포맷을 바꿔주는 util함수를 먼저 만들어 준다.

 

src에 util폴더가 없으면 util폴더를 먼저 생성하고 해당 폴더에 commonUtil.js를 만든 뒤 다음과 같이 코드를 써준다.

export function getCurrentDateTime() {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0"); // 월은 0부터 시작하므로 +1
  const day = String(now.getDate()).padStart(2, "0");
  const hours = String(now.getHours()).padStart(2, "0");
  const minutes = String(now.getMinutes()).padStart(2, "0");
  return `${year}-${month}-${day}T${hours}:${minutes}`;
}

new Date();함수를 통해 현재 시간을 가져오는데 가져온 현재시간을 바로 사용할 수가 없어 연월일시분초를 나누어 문자열로 다시 조합해 주는 함수이다. now.get... 을 사용하여 연월일시분초를 가져올 수 있고 getMonth 같은 경우는 0부터 시작이기 때문에 + 1을 해주어야 한다. padStart(2, "0") 은 2자리가 될 때까지 왼쪽부터 0을 채우는 함수인데 쉽게 말해서 월, 일, 시, 분, 초가 한자리일 때 앞을 0으로 메워주는 함수다.

 

이후 index.js파일을 동일한 util폴더에 생성 후에 해당 index.js파일을 생성후 다음과 같이 코드를 써준다.

export * from "./commonUtil";

이후 WritetodoForm.js파일을 다음과 같이 수정한다.

import { faMarker } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, TextField } from "@mui/material";
import { useSnackBarState, useTodoState } from "../states";
import { useEffect } from "react";
import { getCurrentDateTime } from "../util";

export default function WriteTodoForm() {
  const todoState = useTodoState();
  const snackBarState = useSnackBarState();
  const onSubmit = (e) => {
    e.preventDefault();
    const form = e.target;
    if (form.regDate.value.length == 0) {
      alert("언제 해야 하는지 알려 주세요.");
      return;
    }
    if (form.content.value.length == 0) {
      alert("무엇을 해야 하는지 알려 주세요.");
      return;
    }
    const newTodoId = todoState.addTodos(form.regDate.value, form.content.value);
    form.content.value = getCurrentDateTime();
    form.content.value = "";
    snackBarState.openSnackBar(`${newTodoId}번 할일이 추가 되었습니다.`);

    console.log("WriteTodoFrom onSubmit End");
  };
  useEffect(() => {
    console.log("todos updated!! : " + JSON.stringify(todoState.todoData.todos));
  },[todoState.todoData.todos])
  return (
    <>
      <form onSubmit={onSubmit} className="flex flex-1 gap-5 flex-col p-16">
        <TextField
          type="datetime-local"
          label="언제 해야 되나요?"
          focused
          name="regDate"
          value={getCurrentDateTime()}
        />
        <TextField
          name="content"
          type="text"
          label="무엇을 해야 하나요?"
          multiline
          className="flex flex-1"
          slotProps={{
            input: { className: "flex-1 flex-col" },
            htmlInput: { className: "flex-1" },
          }}
        />
        <Button type="submit" variant="contained">
          {/* <i class="fa-solid fa-marker"></i> */}
          <FontAwesomeIcon icon={faMarker} className="mr-2" />
          <span>할일추가</span>
        </Button>
      </form>
    </>
  );
}

https://www.diffchecker.com/3DTov5FA/

 

Diffchecker - Compare text online to find the difference between two text files

Diffchecker will compare text to find the difference between two text files. Just paste your files and click Find Difference!

www.diffchecker.com

위와 같이 수정이 완료되면 다음과 같이 할 일추가 시 날짜지정이 현재 시간으로 디폴트 지정되어 있다.

날짜 default설정

https://github.com/TaeJinAn/happy_note/commit/ffc9951741c7b10c60dfdd5d61739883395dec43#diff-87d7a96211bcbd52297c8151003ac3b9ac8a1003b7aac5a41337d261a6 a0233eR1

 

todo 추가하기 기능 완성. todolist 메인화면에 표시하기. 기타 버튼 이름수정. 디폴트 날짜지정을

taejin.ahn committed Sep 9, 2024

github.com

 

이제 체크기능을 활성화하기 전에 필요한 라이브러리를 설치해 준다. 필요한 라이브러리는 immer와 classNames.

[ctrl + shift + ~]를 상용하여 터미널로 이동하여 다음과 같이 입력한다.

npm i classnames
npm i immer

classnames는 className props를 더욱더 작업하기 쉽게 도와주는 라이브러리이며 immer는 데이터의 불변성을 유지해 주면서 데이터를 핸들링하기 쉽게 도와주는 라이브러리이다.

https://github.com/JedWatson/classnames

 

GitHub - JedWatson/classnames: A simple javascript utility for conditionally joining classNames together

A simple javascript utility for conditionally joining classNames together - JedWatson/classnames

github.com

https://immerjs.github.io/immer/installation

 

Installation | Immer

<div

immerjs.github.io

immer는 공식페이지의 update patterns를 참고하면 손쉽게 사용할 수 있다.

 

라이브러리 설치 이후 states.js파일에 check기능을 추가해 준다. 다음과 같이 파일을 수정한다.

import { useRef, useState } from "react";
import { useRecoilState } from "recoil";
import { snackbarAtom, todosAtom } from "./atoms";
import { produce } from "immer";

export function useTodoState() {
  const [todoData, setTodoData] = useRecoilState(todosAtom);
  const lastTodoIdRef = useRef(todoData.lastTodoId);

  lastTodoIdRef.current = todoData.lastTodoId;

  const addTodos = (regDate, content) => {
    console.log(regDate, content, "start addTodos");
    const id = ++lastTodoIdRef.current;
    const newTodo = {
      regDate: regDate,
      content: content,
      id: id,
      checked: false,
    };
    setTodoData({
      ...todoData,
      lastTodoId: id,
      todos: [newTodo, ...todoData.todos],
    });
    console.log("addTodos End todos");
    return id;
  };

  const checkTodo = (id) => {
    console.log("checkTodo start!!");
    setTodoData(
      produce(todoData, (draft) => {
        const index = draft.todos.findIndex((todo) => todo.id == id);
        if (index != -1) {
          draft.todos[index].checked = !draft.todos[index].checked;
        }
      })
    );
  };

  return {
    addTodos,
    checkTodo,
    todoData,
  };
}

export function useSnackBarState() {
  const [snackbarData, setSnackbarData] = useRecoilState(snackbarAtom);
  const handleClose = () => {
    setSnackbarData({ ...snackbarData, open: false });
  };
  const openSnackBar = (msg, severity = "success", duration = 6000) => {
    console.log("openSnackBar start!!!");
    setSnackbarData({
      open: true,
      severity: severity,
      duration: duration,
      msg: msg,
    });
  };

  return {
    snackbarData,
    handleClose,
    openSnackBar,
  };
}

checkTodo가 추가되었고 immer를 사용하여 데이터의 불변성을 손쉽고 간편하게 관리하고 있다.

 

TodoListItem.js 컴포넌트를 다음과 같이 수정해 준다.

import { Button, Chip, Divider } from "@mui/material";
import classNames from "classnames";
import { useTodoState } from "../states/states";

export default function TodoListItem({ todo }) {
  const todoState = useTodoState();
  console.log(JSON.stringify(todo.id));
  return (
    <>
      <li className="mt-5 mx-20  text-4xl">
        <div className="flex flex-col gap-3">
          <div className="flex gap-2">
            <Chip
              label={`번호 : ${todo.id}`}
              variant="filled"
              color="primary"
            />
            <Chip label={todo.regDate} variant="outlined" color="primary" />
          </div>
          <div className="rounded-lg shadow-md flex items-center">
            <Button color="inherit" className="rounded-lg flex-shrink-0" onClick={() => {todoState.checkTodo(todo.id);}}>
              <span
                className={classNames(
                  { "text-[#990011]": todo.checked },
                  { "text-[#dcdcdc]": !todo.checked },
                  "text-4xl", "h-[80px]","flex items-center"
                )}
              >
                <i className="fa-solid fa-check " />
              </span>
            </Button>
            <Divider
              orientation="vertical"
              variant="middle"
              flexItem
              sx={{ background: "#dcdcdc", width: "3px" }}
            />
            <div className="hover:text-[#990011] whitespace-pre-wrap leading-relaxed flex-grow items-center p-3">
              {todo.content}
            </div>
          </div>
        </div>
      </li>
    </>
  );
}

checkTodo함수를 사용하기 위해 TodoListItem에 useTodoState() 훅을 선언하는 부분을 추가하였고 check아이콘이 들어간 부분을 클릭 시 action과 체크 아이콘의 색깔변경을 위해 수정하였다.

체크

체크 시 todo데이터에 저장됨은 물론 체크까지 활성화되고 있는 것을 확인해 보았다.

https://github.com/TaeJinAn/happy_note/commit/297144151974dd4e8226b94e0326569cc96a1be7

 

리스트의 체크기능 추가, 불변성 업데이트를 위한 immer설치, css조작을 위한 classnames설치 · TaeJinAn

taejin.ahn committed Sep 9, 2024

github.com

이번 포스팅은 할 일을 추가하고 메인 페이지에 추가한 할일 리스트를 띄우는 것과 리스트의 체크기능까지 활성화시켰다. 

 

개발한 것을 페이지에 글로 하나하나 옮겨 적으려니 의외로 양이 많다. 회사 다닐 때 인수인계서를 작성하는 느낌이 들기도 한다. 내 글을 읽는 사람이 하나라도 더 쉽게 이해했으면 하는 마음으로 포스팅을 작성하지만 생각보다 내용이 많아져 가독성이 안 좋지 않을까 하는 걱정이 든다. 그래도 최대한 개발할 때 자주 사용한 사이트라던지 사용한 라이브러리에 대한 설명들을 해놓았으니 주의 깊게 읽어주었으면 좋겠다. 이제 앞으로 할 일리스트의 삭제 수정 기능만 남았다 조금만 힘내면 자신이 만든 할일리스트 앱을 볼 수 있다. 글을 읽는 독자들도 나도 모두 파이팅이다.