요르딩딩
섹션9. 프로젝트2. TODO 리스트 본문
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