요르딩딩

섹션9. 프로젝트2. TODO 리스트 본문

[강의]/[한 입 크기로 잘라 먹는 리액트(React.js)]

섹션9. 프로젝트2. TODO 리스트

요르딩딩 2025. 4. 2. 21:57
728x90
반응형

8.1) 프로젝트 소개 및 준비

8.2) UI 구현하기

8.3) 기능 구현 준비하기

# App

import "./App.css";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";

function App() {
  return (
    <div className="App">
      <Header />
      <Editor />
      <List />
    </div>
  );
}

export default App;

 

.App {
  width: 500px;
  margin: 0 auto;

  display: flex;  // 자식요소의 배치를 유연하게 해줌
  flex-direction: column; // 열 기준 세로로 배치해줌
  gap: 10px; // 간격
}

 

# Header

import "./Header.css";

const Header = () => {
  return (
    <div className="Header"> // CSS 불러오기
      <h3>오늘은 📆</h3>
      <h1>{new Date().toDateString()}</h1>
    </div>
  );
};

export default Header;
.Header > h1 {
  color: rgb(37, 147, 255);
}

 

# Editor

import "./Editor.css";

const Editor = () => {
  return (
    <div className="Editor"> // CSS
      <input placeholder="새로운 Todo..." /> // 흐린문구
      <button>추가</button>
    </div>
  );
};

export default Editor;
.Editor {
  display: flex;
  gap: 10px;
}

.Editor input {
  flex: 1;
  padding: 15px;
  border: 1px solid rgb(220, 220, 220);
  border-radius: 5px;
}

.Editor button {
  cursor: pointer;
  width: 80px;
  border: none;
  background-color: rgb(37, 147, 255);
  color: white;
  border-radius: 5px;
}

 

# List

import "./List.css";
import TodoItem from "./TodoItem";

const List = () => {
  return (
    <div className="List">  //CSS
      <h4>Todo List 🌱</h4>
      <input placeholder="검색어를 입력하세요" /> //  흐린문구
      <div className="todos_wrapper">
        <TodoItem />
        <TodoItem />
        <TodoItem />
      </div>
    </div>
  );
};

export default List;
.List {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.List > input {
  width: 100%;
  border: none;
  border-bottom: 1px solid rgb(220, 220, 220);
  padding: 15px 0px;
}

.List > input:focus {
  outline: none;
  border-bottom: 1px solid rgb(37, 147, 255);
}

.List .todos_wrapper {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

 

# TODO Item

import "./TodoItem.css";

const TodoItem = () => {
  return (
    <div className="TodoItem">  // CSS
      <input type="checkbox" />  // 체크박스
      <div className="content">Todo...</div>
      <div className="date">Date</div>
      <button>삭제</button>
    </div>
  );
};

export default TodoItem;
.TodoItem {
  display: flex;
  align-items: center;
  gap: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid rgb(240, 240, 240);
}

.TodoItem input {
  width: 20px;
}

.TodoItem .content {
  flex: 1;
}

.TodoItem .date {
  font-size: 14px;
  color: gray;
}

.TodoItem button {
  cursor: pointer;
  color: gray;
  font-size: 14px;
  border: none;
  border-radius: 5px;
  padding: 5px;
}

 

8.4) Create - 투두 추가하기

import "./App.css";
import { useRef, useState } from "react";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";

const mockData = [
  {
    id: 0,
    isDone: false,
    content: "React 공부하기",
    date: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "빨래하기",
    date: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "노래 연습하기",
    date: new Date().getTime(),
  },
];

function App() {
  const [todos, setTodos] = useState(mockData); // 리스트 데이터!!!
  const idRef = useRef(3); // id를 기억하기위한 값!!!

  const onCreate = (content) => { // Edit(하위)에서 받은 값을 추가 가능!!!
    const newTodo = {
      id: idRef.current++, // id값 증가!!!
      isDone: false,
      content: content,
      date: new Date().getTime(),
    };

    setTodos([newTodo, ...todos]); // 앞에 새로운거 + 기존 그대로!!!
  };

  return (
    <div className="App">
      <Header />
      <Editor onCreate={onCreate} /> // 함수 Props로 넘기기
      <List />
    </div>
  );
}

export default App;
import { useRef, useState } from "react";
import "./Editor.css";

const Editor = ({ onCreate }) => { // App으로부터 함수 Props로 받기!!! (상위로 값 넘기기)
  const [content, setContent] = useState("");
  const inputRef = useRef();

  const onChangeContent = (e) => { // 입력한 값 세팅!!!
    setContent(e.target.value);
  };

  const onKeydown = (e) => { // 엔터키 입력시 동작!!!
    if (e.keyCode === 13) {
      onSubmit();
    }
  };

  const onSubmit = () => {
    if (content === "") {
      inputRef.current.focus(); // 마우스 포커싱!!!
      return; // 빈값이면 미동작!!!
    }
    onCreate(content); // 세팅된값 App(상위)로 값 넘기기 위함!!!
    setContent(""); // 제출 후 공백으로!!!
  };

  return (
    <div className="Editor">
      <input
        ref={inputRef}
        value={content}
        onChange={onChangeContent}
        onKeyDown={onKeydown}
        placeholder="새로운 Todo..."
      />
      <button onClick={onSubmit}>추가</button>
    </div>
  );
};

export default Editor;

 

8.5) Read - 투두리스트 렌더링하기

import "./App.css";
import { useRef, useState } from "react";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";

const mockData = [
  {
    id: 0,
    isDone: false,
    content: "React 공부하기",
    date: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "빨래하기",
    date: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "노래 연습하기",
    date: new Date().getTime(),
  },
];

function App() {
  const [todos, setTodos] = useState(mockData);
  const idRef = useRef(3);

  const onCreate = (content) => {
    const newTodo = {
      id: idRef.current++,
      isDone: false,
      content: content,
      date: new Date().getTime(),
    };

    setTodos([newTodo, ...todos]);
  };

  return (
    <div className="App">
      <Header />
      <Editor onCreate={onCreate} />
      <List todos={todos} />
    </div>
  );
}

export default App;
import "./List.css";
import TodoItem from "./TodoItem";
import { useState } from "react";

const List = ({ todos }) => {
  const [search, setSearch] = useState(""); // 1. 검색어 저장!!!

  const onChangeSearch = (e) => {  // 1. 검색어 저장!!!
    setSearch(e.target.value);
  };

  const getFilteredData = () => { // 2. 검색어 필터링!!!
    if (search === "") {
      return todos; 
    }
    return todos.filter((todo) =>
      todo.content
        .toLowerCase()  // 대소문자 구분 없애기!!!
        .includes(search.toLowerCase()) // 대소문자 구분 없애기!!!
    );
  };

  const filteredTodos = getFilteredData();  // 2.필터링된 결과갑 저장!!!

  return (
    <div className="List">
      <h4>Todo List 🌱</h4>
      <input
        value={search}
        onChange={onChangeSearch}
        placeholder="검색어를 입력하세요"
      />
      <div className="todos_wrapper">
        {filteredTodos.map((todo) => {  // 2. 콜백함수로 리스트형태로 랜더링!!!
          return <TodoItem key={todo.id} {...todo} />;  // 키값이 있어야함!!!
        })}
      </div>
    </div>
  );
};

export default List;
import "./TodoItem.css";

const TodoItem = ({ id, isDone, content, date }) => { // Porps 받기!!!
  return (
    <div className="TodoItem">
      <input readOnly checked={isDone} type="checkbox" /> // readOnly에러 해결(이번트핸들러 미작성)!!!
      <div className="content">{content}</div>
      <div className="date">
        {new Date(date).toLocaleDateString()} // 날짜 포맷팅!!!
      </div>
      <button>삭제</button>
    </div>
  );
};

export default TodoItem;

 

8.6) Update - 투두 수정하기

import "./App.css";
import { useRef, useState } from "react";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";

const mockData = [
  {
    id: 0,
    isDone: false,
    content: "React 공부하기",
    date: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "빨래하기",
    date: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "노래 연습하기",
    date: new Date().getTime(),
  },
];

function App() {
  const [todos, setTodos] = useState(mockData);
  const idRef = useRef(3);

  const onCreate = (content) => {
    const newTodo = {
      id: idRef.current++,
      isDone: false,
      content: content,
      date: new Date().getTime(),
    };

    setTodos([newTodo, ...todos]);
  };

// 체크박스 업데이트!!!
  const onUpdate = (targetId) => {
    // todos State의 값들 중에
    // targetId와 일치하는 id를 갖는 투두 아이템의 isDone 변경

    // 인수: todos 배열에서 targetId와 일치하는 id를 갖는 요소의 데이터만 딱 바꾼 새로운 배열
    setTodos(
      todos.map((todo) => // 간단하게 삼항연산자 사용!!!
        todo.id === targetId
          ? { ...todo, isDone: !todo.isDone }
          : todo
      )
    );
  };

  return (
    <div className="App">
      <Header />
      <Editor onCreate={onCreate} />
      <List todos={todos} onUpdate={onUpdate} /> // 함수 Props로 넘겨주기
      111
    </div>
  );
}

export default App;

 

import "./List.css";
import TodoItem from "./TodoItem";
import { useState } from "react";

const List = ({ todos, onUpdate }) => {
  const [search, setSearch] = useState("");

  const onChangeSearch = (e) => {
    setSearch(e.target.value);
  };

  const getFilteredData = () => {
    if (search === "") {
      return todos;
    }
    return todos.filter((todo) =>
      todo.content
        .toLowerCase()
        .includes(search.toLowerCase())
    );
  };

  const filteredTodos = getFilteredData();

  return (
    <div className="List">
      <h4>Todo List 🌱</h4>
      <input
        value={search}
        onChange={onChangeSearch}
        placeholder="검색어를 입력하세요"
      />
      <div className="todos_wrapper">
        {filteredTodos.map((todo) => {
          return (
            <TodoItem
              key={todo.id}
              {...todo}
              onUpdate={onUpdate} // App(상위)로부터 받은 Props TodoItem(하위)로 넘기기!!!
            />
          );
        })}
      </div>
    </div>
  );
};

export default List;
import "./TodoItem.css";

const TodoItem = ({
  id,
  isDone,
  content,
  date,
  onUpdate,
}) => {
  const onChangeCheckbox = () => { // 상위로 넘기기!!!
    onUpdate(id);
  };

  return (
    <div className="TodoItem">
      <input
        onChange={onChangeCheckbox} // input요소로 onChange임!!!
        readOnly
        checked={isDone}
        type="checkbox"
      />
      <div className="content">{content}</div>
      <div className="date">
        {new Date(date).toLocaleDateString()}
      </div>
      <button>삭제</button>
    </div>
  );
};

export default TodoItem;

 

8.7) Delete - 투두 삭제하기

import "./App.css";
import { useRef, useState } from "react";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";

const mockData = [
  {
    id: 0,
    isDone: false,
    content: "React 공부하기",
    date: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "빨래하기",
    date: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "노래 연습하기",
    date: new Date().getTime(),
  },
];

function App() {
  const [todos, setTodos] = useState(mockData);
  const idRef = useRef(3);

  const onCreate = (content) => {
    const newTodo = {
      id: idRef.current++,
      isDone: false,
      content: content,
      date: new Date().getTime(),
    };

    setTodos([newTodo, ...todos]);
  };

  const onUpdate = (targetId) => {
    // todos State의 값들 중에
    // targetId와 일치하는 id를 갖는 투두 아이템의 isDone 변경

    // 인수: todos 배열에서 targetId와 일치하는 id를 갖는 요소의 데이터만 딱 바꾼 새로운 배열
    setTodos(
      todos.map((todo) =>
        todo.id === targetId
          ? { ...todo, isDone: !todo.isDone }
          : todo
      )
    );
  };

// Item삭제하기!!!
  const onDelete = (targetId) => {
    // 인수: todos 배열에서 targetId와 일치하는 id를 갖는 요소만 삭제한 새로운 배열
    setTodos(todos.filter((todo) => todo.id !== targetId));
  };

  return (
    <div className="App">
      <Header />
      <Editor onCreate={onCreate} />
      <List
        todos={todos}
        onUpdate={onUpdate}
        onDelete={onDelete}
      />
    </div>
  );
}

export default App;
import "./List.css";
import TodoItem from "./TodoItem";
import { useState } from "react";

const List = ({ todos, onUpdate, onDelete }) => {
  const [search, setSearch] = useState("");

  const onChangeSearch = (e) => {
    setSearch(e.target.value);
  };

  const getFilteredData = () => {
    if (search === "") {
      return todos;
    }
    return todos.filter((todo) =>
      todo.content
        .toLowerCase()
        .includes(search.toLowerCase())
    );
  };

  const filteredTodos = getFilteredData();

  return (
    <div className="List">
      <h4>Todo List 🌱</h4>
      <input
        value={search}
        onChange={onChangeSearch}
        placeholder="검색어를 입력하세요"
      />
      <div className="todos_wrapper">
        {filteredTodos.map((todo) => {
          return (
            <TodoItem
              key={todo.id}
              {...todo}
              onUpdate={onUpdate}
              onDelete={onDelete} // 상위에서 받은 Props 하위로 넘기기!!!
            />
          );
        })}
      </div>
    </div>
  );
};

export default List;
import "./TodoItem.css";

const TodoItem = ({
  id,
  isDone,
  content,
  date,
  onUpdate,
  onDelete,
}) => {
  const onChangeCheckbox = () => {
    onUpdate(id);
  };

  // 삭제하기!!!
  const onClickDeleteButton = () => {
    onDelete(id);
  };

  return (
    <div className="TodoItem">
      <input
        onChange={onChangeCheckbox}
        readOnly
        checked={isDone}
        type="checkbox"
      />
      <div className="content">{content}</div>
      <div className="date">
        {new Date(date).toLocaleDateString()}
      </div>
      <button onClick={onClickDeleteButton}>삭제</button>
    </div>
  );
};

export default TodoItem;
728x90
반응형

'[강의] > [한 입 크기로 잘라 먹는 리액트(React.js)]' 카테고리의 다른 글

섹션11. 최적화  (0) 2025.04.07
섹션10. userReducer  (0) 2025.04.07
섹션8. 라이프사이  (0) 2025.04.02
섹션7. 프로젝트1. 카운터  (0) 2025.04.02
섹션6. React.js 입문  (0) 2025.03.28
Comments