간단한 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에서도 해당 문서를 삭제
'TIL' 카테고리의 다른 글
TIL - 블로그 만들기 모달창 구현 및 블로그 글 목록 선입선출로 만들기 (2) (0) | 2024.04.21 |
---|---|
TIL - JS와 Firestore 이용해서 블로그 만들기 - 탭 전환 (1) (0) | 2024.04.19 |
TIL - Git의 필요성과 prepend()를 사용하면서 생긴 문제 발견과 해결 (1) | 2024.04.18 |
TIL - 방명록 만들기로 CRUD 구현 (0) | 2024.04.16 |
TIL - firebase와 JS를 이용해서 방명록 구현 (0) | 2024.04.15 |