※ 주말 포함 이틀에 걸쳐 만든 내용이라 스크롤이 길기 때문에 오른쪽 List에서 참고할 부분만 눌러서 보시길 추천드립니당😊
import { useState } from "react"
// 자식 컴포넌트 import
import Header from "./components/Header"
import Input from "./components/Input"
import TodoList from "./components/TodoList"
function App() {
// 새로운 todolist 생성 폼(초기값은 빈 배열)
const [todoList, setTodoList] = useState([])
return (
<div>
<Header>🥰 Habin's Todo List 😉</Header>
// Input 컴포넌트로 setTodoList 값 내보내기
<Input setTodoList={setTodoList}></Input>
// *** check 1 ***
// TodoList 컴포넌트로 todoList와 setTodoList 값 내보내기
<TodoList IsActive={false} todoList={todoList} setTodoList={setTodoList}></TodoList>
<TodoList IsActive={true} todoList={todoList} setTodoList={setTodoList}></TodoList>
</div>
)
}
// 컴포넌트 내보내기(export default)
export default App
// Header.jsx
// css import
import 'header.css';
function Header({children}) {
return (
<h1 className='header'>{children}</h1>
)
}
export default Header
// header.css
.header {
padding: 0.5rem;
color: rgb(0, 110, 255);
}
// Input.jsx
import { useState } from "react"
// 중복되지 않는 유일한 값으로 id 값을 주기 위해 사용 - 아래의 import를 꼭 해줘야함
import { v4 as uuidv4 } from "uuid"
import 'input.css';
// setTodoList 값 가져오기
function Input({setTodoList}) {
// 할일 제목 작성 폼
const [title, setTitle] = useState('')
// 할일 내용 작성 폼
const [contents, setContents] = useState('')
// *** check 2 ***
const onSubmit = (event) => {
// 폼 새로고침 방지
event.preventDefault();
// 입력 값이 빈 값일 경우 alert
if (title === '' || contents === '')
return alert('제목과 내용을 모두 입력해주세요');
const newTodo = {
// *** check 3 ***
id: uuidv4(),
title,
contents,
isDone: false,
}
// 입력 값을 제대로 가져오는지 체크하기 위한 console.log
console.log(newTodo)
// prev(전의 값) 뒤에 가져와서 새로운 todo를 아래에 붙임
setTodoList((prev) => [...prev, newTodo]);
// todo 추가 후에는 제목 란과 내용 란을 빈 값으로 만들기
setTitle('');
setContents('');
}
// 제목 입력 값 가져오는 함수
const onChangeTitle =(event) => {
setTitle(event.target.value)
}
// 내용 입력 값 가져오는 함수
const onChangeContents =(event) => {
setContents(event.target.value)
}
return (
<form onSubmit={onSubmit} className="input-form">
제목 <input className="input" type='text' value={title} onChange={onChangeTitle} placeholder='제목을 작성해주세요.' /> 
내용 <input className="input" type='text' value={contents} onChange={onChangeContents} placeholder='내용을 작성해주세요.' /> 
<button className="add-btn">추가하기</button>
</form>
)
}
export default Input
// input.css
.input-form {
padding: 1rem;
color: rgb(3, 82, 185);
font-weight: bold;
border-color: white;
margin-bottom: 2.5rem;
}
.add-btn {
background-color: white;
color: rgb(3, 82, 185);
font-weight: bold;
padding: 0.3rem 2rem 0.3rem 2rem;
border-radius: 7px;
margin-left: 3rem;
border-color: rgb(175, 217, 253);
}
// [추가하기] 버튼에 마우스 올리면 커서가 포인터로 변하고 색상도 변할 수 있도록 지정
.add-btn:hover {
cursor: pointer;
background-color: #b0d6fc;
border-color: aliceblue;
}
.input {
background-color: white;
height: 17px;
width: 200px;
padding: 0.4rem;
border: 2px solid rgba(0, 0, 0, 0.23);
border-radius: 7px;
}
// input 박스에 마우스를 올리면 테두리가 검은색으로 변하도록 지정
.input:hover {
border: 2px solid black;
}
// input 박스를 누르면 아웃라인은 none으로 없애고 테두리는 파란색으로 변하도록 지정
.input:focus-visible {
outline: none!important;
border: 2px solid #1976d2;
}
uuid v4를 사용하여 id에 고유 값을 배정하였다.
: uuid는 중복되지 않는 유일한 값을 구성하기 위해 많이 사용하는데, 보안성이 높고 생성속도가 빠른 UUID Version4를 많이 사용한다하고 한다.
import를 꼭 해줘야함
import { v4 as uuidv4 } from "uuid"
추가로 uuid를 사용하지 않고 고유값을 만드는 방법도 있다.
: uuid를 알기 전에 썼었는데, 이 방법도 괜찮은 것 같다.
// 새로운 id값 생성하는 useState를 사용한다.
const [idNumber, setIdNumber] = useState(0);
// 새로운 todolist 생성 폼의 id 값에 uuidv4() 대신 idNumber를 넣어준다.
id: idNumber,
// 마지막 return에 setIdNumber 사용
// 전의 요소(prev)에 +1 을 해주면 앞의 id값이 삭제되어도 새로 생성되는 id의 숫자는 계속 올라간다.
setIdNumber((prev) => prev + 1);
// TodoList.jsx
import Todo from "./Todo"
import 'todo.css';
/* IsActive, todoList, setTodoList 값 가져오기
setTodoList값은 Todo 컴포넌트에서 사용하기 위해 TodoList 컴포넌트에서는 전달 역할만 해주고 있다.
이것을 prop drilling 이라고 부른다.*/
function TodoList({IsActive, todoList, setTodoList}) {
return (
<div>
// *** check 1 ***
// IsActive가(? 조건) true이면 완료(:의 왼쪽)를, false이면 진행중(:의 오른쪽)을 표시
<h2>{IsActive ? "완료🎉" : "진행중...🐇"}</h2>
// *** check 4 ***
<div className='todo-list'>
/* filter 함수를 이용하여 todoList에 있는 isDone의 값과 IsActive의 값이 일치하면
map으로 만들어진 각각에 해당하는 카드를 위쪽으로 or 아래쪽으로 배치시킨다. */
{todoList.filter(item => item.isDone === IsActive)
// map 함수를 사용하여 새로 만들어진 배열을 화면에 뿌리기
.map(item=>{return(
// Todo 컴포넌트에 item, IsActive, setTodoList 값 내보내기
<Todo item={item} IsActive={IsActive} setTodoList={setTodoList}></Todo>
)})}
</div>
</div>
)
}
export default TodoList
// todo.css
.todo-list {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
/* 아래의 css도 비슷한 결과를 나타내지만, 결정적으로 wrap이 되지 않아 사용하지 않았다.
만약 브라우저 창이 작아질 때 줄바꿈되는 현상이 싫다면 flex-wrap: nowrap;을 사용하거나
아래의 grid 코드를 사용하면 된다. */
/* display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr; <- 1fr의 숫자만큼 카드가 옆으로 붙는다.
place-items: center; */
}
// Todo.jsx
import 'todo.css';
// item, IsActive, setTodoList 값 가져오기
function Todo({item, IsActive, setTodoList}) {
// 삭제 기능 이벤트
const btnDelete = () => {
/* prev로 전에 있던 값들을 가져와 filter를 사용하여 입력값을 't'에 담고
't'의 id값이 TodoList 컴포넌트의 item의 id값과 일치하지 않으면 삭제
-> 삭제를 눌렀을 일치하는 카드들만 반환하는 형식 */
setTodoList(prev => prev.filter((t) => t.id !== item.id));
}
// 완료-취소 기능 이벤트
const btnIsDone = () => {
/* prev로 전에 있던 값들을 가져와 map을 사용하여 입력값을 't'에 담고
't'의 id값이 TodoList 컴포넌트의 item의 id값과 일치하면 isDone을 반대로 바꾸기 */
setTodoList((prev) =>
prev.map((t) => {
if (t.id === item.id) {
// 전에 있던 값을 가져와서 스프레드로 복사해와서 isDone 상태만 반대로 바꿔 카드 생성
return {...t, isDone: !t.isDone}
} else {
// 그렇지 않으면 전에 있던 값을 그대로 반환
return t;
}
})
);
}
return (
<div className="todo">
// input 값을 가져오기 - React에서 JS의 값을 가져올 때는 꼭 {} 중괄호 안에 넣어 가져오기!
<h3>{item.title}</h3>
<p>{item.contents}</p>
<div className='todo-btn-box'>
<button className='todo-btn' onClick={btnDelete}>삭제</button>
// IsActive가 true이면 '취소'버튼을, false이면 '완료'버튼을 출력
<button className='todo-btn' onClick={btnIsDone}>{IsActive ? "취소" : "완료"}</button>
</div>
</div>
)
}
export default Todo
// todo.css
.todo {
width: 250px;
border-radius: 15px;
box-shadow: 4px 6px 16px 0 rgb(75, 127, 173);
padding: 0.2rem 1rem 1rem 1rem;
margin-bottom: 1.5rem;
margin-right: 1.1rem;
}
.todo-btn {
background-color: white;
color: rgb(3, 82, 185);
font-weight: bold;
padding: 0.3rem 2rem 0.3rem 2rem;
margin-left: 0.5rem;
margin-right: 0.5rem;
border-radius: 7px;
border-color: rgb(175, 217, 253);
}
.todo-btn:hover {
cursor: pointer;
background-color: #b0d6fc;
border-color: aliceblue;
}
.todo-btn-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
// index.css
body {
padding: 0 2rem 1rem 2rem;
margin-left: auto;
margin-right: auto;
min-width: 800px;
max-width: 1200px;
background-color: rgb(240, 248, 255);
}
// header.css
.header {
padding: 0.5rem;
color: rgb(0, 110, 255);
}
// input.css
.input-form {
padding: 1rem;
color: rgb(3, 82, 185);
font-weight: bold;
border-color: white;
margin-bottom: 2.5rem;
}
.add-btn {
background-color: white;
color: rgb(3, 82, 185);
font-weight: bold;
padding: 0.3rem 2rem 0.3rem 2rem;
border-radius: 7px;
margin-left: 3rem;
border-color: rgb(175, 217, 253);
}
.add-btn:hover {
cursor: pointer;
background-color: #b0d6fc;
border-color: aliceblue;
}
.input {
background-color: white;
height: 17px;
width: 200px;
padding: 0.4rem;
border: 2px solid rgba(0, 0, 0, 0.23);
border-radius: 7px;
}
.input:hover {
border: 2px solid black;
}
.input:focus-visible {
outline: none!important;
border: 2px solid #1976d2;
}
// todo.css
.todo {
width: 250px;
border-radius: 15px;
box-shadow: 4px 6px 16px 0 rgb(75, 127, 173);
padding: 0.2rem 1rem 1rem 1rem;
margin-bottom: 1.5rem;
margin-right: 1.1rem;
}
.todo-list {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
}
.todo-btn {
background-color: white;
color: rgb(3, 82, 185);
font-weight: bold;
padding: 0.3rem 2rem 0.3rem 2rem;
margin-left: 0.5rem;
margin-right: 0.5rem;
border-radius: 7px;
border-color: rgb(175, 217, 253);
}
.todo-btn:hover {
cursor: pointer;
background-color: #b0d6fc;
border-color: aliceblue;
}
.todo-btn-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import 'index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
// App.js
import { useState } from "react"
import { v4 as uuidv4 } from "uuid"
import Header from "./components/Header"
import Input from "./components/Input"
import TodoList from "./components/TodoList"
function App() {
const [todoList, setTodoList] = useState([])
return (
<div>
<Header>🥰 Habin's Todo List 😉</Header>
<Input setTodoList={setTodoList}></Input>
<TodoList IsActive={false} todoList={todoList} setTodoList={setTodoList}></TodoList>
<TodoList IsActive={true} todoList={todoList} setTodoList={setTodoList}></TodoList>
</div>
)
}
export default App
// Header.jsx
import 'header.css';
function Header({children}) {
return (
<h1 className='header'>{children}</h1>
)
}
export default Header
// Input.jsx
import { useState } from "react"
import { v4 as uuidv4 } from "uuid"
import 'input.css';
function Input({setTodoList}) {
const [title, setTitle] = useState('')
const [contents, setContents] = useState('')
const onSubmit = (event) => {
event.preventDefault();
if (title === '' || contents === '')
return alert('제목과 내용을 모두 입력해주세요');
const newTodo = {
id: uuidv4(),
title,
contents,
isDone: false,
}
setTodoList((prev) => [...prev, newTodo]);
setTitle('');
setContents('');
}
const onChangeTitle =(event) => {
setTitle(event.target.value)
}
const onChangeContents =(event) => {
setContents(event.target.value)
}
return (
<form onSubmit={onSubmit} className="input-form">
제목 <input className="input" type='text' value={title} onChange={onChangeTitle} placeholder='제목을 작성해주세요.' /> 
내용 <input className="input" type='text' value={contents} onChange={onChangeContents} placeholder='내용을 작성해주세요.' /> 
<button className="add-btn">추가하기</button>
</form>
)
}
export default Input
// TodoList.jsx
import Todo from "./Todo"
import 'todo.css';
function TodoList({IsActive, todoList, setTodoList}) {
return (
<div>
<h2>{IsActive ? "완료🎉" : "진행중...🐇"}</h2>
<div className='todo-list'>
{todoList.filter(item => item.isDone === IsActive)
.map(item=>{return(
<Todo item={item} IsActive={IsActive} setTodoList={setTodoList}></Todo>
)})}
</div>
</div>
)
}
export default TodoList
// Todo.jsx
import 'todo.css';
function Todo({item, IsActive, setTodoList}) {
const btnDelete = () => {
setTodoList(prev => prev.filter((t) => t.id !== item.id));
}
const btnIsDone = () => {
setTodoList((prev) =>
prev.map((t) => {
if (t.id === item.id) {
return {...t, isDone: !t.isDone}
} else {
return t;
}
})
);
}
return (
<div className="todo">
<h3>{item.title}</h3>
<p>{item.contents}</p>
<div className='todo-btn-box'>
<button className='todo-btn' onClick={btnDelete}>삭제</button>
<button className='todo-btn' onClick={btnIsDone}>{IsActive ? "취소" : "완료"}</button>
</div>
</div>
)
}
export default Todo
처음에 컴포넌트부터 쭉 나눠놓고 시작했더니 적다가 너무 헷갈려서 코드를 다 갈아엎었다. 아직 자바스크립트도 다 이해하지 못했는데 리액트를 우겨 넣으려니 머릿속이 뒤죽박죽 되고 용량이 초과됐다 ㅠㅠ
아무 것도 없는 상태에서 시작할 때는 기본 모양부터 App.js에 쭉 적어놓고 그 컴포넌트에 맞는 내용으로 조금씩 쪼개고 props하는게 훨씬 편하고 보기도 좋다는 것을 깨달았다.
(물론 익숙해지면 바로 컴포넌트를 나눌 수 있겠지만..😂)
당연하겠지만 강의를 듣기만 할 때와 직접 해보는 것에는 차이가 있었는데, 아무리 예시 코드를 많이 쓰더라도 직접 구현해보는 것만큼 확실한 방법이 없었다. 코드를 다 짜고 나서도 나중에 보면 또 까먹을까봐 코드를 짠 후 내 코드를 내가 리뷰하는 시간을 가졌는데, 기억에 훨씬 잘 남아서 과제 후에 항상 해보면 좋을 것 같다.
더불어 전에는 완성한 코드만 올렸는데, 중간에 막혔던 부분을 추가하면 다음에 또 같은 상황이 오면 생각이 더 잘 떠오를 것 같아서 이번에는 컴포넌트마다 문제점과 해결법을 올려봤다. 써놓고 보니 생각보다 보기가 편한 것 같아서 앞으로도 이렇게 적으면 좋을 것 같다.
코린이로서 다른 블로그 내용을 참고할 때, 코드만 적혀있고 아래쪽에 따로 설명을 해놓으니 어느 부분인지 알아볼 수가 없어서 나는 코드에 주석을 덕지덕지 달아놨는데, 전체적인 코드가 보이지 않아서 답답하긴 하지만(그래서 전체 코드도 함께 올림 ㅎㅎ) 사실 아무 것도 모를 때는 이렇게 적는게 훨씬 이해하기가 쉬운 것 같다.