ABOUT ME

아무거나 찌끄리기

Today
Yesterday
Total
  • 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문을 통해 newTodotitle 값이 존재하는지 확인한다.
    • 만약 입력값이 있을 경우, 기존의 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와 일치하는 항목의 titlenewTitle로 업데이트하고 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에게 질문도 하면서 다시 시도해 보았다. 그리고 그 결과를 기록해 두고 싶었다.

    이렇게 과정을 돌아보니, 배운 내용을 되새기는 것이 얼마나 중요한지 다시 한 번 느꼈다. 앞으로도 계속해서 연습하고, 더 나아가고 싶었다. 그래서 다시 한 번 리액트를 제대로 공부하고 싶다.

     

Designed by Tistory.