08. TODO APP (Read, Create)

Todo APP 을 만들어봅니다

들어가기전에

앞서 배운 내용들을 바탕으로 Todo APP 을 만들어보려고 합니다. Todo 는 크게 추가, 삭제, 수정, 필터링으로 이루어집니다. 동작은 여기 에서 확인 가능합니다.

실습

프로젝트는 초기 세팅 상태로 진행되어집니다. 코드량을 최대한 줄이고자 style 은 적용하지 않습니다.

먼저 Todo List 를 가져와 보여주는 부분부터 진행해보겠습니다.

위의 이미지에서 Input 부분이 Header 이고, List 가 표현되는 부분이 Content, 아래 Filter 부분이 Footer 입니다. 크게 3 덩어리로 분리하려고합니다.

기본 구성

3개의 Component 를 추가합니다.

// src/components/header.js

import React from "react";

function Header() {
  return <div>Header</div>;
}

export default Header;
// src/components/contents.js

import React from "react";

function Contents() {
  return <div>Contents</div>;
}

export default Contents;
// src/components/footer.js

import React from "react";

function Footer() {
  return <div>Footer</div>;
}

export default Footer;

App.js 에서 가져오겠습니다.

// App.js 

import React from "react";

import Header from "./components/header";
import Contents from "./components/contents";
import Footer from "./components/footer";

function App() {
  return (
    <div>
      <Header />
      <Contents />
      <Footer />
    </div>
  );
}

export default App;

Todo State 를 추가합니다. Todo 는 텍스트, 상태를 가집니다. 그리고 수정, 삭제시 필요한 고유한 id 를 부여해줘야합니다. 클라이언트에서 고유한 값을 판단하기 가장 쉽고 좋은것은 Date 를 이용하는 것 입니다.

// App.js 

import React, { useState } from "react";

import Header from "./components/header";
import Contents from "./components/contents";
import Footer from "./components/footer";

function App() {
  // hook 을 이용하여 state 를 다룹니다.
  const [todos, setTodos] = useState([
    {
      id: Date.now(), // 고유한 id
      text: "리액트 공부하기", // 텍스트
      isDone: false // 완료, 미완료 상태
    },
    {
      id: Date.now(),
      text: "밥 먹기",
      isDone: true
    }
  ]);

  return (
    <div>
      <Header />
      <Contents />
      <Footer />
    </div>
  );
}

export default App;

만든 Todos State 를 해당 내용을 그려줄 Contents 에게 전달합니다.

// App.js

<Contents todos={todos} />

List 그리기

Contents 는 Props 로 Todo List 를 받습니다.

// src/components/contents.js 

import React from "react";

function Contents({ todos }) {
  return <div>{JSON.stringify(todos)}</div>;
}

export default Contents;

Props 로 넘어오는것을 확인 할 수 있습니다. 이제 받은 데이터를 바탕으로 List 를 그려줍니다.

Todo Component 를 추가합니다. 이 Component 는 Todo 하나에 대한 상태를 업데이트 할 수 있는 Checkbox 와 텍스트를 가지고 있습니다.

// src/components/contents.js 

import React from "react";

function Todo({ todo: { id, text, isDone } }) {
  return (
    <div>
      <input type="checkbox" checked={isDone} />
      <span>{text}</span>
    </div>
  );
}

function Contents({ todos }) {
  return (
    <div>
      {todos.map(todo => (
        <Todo todo={todo} key={todo.id} />
      ))}
    </div>
  );
}

export default Contents;

Checkbox 의 상태는 isDone 에 영향을 받습니다. 여기까지하면 일단 Read 는 다 했습니다. 위 처럼 map 같이 loop 를 돌면서 Component 를 그려줄 때는 항상 고유한 key 값이 필요합니다. => 지금은 샘플 데이터가 Date.now 를 동시에 호출해서 만들기 때문에 경고가 납니다. 뒤에 샘플 데이터를 제거하면 경고가 사라질거에요

Todo 추가

이제 Input 에 적은 글을 바탕으로 새로운 Todo 를 만들어내는 것을 해야합니다. 그 전에 생각해보아야 할 것이 있습니다.

Input 은 Header 에 있고 Todo List 의 State 는 App.js 즉 부모에 있습니다. => 즉, 자식에서 만든 데이터를 부모의 데이터에 추가해야 되는 일이 발생한 것 입니다. => 이럴때는 보통 데이터를 가지고 있는곳에서 함수를 만들어 자식에게 제공합니다. => Todo List 를 가진 App.js 에서 Todos 데이터를 다룰 수 있는 함수를 만들어 자식 Header 에게 제공합니다.

App.js 에 handleAdd 라고 불리는 함수를 추가합니다. 해당 함수는 text 를 받아 새로운 Todo 를 만들고 자신이 가진 todos 에 push 해주는 역할을 합니다. 이 함수를 자식 Header 에게 전달합니다.

=> 보통 내부에서 데이터를 다루는 함수는 handle, 외부에서 넘겨준 event 는 on 이라는 이름을 많이 사용합니다.

// app.js

import React, { useState } from "react";

import Header from "./components/header";
import Contents from "./components/contents";
import Footer from "./components/footer";

function App() {
  const [todos, setTodos] = useState([
    {
      id: Date.now(),
      text: "리액트 공부하기",
      isDone: false
    },
    {
      id: Date.now(),
      text: "밥 먹기",
      isDone: true
    }
  ]);

  const handleAdd = text => { 
    setTodos([ // 기존의 데이터와 text 를 바탕으로 만들어진 새로운 todo 를 합칩니다
      ...todos,
      {
        id: Date.now(),
        text,
        isDone: false
      }
    ]);
  };

  return (
    <div>
      <Header onChange={handleAdd} />
      <Contents todos={todos} />
      <Footer />
    </div>
  );
}

export default App;

이제 Header 를 만들어보겠습니다. Header 는 text 라는 State 를 가지고 있고 이 State 는 Input 에 값에 따라 변경됩니다.

// src/components/header.js

import React, { useState } from "react";

function Header({ onChange }) {
  const [text, setText] = useState("");

  const handleChange = e => { // input event 
    setText(e.target.value);
  };
  
  return (
    <div>
      <h1>TODO APP</h1>
      <input onChange={handleChange} value={text} />
    </div>
  );
}

export default Header;

이제 ADD 버튼을 추가하여 해당 버튼이 눌렸을 때 text 의 값을 부모에서 내려받은 onChange 에 전달하면 새로운 Todo 가 추가되는 것 입니다.

// src/components/header.js

import React, { useState } from "react";

function Header({ onChange }) {
  const [text, setText] = useState("");

  const handleChange = e => {
    setText(e.target.value);
  };

  const handleClick = () => {
    console.log("text", text);
  };

  return (
    <div>
      <h1>TODO APP</h1>
      <input
        type="text"
        onChange={handleChange}
        value={text}
        placeholder="오늘은 어떤 일을 하시나요?"
      />
      <button onClick={handleClick}>추가</button>
    </div>
  );
}

export default Header;

handleClick 함수를 조금만 수정해주면됩니다.

// src/components/header.js

const handleClick = () => {
  onChange(text);
  setText(""); // 추가 후 input 을 비워주기 위해 state 를 초기화 합니다.
};

그리고 App.js 에 있는 샘플 데이터를 제거합니다. todos 를 [] 로 만들어줍니다.

// app.js

import React, { useState } from "react";

import Header from "./components/header";
import Contents from "./components/contents";
import Footer from "./components/footer";

function App() {
  const [todos, setTodos] = useState([]); // 샘플 데이터 제거

  const handleAdd = text => {
    setTodos([
      ...todos,
      {
        id: Date.now(),
        text,
        isDone: false
      }
    ]);
  };

  return (
    <div>
      <Header onChange={handleAdd} />
      <Contents todos={todos} />
      <Footer />
    </div>
  );
}

export default App;

Last updated