본문 바로가기

TIL

TIL - 간단한 Todo list 만들어서 CRUD기능 구현


간단한 Todo list 만들기


어제 스파르트코딩클럽 1주차 팀 프로젝트에서 방명록 만들기에서 CRUD를 구현 해봤고 

방명록을 만들었던 기억을 통해 간단하게 Todo list를 간단한 CRUD를 만들 수 있을 것 같아서 복습겸 만들어 보기로 했다.

    <div class="todo-container">
        <h1>오늘 할일📚</h1>
        <form id="todo-form" method="post">
            <input type="text" id="todo-input" placeholder="할 일을 입력하세요" required>
            <button type="submit">추가</button>
            </label>
        </form>
        <ul id="todo-list">
            <!-- Todo리스트 추가 -->
        </ul>
        <button class="delete-completed">완료된 항목 삭제</button>
    </div>

html 구조는 방명록과 비슷한 구조로 만들었고 firestore를 이용해서 만들었다.

 

        // todo 폼 제출 이벤트 핸들러
        document.getElementById("todo-form").addEventListener("submit", async (event) => {
            event.preventDefault(); // 폼 제출 방지

            const todoInput = document.getElementById("todo-input");
            const todo = todoInput.value;
            try {
                // firestore에 데이터 추가
                await addDoc(collection(db, "tododata"), {
                    todo: todo,
                });
                todoInput.value = "";
                // window.location.reload(); (1)
                displayTodolist(doc.id, { todo });
            } catch (error) {
                console.log("데이터에 추가하는 동안 에러가 발생 : ", error);
                window.alert("데이터에 추가하는 동안 에러가 발생했습니다.");
            }


        });

 

처음에는 방명록과 같이 window.location.reload()를 해줬는데 input창에 값을 입력 할 때 마다 페이지가 새로고침 하는

것은 불편할 것 같아서 대신 todolist를 화면에 표시하는 function displayTodolist(docId, entry)로 대체 했다.

 

      // firestore에서 데이터 가져오기 
        async function todolistEntries() {
            const querySnapshot = await getDocs(
                query(collection(db, "tododata"))
            );
            for (const doc of querySnapshot.docs) {
                const entry = doc.data();
                displayTodolist(doc.id, entry);
            }
        }

        // todolist 화면에 표시
        function displayTodolist(docId, entry) {
            const todolistEntryList = document.getElementById("todo-list");
            const entryLi = document.createElement("li");
            entryLi.classList.add("todo-entry");

            // 고유한 ID생성
            const uniqueId = 'todo-' + Math.random().toString(36).substr(2, 9);
            entryLi.innerHTML = `
            <label class="todo-label" data-doc-id="${docId}">
                <input id="${uniqueId}" type="checkbox" class="todo-checkbox" name="todos" data-doc-id="${docId}">
                <p class="todo">${entry.todo}</p> 
                <div class='btn-class'>               
                    <button class="edit-btn" data-doc-id="${docId}">✏️</button>
                    <button class="delete-btn" data-doc-id="${docId}">🗑️</button>
                </div>
            </label>   
            `;
            todolistEntryList.prepend(entryLi);
        }

 

방명록의 코드에서 개선했던 점은 방명록 처럼 todo의 목록 또한 최신순으로 등록하는 것을 원했고

timestamp: Number(new Date())와 orderBy를 이용해서 내림차순을 하려고 했는데 firestore에는 순서가 상관 없고 

화면에 표시된 todo 항목만 내림차순으로 나오면 되니까 prepend()를 이용해서 새로 추가된 할 일을 기존에 등록된 entryLi 앞에 추가해서 코드가 더 간단해졌다

 

        // 휴지통 버튼 클릭 시 이벤트 처리        
        document.addEventListener("click", async (event) => {
            if (event.target.classList.contains("delete-btn")) {
                const docId = event.target.getAttribute("data-doc-id");

                try {
                    // Firestore에서 문서 삭제
                    await deleteDoc(doc(db, "tododata", docId));
                    // 삭제한 항목 제거
                    event.target.closest(".todo-entry").remove();
                    console.log("Document deleted successfully."); //삭제 성공 메시지 출력
                } catch (error) {
                    console.error("Error deleting document: ", error);
                    window.alert("방명록 항목을 삭제하는 동안 오류가 발생했습니다.");
                }
            }
        });

        // 수정 버튼 클릭 이벤트 처리
        document.addEventListener("click", async (event) => {
            if (event.target.classList.contains("edit-btn")) {
                const docId = event.target.getAttribute("data-doc-id");
                const parentLabel = event.target.closest(".todo-label");
                const todoParagraph = parentLabel.querySelector(".todo");

                // 이미 input 요소가 생성되어 있는지 확인
                if (!parentLabel.querySelector("input[type='text']")) {
                    // 새로운 input 요소 생성
                    const newTodoInput = document.createElement("input");
                    newTodoInput.type = "text";
                    newTodoInput.placeholder = "수정할 메시지를 입력하세요";
                    todoParagraph.style.display = "none";

                    // 기존 todo 항목 바로 뒤에 추가
                    parentLabel.insertBefore(newTodoInput, parentLabel.childNodes[2]);

                    // input 요소에서 엔터키 입력 시 발생하는 이벤트 처리
                    newTodoInput.addEventListener("keydown", async (e) => {
                        if (e.key === "Enter") {
                            const newTodo = newTodoInput.value;
                            try {
                                // Firestore에서 문서 업데이트
                                await updateDoc(doc(db, "tododata", docId), {
                                    todo: newTodo
                                });
                                // 업데이트된 메시지를 화면에 반영
                                todoParagraph.textContent = newTodo;
                                newTodoInput.remove(); // 수정 영역 제거
                                todoParagraph.style.display = "";
                                window.alert("할 일이 업데이트되었습니다.");
                            } catch (error) {
                                console.error("Error updating document: ", error);
                                window.alert("할 일을 업데이트하는 동안 오류가 발생했습니다.");
                            }
                        }
                    });
                }
            }
        });

휴지통은 이모지로 방명록의 삭제버튼과 비슷하게 코드를 쳤고 수정버튼은 방명록의 수정버튼과 다른 점은

textarea에서 input창으로 변경된 만큼 enter 키를 입력해서 업데이트를 하고 싶어서 keydown으로 이벤트를 처리했다 

그리고 꽤 치명적인 오류를 발견했는데 수정버튼을 여러 번 클릭하면 여러 번 input창이 띄워지고 이 문제는 방명록에 textarea에서도 똑같이 발견 됐고 if 조건문을 통해서 input창의 여부를 확인해서 해결 할 수 있었다.

 

 

        // 완료된 항목 삭제 버튼 클릭 시 이벤트 처리
        document.addEventListener("click", async (event) => {
            if (event.target.classList.contains("delete-completed")) {
                // 모든 체크된 항목들을 선택
                const checkboxes = document.querySelectorAll('.todo-checkbox:checked');

                // 각 체크된 항목에 대해 삭제
                for (const checkbox of checkboxes) {
                    const docId = checkbox.getAttribute("data-doc-id"); // Firestore 문서 ID 가져오기            

                    try {
                        // Firestore에서 해당 문서를 삭제
                        await deleteDoc(doc(db, "tododata", docId));
                        // 해당 항목을 화면에서 제거
                        checkbox.closest('.todo-entry').remove();
                        console.log("Completed document deleted successfully.");
                    } catch (error) {
                        console.error("Error deleting completed document: ", error);
                        window.alert("완료된 항목을 삭제하는 동안 오류가 발생했습니다.");
                    }
                }
            }
        });

또한 새로운 기능으로 todo list에서 todo 옆에 input type=checkbox이 클릭된 부분들을 한번에 삭제 하는 기능을 구현했다.

 

 

이렇게 만든 결과물은 아래와 같다

잘못된 결과물

 

보면 알겠지만 새로고침을 해야 firestore에 데이터가 전송이 되는 문제가 있었다.

Firestore에 데이터가 바로 전송되지 않는 문제 해결하기

        // 문제 있는 todo 폼 제출 이벤트 핸들러
        document.getElementById("todo-form").addEventListener("submit", async (event) => {
            event.preventDefault(); // 폼 제출 방지

            const todoInput = document.getElementById("todo-input");
            const todo = todoInput.value;
            try {
                // firestore에 데이터 추가
                await addDoc(collection(db, "tododata"), { // (1)
                    todo: todo,
                });
                todoInput.value = "";
                // window.location.reload();
                displayTodolist(doc.id, { todo });
            } catch (error) {
                console.log("데이터에 추가하는 동안 에러가 발생 : ", error);
                window.alert("데이터에 추가하는 동안 에러가 발생했습니다.");
            }


        });

 

삭제버튼 수정버튼은 문제 없이 작동하는 것을 보고 폼 제출하는 곳에 문제가 있다고 생각했다.

사용자가 ToDo를 추가할 때마다 페이지를 새로고침해야만 데이터가 Firestore에 전송되는 문제는 개발자 도구에도 오류로 안 잡혀서 원인이 무엇인가 코드를 계속 보고나니까 (1) 부분에 await addDoc 함수가 끝나기 전displayTodolist() 함수가 실행이 되서  firestore에 데이터가 전달이 되기 전에 웹페이지 화면에 표시가 되었던 것이다. 즉 비동기 함수가 비동기적으로 수행되서 함수가 실행되서 완료 될 때 까지 기다리지 않고 다음 코드가 실행되었던 것

 

  // todo 폼 제출 이벤트 핸들러
        document.getElementById("todo-form").addEventListener("submit", async (event) => {
            event.preventDefault(); // 폼 제출 방지

            const todoInput = document.getElementById("todo-input");
            const todo = todoInput.value;
            try {
                // firestore에 데이터 추가
                const docRef = await addDoc(collection(db, "tododata"), { // (1)
                    todo: todo,
                });
                todoInput.value = "";
                // window.location.reload();
                displayTodolist(docRef.id, { todo });   // (2)
            } catch (error) {
                console.log("데이터에 추가하는 동안 에러가 발생 : ", error);
                window.alert("데이터에 추가하는 동안 에러가 발생했습니다.");
            }


        });

 

(1) 부분에 함수표현식으로 바꿔서 (2)에 있는 displayTodolist()에 있는 함수를

addDoc 함수가 완료된 후 displayTodolist 함수를 호출하는 클로저를 생성하여 (1)부터 (2)까지의 코드가 동기적으로 실행되도록 해서 오류가 해결했다

 

 

최종결과물

 

 

CRUD

Create (생성)

  • 폼을 통해 새로운 할 일을 입력해서 리스트에 추가 가능

Read (읽기)

  • 페이지가 로드될 때 먼저 Firestore에서 저장된 모든 할 일 목록을 가져와서 화면에 표시

Update (수정)

  • Todo List에 할 일을 수정하고자 할 때 버튼을 눌러 수정 가능
  • 수정 버튼을 클릭하면 해당 항목을 수정할 수 있는 input 요소가 생성
  • 수정을 완료하면  Firestore에서 해당 문서를 업데이트하고, 화면에 반영

Delete (삭제)

  • 리스트의 항목을 삭제하고자 할 때 완료된 항목 삭제 버튼을 눌러 삭제 가능
  • 완료된 항목 삭제 버튼을 클릭하면 체크박스에 체크된 항목들이 삭제
  • 화면에 삭제된 항목들은 Firestore에서도 해당 문서를 삭제