5 분 소요

To-do List 앱 만들기

이전까지 자바스크립트, css, html을 이용해 바닐라 자바스크립트 to-do List를 만들어 보면서 기본적인 프로그래밍 개념을 익혔습니다.

이번에는 앱을 만들기 위해서 지금까지 만들어 둔 코드들을 react를 사용한 코드로 변경해 보겠습니다.


주요 기능

  1. ’+’ 버튼으로 할 일을 추가한다.
  2. ’-‘ 버튼으로 할 일을 삭제한다.
  3. 완료된 할 일을 체크하거나 체크 해제한다.
  4. 할 일 목록이 페이지 새로고침 후에도 유지된다.
  5. ‘CLEAR’ 버튼으로 할 일을 전체삭제한다.
  6. 완료된 할 일은 리스트의 가장 아래로 이동한다.
  7. 추가된 할 일은 리스트의 가장 위에 추가된다.

구현하기

사전지식


구현 이미지

[기본 화면] todoList11


[할일이 입력된 화면] todoList12


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


[할일을 완료했을 때] todoList14


코드

[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);
}


댓글남기기