-
React로 간단한 To-Do-List 만들기만든 화면 찌끄리기 2024. 9. 24. 23:45
오늘은 리액트로 투두리스트를 만들어 보려고 한다.
1. HTML 작성
HTML을 작성해서 전체적인 구조 잡기
기능을 넣기 위해 HTML코드에 추가/삭제/수정 버튼을 넣었고 완료 기능을 구현하기 위해 타이틀 앞에 체크박스를 넣었다.
<div className="todolist"> <h1 className="main-title">To-Do-List 🍖</h1> {/* form 영역 */} <form className="todolist-form"> <span className="inp-box"> <input type="text" /> <button type="submit" className="btn add-btn"> 추가 </button> </span> </form> {/* To-Do-List 영역 */} <ul className="todolist-box"> <li> <span className="chk-box"> <input type="checkbox" id="check01" /> <label htmlFor="check01">타이틀1</label> </span> <div className="btn-box"> <button className="btn btn-edit">수정</button> <button className="btn btn-del">삭제</button> </div> </li> <li> <span className="chk-box"> <input type="checkbox" id="check02" /> <label htmlFor="check02">타이틀2</label> </span> <div className="btn-box"> <button className="btn btn-edit">수정</button> <button className="btn btn-del">삭제</button> </div> </li> </ul> </div>
HTML/CSS 세팅 2. 할 일 추가
import React, { useRef, useState } from "react"; const inputRef = useRef(null); const [todos, setTodos] = useState([]); const handleSubmit = (e) => { e.preventDefault(); const newTodo = { id: Number(new Date()), title: inputRef.current.value.trim(), complete: false, isEditing: false }; if (newTodo.title) { const updatedTodos = [...todos, newTodo]; // 새로운 todos 배열 생성 setTodos(updatedTodos); // 상태 업데이트 inputRef.current.value = ""; // 인풋 초기화 } else { alert("내용을 입력하세요."); } };
코드 수정 - 입력값을 받아오기 위해 useRef를 사용했다.
- 사용자가 할 일을 입력하고 추가 버튼을 누르면, id, title, complete 키 값을 가진 객체가 생성되어 newTodo에 저장된다.
- 입력값이 없을 때를 대비해 if문을 통해 newTodo의 title 값이 존재하는지 확인한다.
- 만약 입력값이 있을 경우, 기존의 todos 배열에 새 항목을 추가하여 updatedTodos에 저장한다.
- 최종적으로, updatedTodos로 상태를 업데이트하여 todos 배열에 새로운 항목이 반영되며, 입력 필드를 초기화하여 사용자가 다음 항목을 쉽게 추가할 수 있도록 한다.
할 일 추가 빈 문자열 입력시 alert창 3. 할 일 삭제
const handleDelete = (id) => { // 인자로 전달된 id와 일치하지 않는 todo 항목(객체)만 필터링하여 새로운 리스트를 생성 const updatedTodos = todos.filter((todo) => todo.id !== id); setTodos(updatedTodos); // 상태를 업데이트하여 삭제된 항목이 반영된 투두리스트로 변경 };
코드 수정 - 주어진 id에 해당하는 투두 항목을 삭제하는 handleDelete 함수이다.
- 입력 매개변수로 삭제할 todo 항목의 고유 식별자인 id를 받는다.
- todos.filter((todo) => todo.id !== id)를 사용하여, 전달된 id와 일치하지 않는 todo 항목만 포함된 새로운 배열 updatedTodos를 생성한다.
- setTodos(updatedTodos)를 호출하여 상태를 업데이트하고, UI가 변경되어 삭제된 항목이 반영된 새로운 투두 리스트가 표시된다.
할 일 삭제 4. 할 일 수정
const editInputRef = useRef(null); const handleToggleEdit = (id) => { const updatedTodos = todos.map((todo) => todo.id === id ? { ...todo, isEditing: !todo.isEditing } : todo ); setTodos(updatedTodos); }; const handleEditSave = (id, newTitle) => { // newTitle이 비어있지 않은 경우에만 수정 진행 if (newTitle) { // todos 배열을 map 메서드로 순회하면서 수정할 항목을 찾음 const updatedTodos = todos.map((todo) => todo.id === id ? { ...todo, title: newTitle, isEditing: !todo.isEditing } : todo ); // 상태를 업데이트하여 변경된 todo 리스트로 반영 setTodos(updatedTodos); } };
- editInputRef는 입력 필드를 참조하기 위해 useRef를 사용한다.
- handleToggleEdit(id) 함수는 특정 할 일의 편집 모드를 전환한다.
- 이 함수는 todos 배열을 순회하며 id와 일치하는 항목의 isEditing 값을 반전시킨다.
- handleEditSave(id, newTitle) 함수는 편집된 할 일을 저장한다.
- 이 함수는 newTitle이 비어있지 않을 때만 실행되며, todos 배열에서 id와 일치하는 항목의 title을 newTitle로 업데이트하고 isEditing 상태를 반전시킨다.
수정 기능 5. 할 일 완료
const handleCheckbox = (id, complete) => { // todos 배열을 map 메서드로 순회하여 id가 일치하는 todo 항목을 찾음 const updatedTodos = todos.map((todo) => todo.id === id ? { ...todo, complete: !complete } : todo ); // 상태를 업데이트하여 완료 상태가 반영된 todo 리스트로 변경 setTodos(updatedTodos); };
코드 수정 - handleCheckbox(id, complete) 함수는 특정 할 일의 완료 상태를 토글하는 기능을 수행한다.
- 이 함수는 todos 배열을 map 메서드로 순회하여 id가 일치하는 todo 항목을 찾는다.
- 일치하는 항목이 있을 경우, 해당 항목의 complete 값을 반전시킨 새로운 객체를 생성한다.
- 최종적으로 setTodos를 호출하여 업데이트된 todo 리스트로 상태를 변경한다.
할 일 완료 6. 로컬 스토리지 저장하기
추가/삭제/수정/완료 함수에서 각각 localstorageSetItem() 함수 호출하기
useEffect(() => { // 로컬 저장소에서 'todolist' 항목 가져오기 const localGetTodo = localStorage.getItem("todolist"); if (localGetTodo === null) { localStorage.setItem("todolist", JSON.stringify([])); // 초기값으로 빈 배열 저장 } else { const parseTodo = JSON.parse(localGetTodo); // 가져온 JSON 문자열을 객체로 변환 setTodos(parseTodo); // 상태 업데이트 } }, []); // 빈 배열을 의존성으로 사용하여 컴포넌트 마운트 시 한 번만 실행 const localstorageSetItem = (updatedTodos) => { // 새로운 배열을 로컬스토리지에 저장 localStorage.setItem("todolist", JSON.stringify(updatedTodos)); };
- useEffect 훅은 컴포넌트가 마운트될 때 로컬 저장소에서 'todolist' 항목을 가져오는 기능을 수행한다.
- localGetTodo에 로컬 저장소에서 해당 항목을 가져온다.
- 만약 localGetTodo가 null이라면, 초기값으로 빈 배열을 로컬 저장소에 저장한다.
- 그렇지 않은 경우, 가져온 JSON 문자열을 객체로 변환하여 parseTodo에 저장하고, 이를 통해 setTodos를 호출하여 상태를 업데이트한다.
- 의존성 배열로 빈 배열을 사용하여 이 효과는 컴포넌트가 마운트될 때 한 번만 실행된다.
- localstorageSetItem(updatedTodos) 함수는 업데이트된 todos 배열을 로컬 저장소에 저장하는 기능을 수행한다.
- 이 함수는 setItem을 사용하여 'todolist' 항목에 새로운 배열을 JSON 문자열로 변환하여 저장한다.
7. 컴포넌트 분할하기
컴포넌트 폴더 및 TodoItem.jsx 파일 생성 // TodoItem.jsx // props의 타입을 지정 import PropTypes from "prop-types"; TodoItem.propTypes = { todo: PropTypes.shape({ id: PropTypes.number.isRequired, // id는 필수로 숫자 title: PropTypes.string.isRequired, // title은 필수로 문자열 complete: PropTypes.bool.isRequired, // complete는 필수로 불리언 isEditing: PropTypes.bool.isRequired // isEditing는 필수로 불리언 }).isRequired, editInputRef: PropTypes.object.isRequired, // editInputRef는 필수로 문자열 handleDelete: PropTypes.func.isRequired, // handleDelete는 필수로 함수 handleToggleEdit: PropTypes.func.isRequired, // handleDelete는 필수로 함수 handleEditSave: PropTypes.func.isRequired, // handleDelete는 필수로 함수 handleCheckbox: PropTypes.func.isRequired // handleEdit 필수로 함수 }; function TodoItem({ todo, editInputRef, handleDelete, handleToggleEdit, handleEditSave, handleCheckbox }) { return ( ...생략 ); } export default TodoItem;
컴포넌트 사용 및 Props 넘기기 8. 완성코드
// App.jsx import React, { useEffect, useRef, useState } from "react"; import "./assets/style/App.scss"; import TodoItem from "./components/TodoItem"; function App() { const inputRef = useRef(null); const editInputRef = useRef(null); const [todos, setTodos] = useState([]); useEffect(() => { // 로컬 저장소에서 'todolist' 항목 가져오기 const localGetTodo = localStorage.getItem("todolist"); if (localGetTodo === null) { localStorage.setItem("todolist", JSON.stringify([])); // 초기값으로 빈 배열 저장 } else { const parseTodo = JSON.parse(localGetTodo); // 가져온 JSON 문자열을 객체로 변환 setTodos(parseTodo); // 상태 업데이트 } }, []); // 빈 배열을 의존성으로 사용하여 컴포넌트 마운트 시 한 번만 실행 const localstorageSetItem = (updatedTodos) => { // 새로운 배열을 로컬스토리지에 저장 localStorage.setItem("todolist", JSON.stringify(updatedTodos)); }; const handleSubmit = (e) => { e.preventDefault(); const newTodo = { id: Number(new Date()), title: inputRef.current.value.trim(), complete: false, isEditing: false }; if (newTodo.title) { const updatedTodos = [...todos, newTodo]; // 새로운 todos 배열 생성 setTodos(updatedTodos); // 상태 업데이트 localstorageSetItem(updatedTodos); // 로컬 저장소에 저장 inputRef.current.value = ""; // 인풋 초기화 } else { alert("내용을 입력하세요."); } }; const handleDelete = (id) => { // 인자로 전달된 id와 일치하지 않는 todo 항목(객체)만 필터링하여 새로운 리스트를 생성 const updatedTodos = todos.filter((todo) => todo.id !== id); setTodos(updatedTodos); // 상태를 업데이트하여 삭제된 항목이 반영된 투두리스트로 변경 localstorageSetItem(updatedTodos); // 로컬 저장소에 저장 }; const handleToggleEdit = (id) => { const updatedTodos = todos.map((todo) => todo.id === id ? { ...todo, isEditing: !todo.isEditing } : todo ); setTodos(updatedTodos); }; const handleEditSave = (id, newTitle) => { // newTitle이 비어있지 않은 경우에만 수정 진행 if (newTitle) { // todos 배열을 map 메서드로 순회하면서 수정할 항목을 찾음 const updatedTodos = todos.map((todo) => todo.id === id ? { ...todo, title: newTitle, isEditing: !todo.isEditing } : todo ); // 상태를 업데이트하여 변경된 todo 리스트로 반영 setTodos(updatedTodos); localstorageSetItem(updatedTodos); // 로컬 저장소에 저장 } }; const handleCheckbox = (id, complete) => { // todos 배열을 map 메서드로 순회하여 id가 일치하는 todo 항목을 찾음 const updatedTodos = todos.map((todo) => todo.id === id ? { ...todo, complete: !complete } : todo ); // 상태를 업데이트하여 완료 상태가 반영된 todo 리스트로 변경 setTodos(updatedTodos); localstorageSetItem(updatedTodos); // 로컬 저장소에 저장 }; return ( <div className="todolist"> <h1 className="main-title">To-Do-List 🍖</h1> {/* form 영역 */} <form className="todolist-form" onSubmit={handleSubmit}> <span className="inp-box"> <input type="text" ref={inputRef} /> <button type="submit" className="btn add-btn"> 추가 </button> </span> </form> {/* To-Do-List 영역 */} <ul className="todolist-box"> {todos?.map((todo) => { return ( <TodoItem key={todo.id} todo={todo} editInputRef={editInputRef} handleDelete={handleDelete} handleToggleEdit={handleToggleEdit} handleEditSave={handleEditSave} handleCheckbox={handleCheckbox} /> ); })} </ul> </div> ); } export default App;
import React from "react"; import PropTypes from "prop-types"; TodoItem.propTypes = { todo: PropTypes.shape({ id: PropTypes.number.isRequired, // id는 필수로 숫자 title: PropTypes.string.isRequired, // title은 필수로 문자열 complete: PropTypes.bool.isRequired, // complete는 필수로 불리언 isEditing: PropTypes.bool.isRequired // isEditing는 필수로 불리언 }).isRequired, editInputRef: PropTypes.object.isRequired, // editInputRef는 필수로 문자열 handleDelete: PropTypes.func.isRequired, // handleDelete는 필수로 함수 handleToggleEdit: PropTypes.func.isRequired, // handleDelete는 필수로 함수 handleEditSave: PropTypes.func.isRequired, // handleDelete는 필수로 함수 handleCheckbox: PropTypes.func.isRequired // handleEdit 필수로 함수 }; function TodoItem({ todo, editInputRef, handleDelete, handleToggleEdit, handleEditSave, handleCheckbox }) { return ( <li> <span className="chk-box"> <input type="checkbox" id={todo.id} checked={todo.complete} onChange={() => { handleCheckbox(todo.id, todo.complete); }} /> <label htmlFor={todo.id} className={todo.complete ? "complete" : undefined} > {todo.title} </label> </span> {todo.isEditing ? ( <span className="inp-box inp-edit"> <input type="text" ref={editInputRef} /> <button className="btn btn-save" onClick={() => { handleEditSave(todo.id, editInputRef.current.value); }} > 저장 </button> </span> ) : null} <div className="btn-box"> {!todo.isEditing && ( <button className="btn btn-edit" onClick={() => { handleToggleEdit(todo.id); setTimeout(() => { if (editInputRef.current) { editInputRef.current.focus(); // 포커스 주기 } }, 0); // 상태 변경 후 즉시 실행 }} > 수정 </button> )} <button className="btn btn-del" onClick={() => { handleDelete(todo.id); }} > 삭제 </button> </div> </li> ); } export default TodoItem;
완성 리액트 강의가 끝난 후, Node, 데이터베이스, AWS, TypeScript까지 이어지는 4주 동안의 학습이 있었다. 그러다 보니 리액트에 대한 기억이 많이 희미해진 것 같았다. 다시 프로젝트를 만들어 보려고 했지만, 기억이 나지 않아서 어려움을 겪었다. 그래서 구글링하고, 이전에 만들었던 작업물도 확인해 보았고, GPT에게 질문도 하면서 다시 시도해 보았다. 그리고 그 결과를 기록해 두고 싶었다.
이렇게 과정을 돌아보니, 배운 내용을 되새기는 것이 얼마나 중요한지 다시 한 번 느꼈다. 앞으로도 계속해서 연습하고, 더 나아가고 싶었다. 그래서 다시 한 번 리액트를 제대로 공부하고 싶다.
'만든 화면 찌끄리기' 카테고리의 다른 글
todolist 또 만들기 (1) 2024.08.02 동물 정보 사이트 만들기 (2) 2024.07.31 회원가입 유효성 검사(feat.todoList) (2) 2024.07.22 롯데월드 클론코딩(react) - 로그인, 회원가입 (2) 2024.07.16 롯데월드 클론코딩(react) (6) 2024.07.15