[React] To-do List 앱 만들기
To-do List 앱 만들기
이전까지 자바스크립트, css, html을 이용해 바닐라 자바스크립트 to-do List를 만들어 보면서 기본적인 프로그래밍 개념을 익혔습니다.
이번에는 앱을 만들기 위해서 지금까지 만들어 둔 코드들을 react를 사용한 코드로 변경해 보겠습니다.
주요 기능
- ’+’ 버튼으로 할 일을 추가한다.
- ’-‘ 버튼으로 할 일을 삭제한다.
- 완료된 할 일을 체크하거나 체크 해제한다.
- 할 일 목록이 페이지 새로고침 후에도 유지된다.
- ‘CLEAR’ 버튼으로 할 일을 전체삭제한다.
- 완료된 할 일은 리스트의 가장 아래로 이동한다.
- 추가된 할 일은 리스트의 가장 위에 추가된다.
구현하기
사전지식
- react 기본 개념
- JavaScript 심화 개념
- CSS와 반응형 디자인
구현 이미지
[기본 화면]

[할일이 입력된 화면]

[새로운 할일 추가됐을 때]

[할일을 완료했을 때]

코드
[components 폴더]
React는 컴포넌트 기반 라이브러리이기 때문에 전체 구조를 컴포넌트 단위로 나누어 개발합니다.
저는 TodoForm.js, TodoItem.js, TodoList.js 이렇게 3개의 컴포넌트로 나누어 코드를 짰습니다.
[TodoForm.js]
import React, { useState } from 'react'; // React 라이브러리에서 기능 가져오기
function TodoForm({ addTodo }) { // addTodo 함수는 props로 전달 -> TodoForm에서 입력한 값을 App 컴포넌트로 전달
const [input, setInput] = useState(''); // 할일 입력 상태 관리
const handleSubmit = (e) => {
e.preventDefault(); // form 제출 기본 동작 막기
if (input.trim() === '') return; // 빈 입력 방지
addTodo(input); // App.js에서 받은 addTodo 함수를 호출해 할일 추가
setInput(''); // 입력 필드 초기화
};
return (
<form onSubmit={handleSubmit}> {/* form 태그 사용: Enter 키로 제출 가능 */}
<input
type="text" /* 텍스트 입력 필드 */
value={input} /* 입력 값을 상태(input)와 연결 (Controlled Component) */
onChange={(e) => setInput(e.target.value)} /* 입력할 때마다 상태 업데이트 */
placeholder="Add a new task" /* 입력창에 표시될 힌트 텍스트 */
/>
<button type="submit">+ {/* 제출 버튼: 클릭 시 handleSubmit 실행 */}</button>
</form>
);
}
export default TodoForm; // 컴포넌트 내보내기
[TodoItem.js]
import React from 'react';
function TodoItem({ todo, deleteTodo, toggleComplete }) {
return (
<li>
<button className={`checkButton ${todo.completed ? 'checked' : ''}`} onClick={toggleComplete}>
✔
</button>
<span className={todo.completed ? 'completed' : ''}>{todo.text}</span>
<button className="deleteButton" onClick={deleteTodo}>
x
</button>
</li>
);
}
export default TodoItem;
[TodoList.js]
import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, deleteTodo, toggleComplete }) {
return (
<ul>
{todos.map((todo, index) => (
<TodoItem
key={index}
todo={todo}
deleteTodo={() => deleteTodo(index)}
toggleComplete={() => toggleComplete(index)}
/>
))}
</ul>
);
}
export default TodoList;
[App.js]
이전에 구현한 to-do List의 HTML과 javaScript의 기능을 React로 구현한 코드입니다.
import React, { useState, useEffect } from 'react'; // React 라이브러리에서 기능 가져오기
import './App.css'; // CSS 파일 가져오기
function App() {
const [todos, setTodos] = useState([]); // 할 일 목록 상태 관리, todos를 업데이트 하려면 setTodos를 업데이트해야함
// 컴포넌트 렌더링 시 LocalStorage에서 데이터를 불러옴
useEffect(() => {
const savedTodos = JSON.parse(localStorage.getItem('todos')) || []; // LocalStorage에서 'todos' 키에 저장된 데이터 가져오기
setTodos(savedTodos); // 가져온 데이터를 상태에 저장
}, []);
// 할 일 목록을 LocalStorage에 저장하는 함수
const saveToLocalStorage = (newTodos) => {
localStorage.setItem('todos', JSON.stringify(newTodos)); // 배열 데이터를 JSON 문자열로 변환해 저장
};
// 새로운 할 일을 추가하는 함수
const addTodo = (todoText) => {
if (!todoText.trim()) return; // 빈 문자열이 입력된 경우 함수 종료
const newTodo = { text: todoText.trim(), completed: false }; // 새로운 할 일 객체 생성
const updatedTodos = [newTodo, ...todos]; // 기존 목록에 새로운 할 일을 추가
setTodos(updatedTodos); // 상태 업데이트
saveToLocalStorage(updatedTodos); // LocalStorage에 저장
};
// 특정 할 일을 삭제하는 함수
const deleteTodo = (index) => {
const updatedTodos = todos.filter((_, i) => i !== index); // 삭제할 할 일을 제외한 새로운 배열 생성
// filter : 배열을 순회하며 조건을 만족하는 요소만 반환하는 함수
setTodos(updatedTodos); // 상태 업데이트
saveToLocalStorage(updatedTodos); // LocalStorage에 저장
};
// 특정 할 일의 완료 상태를 반전(토글)하는 함수
const toggleComplete = (index) => {
const updatedTodos = [...todos]; // 기존 배열 복사
updatedTodos[index].completed = !updatedTodos[index].completed; // 완료 상태 반전
// 완료된 항목은 아래로, 미완료된 항목은 위로 정렬
updatedTodos.sort((a, b) => a.completed - b.completed);
setTodos(updatedTodos); // 상태 업데이트
saveToLocalStorage(updatedTodos); // LocalStorage에 저장
};
// 모든 할 일을 삭제하는 함수
const clearAllTodos = () => {
setTodos([]); // 상태를 빈 배열로 초기화
localStorage.removeItem('todos'); // LocalStorage에서 'todos' 키 제거
};
return (
<div className="container"> {/* 전체 UI를 감싸는 컨테이너 */}
<h2>To-Do List</h2>
<form
onSubmit={(e) => {
e.preventDefault(); // form 제출 기본 동작 막기
const input = e.target.elements.todoInput; // form 내 입력 필드 참조
addTodo(input.value); // 입력된 값으로 새로운 할 일 추가
input.value = ''; // 입력 필드 초기화 (DOM 직접 조작)
}}
>
<input type="text" name="todoInput" placeholder="Today's work" /> {/* 할 일 입력 필드 */}
<button type="submit">+</button> {/* 할 일 추가 버튼 */}
<button className="clearAllButton" onClick={clearAllTodos}>
CLEAR {/* 모든 할 일 삭제 (localStorage 데이터 포함)*/}
</button>
</form>
<ul>
{todos.map((todo, index) => (
{/* todos 배열을 순회하며 각 요소(todo)를 <li>로 생성, index를 key로 사용하여 식별 */}
<li key={index}> {/* 각 할 일을 표시하는 리스트 항목 */}
<button
className={`checkButton ${todo.completed ? 'checked' : ''}`} // 완료 상태에 따라 클래스 설정
onClick={() => toggleComplete(index)} // 완료 상태 변경 및 완료된 항목을 아래로 정렬
></button>
<span className={todo.completed ? 'completed' : ''}>
{todo.text} {/* 할 일 텍스트 */}
</span>
<button className="deleteButton" onClick={() => deleteTodo(index)}>
x {/* 삭제 버튼 */}
</button>
</li>
))}
</ul>
</div>
);
}
export default App; // 다른 파일에서 사용할 수 있도록 컴포넌트 내보내기
[index.css]
이전에 구현한 to-do List의 CSS와 거의 동일하게 구현했습니다.
body {
margin: 0;
padding: 0;
font-family: Arial, 'Courier New';
display: flex;
justify-content: center;
align-items: center;
background-color:rgb(236, 236, 236);
height: 80vh;
}
.container {
background-color: white;
margin: 50px;
padding: 30px;
border-radius: 30px;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
text-align: center;
width: 600px;
}
h2 {
color:white;
background-color: rgb(70, 198, 207);
margin-bottom: 20px;
margin-top: 20px;
margin-left: 100px;
margin-right: 100px;
padding: 10px;
border-radius: 30px;
}
input {
flex: 1;
padding: 10px;
border: 1px solid rgb(219, 219, 219);
border-radius: 5px;
margin: 2px;
}
button {
margin : 2px;
padding: 10px;
background-color: #5cb85c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #4cae4c;
}
li .deleteButton{
background-color:salmon;
color: white;
border: none;
padding-top: 3px;
padding-bottom: 3px;
border-radius: 5px;
cursor: pointer;
}
li .deleteButton:hover {
background-color: rgb(255, 113, 98);
}
ul {
list-style: none;
padding: 0;
margin: 20px 0 0;
}
li {
font-family: Arial;
display: flex;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid rgb(199, 199, 199);
}
.checkButton{
width: 20px;
height: 20px;
border: 2px solid rgb(70, 198, 207);
border-radius: 5px;
background-color: transparent;
cursor: pointer;
margin-right: 10px;
}
.checkButton:hover {
background-color:rgb(70, 198, 207);
position: relative;
}
.checkButton.checked {
background-color:rgb(70, 198, 207);
position: relative;
}
.checkButton.checked::after {
content: '✔';
color: white;
font-size: 14px;
position: absolute;
left: 3px;
bottom: 1px;
}
.completed {
text-decoration: line-through;
color: #6d6d6d;
}
.clearAllButton {
padding: 10px;
background-color: salmon;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.clearAllButton:hover {
background-color: rgb(255, 113, 98);
}
댓글남기기