공부 및 일상기록

[React] TodoList 만들기 본문

개발/React

[React] TodoList 만들기

낚시하고싶어요 2022. 10. 4. 14:50

먼저 완성된 TodoList는 다음과 같다.

기능 1. 제목과 내용을 입력받아 추가하기 버튼을 누르면 Working에 추가된다.

기능 2. 추가하기 버튼을 누르면 제목과 내용 Input 값을 초기화 하여 비워준다.

기능 3. Working에 들어온 게시물을 완료 버튼을 누르면 Done으로 내려가게 한다.

기능 4. 취소버튼을 누르면 반대로 다시 Working으로 올라간다.

기능 5. 삭제하기 버튼을 누르면 해당 게시물이 삭제된다.


이제 컴포넌트를 어떻게 구성하였는지 먼저 살펴본다.

위 구성대로 진행하였고 진행순서는 1. 컴포넌트 구성-> 2. html 구조 작성-> 3. props와 state 작성 및 함수 작성  의 순서대로 진행하였다.

 


App.js

간단한 코드는 설명없이 지나친다.

import React from "react";
import TodoList from "./pages/TodoList";

function App() {
  return <TodoList />;
}

export default App;

 


TodoList.js

import Layout from "../components/layout/Layout";

function TodoList() {
  return <Layout/>;
}
export default TodoList;

Layout.js

import React, { useState } from "react";
import "./style.css";
import Header from "../header/Header";
import Form from "../form/Form";
import List from "../list/List";

const Layout = () => {
  //투두리스트에서 사용할 리스트들을 저장할 상태를 todos란 스테이트로 선언
  const [todos, setTodos] = useState([]);

  return (
    <div className="layout">
      <Header />
      <Form todos={todos} setTodos={setTodos} />
      <List todos={todos} setTodos={setTodos} />
    </div>
  );
};
export default Layout;

todos 라는 스테이트를 만들었다.

이는 리스트에 붙일 '할 일'(to do) 들이 들어있는 배열 형태이다.

각각 Form과 List에 props로 전달해줬다.


Header.js

import React from "react";
import "./style.css";
function Header() {
  return (
    <div className="header">
      <div>My Todo List</div>
      <div>React</div>
    </div>
  );
}
export default Header;

Form.js

import React, { useState } from "react";

import "./style.css";

//ID값 입력해 줄 넘버
let number = 0;

//Layout으로부터 setTodos, todos를 props로 받아옴
function Form({ setTodos, todos }) {
  //초기화를 위한 초기상태를 선언
  const initialState = {
    id: undefined,
    title: "",
    content: "",
    isDone: false,
  };
  //투두리스트에 들어갈 각각의 투두를 스테이트에 저장하기 위해 useState 사용
  const [todo, setTodo] = useState(initialState);

  //Input에서 onChange된 값을 e.target.value형태로 name과 value를 받아옴
  const onChangeHandler = (e) => {
    const { name, value } = e.target;
    //todo를 Input에서 받아온 값으로 변경 ---> onchange가 발생할 때 마다 setTodo가 실행됨
    //여기서 name은 title과 content 두가지가 있으므로 대괄호로 감싸줌
    setTodo({ ...todo, [name]: value });
  };

  //추가하기 버튼의 onclick에 사용될 함수 선언
  const addButton = () => {
    //Todos 스테이트에 onchange로 받아온 todo를 추가함, id값도 각각 부여함
    setTodos([...todos, { ...todo, id: number }]);
    number = number + 1;
    //input창 초기화를 위해 Todos스테이트 변경 후 Todo를 초기화함
    setTodo(initialState);
  };

  return (
    <div className="form">
      <div className="input-group">
        <label>제목</label>
        <input
          type="text"
          onChange={onChangeHandler}
          //value를 onclick함수가 발동될때마다 초기화 시켜주기 위해 todo.title을 받아옴
          value={todo.title}
          name="title"
        ></input>
        <label>내용</label>
        <input
          type="text"
          onChange={onChangeHandler}
          //value를 onclick함수가 발동될때마다 초기화 시켜주기 위해 todo.content을 받아옴
          value={todo.content}
          name="content"
        ></input>
      </div>
      <button onClick={addButton}>추가하기</button>
    </div>
  );
}
export default Form;

코드 설명

let number는 고유 key값을 부여하기 위해 선언한 변수이다.

 

Layout으로부터 setTodos와 todos props를 받아서 사용한다.

여기서 props 라고 써서 받지 않고 바로 setTodos 와 todos처럼 보내준 키값을 써서 받으면 함수 안에서 변수처럼 사용할 수 있다. (그렇지 않으면 props.setTodos, props.todos 처럼 귀찮게 다 써야한다..)

 

const initialState는 아래에 만들 todo 스테이트를 초기화 하기 위해서 선언하였다.

id: 고유 키값을 주거나 각각의 todo 컴포넌트를 비교하기 위해 사용

title: 제목 

content: 내용

isDone: 이는 해당 todo가 완료가 되었는지 판별하기 위해 넣었고, 처음 작성된 게시물은 모두 false이다.

 

todo라는 스테이트를 만들었고 초기값으로 initialState를 받았다.

 

const onChangeHandler는 인풋에서 발생한 onChange 이벤트를 todo 스테이트에 저장하기 위한 함수이다.

event가 발생하면 event.target의 데이터를 setTodo를 이용하여 저장하는데 이때 todo는 전개연산자로 객체를 풀어두고, [name]: value 처럼 키: 값 형태로 각 name별 value를 저장한다. 이때 name에 대괄호를 쓴 이유는 아래에 인풋이 제목과 내용 두가지 인데 그 두가지의 이름이 다르기때문에 대괄호를 사용했다.

 

const addButton은 추가하기 버튼의 onClick 함수이다.

setTodos를 사용해 현재 onChange로 입력된 todo 스테이트를, todos 스테이트에 저장한다.

이때 id:number 라는 고유값을 하나 저장해두는데 이는 나중에 key 값으로 사용하면서 동시에 다른 함수에서 동일한 객체 찾기에 사용된다. 여기서 number는 코드 맨 위에 선언한 그 number 이고, 한번 저장될때마다 number+1을 시켜준다.

마지막으로 setTodo를 이용해서 todo를 initialState로 초기화 시켜준다.

 

**여기서 나는 onClick을 이용하여 추가하기를 사용하였는데 만약 form 형태로 onSubmit을 이용하였다면 버튼을 눌러 저장됨과 동시에 페이지가 새로고침되어 아무 일도 일어나지 않게 될 것이다. 이때 onSubmit 아래에 event.preventDefault() 를 추가하여 submit은 되지만 새로고침이 되지 않도록 막아야 한다.

 

input박스에 value가 {todo.title}, {todo.content}인이유는 추가하기 버튼을 누르면 input박스를 비워주기 위해 초기화 시켜준 것이다. (위에 setTodo로 initialState상태로 돌리면서 그 초기값을 밸류에 넣은것)


List.js

import Todo from "../todo/Todo";
//todos를 Layout으로부터 받아옴
function List({ todos, setTodos }) {
  return (
    <div className="listContainer">
      <h2>Working...🔥</h2>

      <div>
        {todos.map((a) => {
          //todos에 연결된 투두객체들의 리스트를 맵함수로 반복시킴
          if (a.isDone === false) {
            return <Todo a={a} todos={todos} setTodos={setTodos} key={a.id} />; //props로 내려줄때 반드시 각각의 고유한 key를 보내줌 (index번호는 안됨, 참조하는 값이 달라져도 인지못함)
          } else {
            return null;
          }
        })}
      </div>
      <h2>Done..✔</h2>
      <div>
        {todos.map((a) => {
          if (a.isDone === true) {
            return <Todo a={a} todos={todos} setTodos={setTodos} key={a.id} />;
          } else {
            return null;
          }
        })}
      </div>
    </div>
  );
}
export default List;

코드 설명

Layout으로부터 todos, setTodos를 props로 받아서 사용한다.

Working부분은 isDone이 모두 false인 게시물이 들어와야 하므로 조건문에서 a.isDone===flase 조건을 걸었다.

여기서 a는 map에서 콜백된 각각의 원소를 의미하고, 이 원소는 각각의 todo들 이므로 객체 형태이다.

else값으로는 빈 화면을 출력하기위해 null을 입력했다.

Done 부분은 위와 반대의 조건으로 동일한 map함수를 사용했다.

두 부분 모두 리턴값으로 Todo.js를 불러오면서 동시에 props로 a, todos, setTodos, key를 넘겨준다.


Todo.js

import "./style.css";

function Todo({ a, todos, setTodos }) {
  const deleteTodo = () => {
    //삭제하기 버튼의 onclick함수, filter를 통해 a.id(list로부터 받아온 각각의 todo)와 콜백으로 불러온 todos의 각원소들의 id가 일치하는경우 그 원소 삭제
    let newDeleteTodo = todos.filter((todo) => {
      return a.id !== todo.id;
    });
    //필터링된 배열로 다시 setTodos를 이용해 todos를 변경함
    setTodos(newDeleteTodo);
  };

  //완료버튼의 onclick함수, map을 이용해 a.id(list에서 받아온 각각의 todo)와 콜백으로 불러온 todos의 각원소들의 id가 일치하는 경우를 찾아서 변경함
  const done = () => {
    let newDone = todos.map((todo) => {
      if (a.id === todo.id) {
        //콜백된 함수에 isDone을 원래값의 반대로 반환함( !todo.isDone  --> true는 false로 , false는 true로)
        return {
          ...todo,
          isDone: !todo.isDone,
        };
      } else {
        return { ...todo };
      }
    });
    setTodos(newDone);
  };

  return (
    <div className="todo-container">
      <h3>{a.title}</h3>
      <p>{a.content}</p>
      <div className="todobtn">
        <button onClick={deleteTodo}>삭제하기</button>
        <button onClick={done}>{a.isDone === true ? "취소" : "완료"}</button>
      </div>
    </div>
  );
}
export default Todo;

코드 설명

List로부터 a, todos, setTodos를 props로 받아 사용한다.

let newDeleteTodo는 todos에 있는 todo들을 filter()함수를 이용해 필터링 된 값을 저장할 변수 선언이다.

필터링 할 값은 a.id(해당 게시물의 id)가 todo.id(필터함수가 불러온 원소의 id)를 비교해 일치하는 부분을 제외하고 모두 저장하는 방식이다. (이 말은 일치하는 원소만 삭제한다는 말이다. 그러면 todos 내부에 해당 원소가 사라지므로 게시된 글이 사라지게 되는 방식이다.)

setTodos를 사용해 삭제되고 남은 나머지 원소들을 다시 todos로 바꾼다.

 

const done은 완료버튼의 onClick 함수이다.

newDone 함수는 todos를 map으로 돌려서 해당 게시글의 id값 (a.id)과 map으로 콜백한 todo들의 id값(todo.id)이 일치하는 게시글은 찾아서 isDone을 false였다면 true로, true였다면 false로 바꾸고, id가 일치하지 않는 값들은 모두 그 상태 그대로 반환하여 새롭게 setTodos를 이용해 변경한다. 쉽게 생각하면 todos의 모든 원소를 꺼내서 완료버튼을 누른 게시글의 id값과 일치하는 원소를 찾은 뒤 해당 원소의 isDone값만 바꾼 뒤 다시 todos에 집어 넣는 형식이다.

 

** 위 함수는 사실 List.js에서 선언했다면 더욱 좋았을 것이라는 피드백을 받았다. 그 이유는 List에서 todos를 props로 받아와서 사용하는데, 만약 todos가 정말 큰 배열이였다면 props로 전달 받는것이 조금이나마 더 느리기 때문이다. 만약 List에 함수를 선언했다면 클릭할때의 a.id(해당 게시글의 id)를 함수의 파라미터로 입력해서 진행하면 되기 때문에 굳이 props 전달이 필요하지 않게 된다.

 

완료 및 취소버튼은 삼항연산자를 사용하여 해당 게시글의 isDone값이 true이면 취소, false이면 완료를 나타나도록 하였다. (isDone이 true이면 해당 게시글은 이미 완료된 상태이므로 취소버튼을 필요로 하고 false라면 완료하지 않았으므로 완료시 완료버튼을 누를수 있도록 하는것이다)


리액트를 입문하고 3일동안 공부하며 만든 결과물이다. 일단 기능 구현을 해내자 라는 생각으로 만들었고, 따라서 해당 코드들은 당연하게 미숙한점이 많을 것이라고 생각한다. 많은 유튜브와 예시로 주어진 todolist페이지, 그리고 God글을 통해 막막했던 숙제가 하나하나 풀려나가고 완전히 다 풀리고 나니 기분이 너무 홀가분하다. 하지만 만들면서 내가 자바스크립트에 대해 얼마나 기초가 없는지도 알게되었고, 리액트도 만만하지 않다는것을 느끼고 더욱 열심히 해야겠다는 생각이 들었다.