Javascript로 Todo App 만들기

김진영·2024년 5월 9일
0

구름톤 트레이닝

목록 보기
7/8
post-thumbnail

구름톤 트레이닝 과제를 기록으로 남겨보려 한다

기록을 남기는 이유는 이번 과제는 포기하지 않고 끝까지 만들어 내고 싶기도 하고 공부? 했다는 기록을 남기기 위해서!!

기본적인 구조

Todo App 기능 (2024/05/09)

  • 사용자가 작성한 Todo 저장기능
  • 사용자가 저장한 Todo 수정기능
  • 사용자가 저장한 Todo 체크기능
    • 체크박스를 이용
    • 체크가 true면 수정X 체크표시 넣기
    • 체크가 false면 수정, 삭제 가능
  • 새로고침 해도 작성한 Todo 유지
    • loacalStorage를 이용

새로 추가된 기능 (2024/05/10)

  • Todo를 작성하고 엔터치면 list 생성

우선 대략 생각한 기능은 여기까지이다 개발 하면서 더 필요하면 추가해 넣도록!

기본틀, HTML, CSS 코드


디자인은 재능이 없어서 이정도면 만족해야겠다.. 나름 엄청 신경썻음ㅎㅎ

2024/05/10 HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Todo App</title>
    <!-- CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css" integrity="sha512-NmLkDIU1C/C88wi324HBc+S2kLhi08PN5GDeUVVVC/BVt/9Izdsc9SVeVfA1UZbY3sHUlDSyRXhCzHfr6hmPPw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <link rel="stylesheet" href="common.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <!-- JS -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
</head>
<body>
    <div class="todo__app">
    <header id="header">
        <h1>Todo List</h1>
    </header>
    <main id="mian">
        <div class="write__box">
            <input type="text" placeholder="할 일 목록을 작성해 주세요." id="user__wirte" value="">
            <button id="create__btn"><i class="fa-solid fa-pen-nib"></i></button>
        </div>
        <div class="todo__box">
            <h3 class="todo__title">할 일 목록</h3>
        </div>
        <button id="reset"> RESET</button>
    </main>
    </div>
    <script src="main.js"></script>
</body>
</html>

2024/05/10 CSS

* {
    box-sizing: border-box;
}

body {
    color: #222;
}
.todo__app {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 520px;
    margin: 0 auto;
    box-shadow: 1px 3px 10px rgba(0,0,0,0.1);
    padding: 30px 0 40px;
    margin-top: 50px;
    border-radius: 15px;
    background-color: blanchedalmond;
}
#header h1 {
    font-size: 30px;
    font-weight: 600;
    text-align: center;
    margin: 20px 0;
}
.write__box {
    display: flex;
    height: 30px;
    justify-content: space-around;
}
#user__wirte {
    width: 410px;
    height: 30px;
    margin: 0 auto;
    border: none;
    outline: none;
    padding: 0 10px;
    border-radius: 10px;
    text-align: center;
    box-shadow: 1px 1px 3px rgba(0,0,0,0.1);
}
#create__btn {
    padding: 0;
    margin-left: 10px;
    width: 30px;
    border: none;
    background-color: white;
    cursor: pointer;
    border-radius: 5px;
    color: #222;
    transition: all 0.2s ease-in-out;
    box-shadow: 1px 1px 3px rgba(0,0,0,0.1);
}

.todo__box {
    margin-top: 15px;
    background-color: white;
    padding: 20px 0;
    border-radius: 15px;
    display: flex;
    flex-direction: column;
    position: relative;
    box-shadow: 1px 1px 3px rgba(0,0,0,0.1);
}
.todo__title {
    text-align: center;
    position: absolute;
    top: 13px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 16px;
    font-weight: 600;
}
.todo__list {
    display: flex;
    padding: 0 5px;
    border: 1px solid #ddd;
    border-radius: 5px;
    margin: 0 15px;
    align-items: center;
}
.todo__list:not(:last-child) {
    margin-bottom: 20px;
}
.todo__list:first-child {
    margin-top: 30px;
}
#user__todo {
    height: 30px;
    width: 300px;
    margin: 0 15px;
    text-align: center;
    background-color: transparent;
    border: none;
    position: relative;
}
#edit__btn,
#remove__btn {
    width: 30px;
    height: 30px;
    background-color: white;
    color: #222;
    border: none;
    border-radius: 5px;
    transition: all 0.2s ease-in-out;
    cursor: pointer;
    text-align: center;
    font-size: 16px;
    padding-top: 7px;
}
#edit__btn {
    margin-right: 5px;
}
#reset:hover,
#edit__btn:hover,
#remove__btn:hover,
#create__btn:hover {
    background-color: #222;
    color: white;
}
/* checkbox 체크박스  */
#ckbox {
    cursor: pointer;
}
.complete {
    opacity: 0.5;
    position: relative;
}

.complete #user__todo::before {
    content: "";
    position: absolute;
    background-color: #222;
    height: 1px;
    top: 50;
    width: 100%;
}

main {
    display: flex;
    flex-direction: column;
}
#reset {
    margin-top:  15px;
    height: 30px;
    border: none;
    background-color: white;
    border-radius: 10px;
    cursor: pointer;
    transition: all 0.2s ease-in-out;
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 3px;
    box-shadow: 1px 1px 3px rgba(0,0,0,0.1);
}

HTML, CSS는 요정도로 하고 JS로 넘어가보자!! 중간중간 개발 하면서 계속 업로드 할 예정!

2024/05/10 Javascript

const createBtn = $("#create__btn");
const userWriteInput = $("#user__wirte");
const todoBox = $(".todo__box");
const userTodoInput = $("#user__todo");

const newTodo = function () {
  // 랜덤 키 생성
  const randomNum = Math.random().toString();
  // 사용자가 입력한 값
  const todoValue = userWriteInput.val();
  const todoData = {
    id: randomNum,
    value: todoValue,
    checked: false,
  };
  if (todoValue == "") {
    alert("할 일 목록을 작성해 주세요.");
  } else {
    // 로컬 스토리지에서 데이터 가져오기
    const localTodoData = JSON.parse(localStorage.getItem("todos")) || [];
    // 로컬 스토리지에 새로운 데이터 추가
    localTodoData.unshift(todoData);
    // 로컬 스토리지에 저장
    localStorage.setItem("todos", JSON.stringify(localTodoData));
    const todoListAdd = `
            <div class="todo__list" id="${todoData.id}"> 
                <input type="checkbox" id="ckbox">
                <input type="text" disabled id="user__todo" value="${todoData.value}"> 
                <i class="fa-regular fa-pen-to-square" id="edit__btn"></i>
                <i class="fa-solid fa-eraser" id="remove__btn"></i>
            </div>`;
    todoBox.prepend(todoListAdd);
    userWriteInput.val("");
  }
};
// 새로운 todo 생성
// createBtn을 눌렀을 때 새로운 todo 생성
createBtn.click(function () {
  newTodo();
});
// 사용자가 input에 todo를 입력하고 엔터키를 치면 생성
userWriteInput.keypress(function (e) {
  if (e.keyCode === 13) {
    newTodo();
  }
});

// 로컬 스토리지에 저장한 데이터 불러오기
$(document).ready(function () {
  const localTodoData = JSON.parse(localStorage.getItem("todos")) || [];
  // 순서대로 저장된 데이터를 역순으로 반복해서 화면에 추가
  localTodoData.reverse().forEach(function (todoData) {
    const todoListAdd = `
    <div class="todo__list ${todoData.checked ? "complete" : ""}" id="${
      todoData.id
    }"> 
                <input type="checkbox" id="ckbox" ${
                  todoData.checked ? "checked" : ""
                }>
                <input type="text" disabled id="user__todo" value="${
                  todoData.value
                }"> 
                <i class="fa-regular fa-pen-to-square" id="edit__btn"></i>
                <i class="fa-solid fa-eraser" id="remove__btn"></i>
            </div>`;
    todoBox.prepend(todoListAdd);
  });
});

// remove 버튼을 누르면 localstorage에서 해당 데이터 삭제
todoBox.on("click", "#remove__btn", function () {
  const todoList = $(this).closest(".todo__list");
  const key = todoList.attr("id");

  // 로컬 스토리지에서 해당 id를 가진 데이터 삭제
  let localTodoData = JSON.parse(localStorage.getItem("todos")) || [];
  localTodoData = localTodoData.filter((item) => item.id !== key);
  localStorage.setItem("todos", JSON.stringify(localTodoData));
  // 화면에서 해당 요소 삭제
  todoList.remove();
});

// 체크박스 체크on off 기능
todoBox.on("change", "#ckbox", function () {
  if ($(this).parent().hasClass("complete")) {
    $(this).parent().removeClass("complete");
  } else {
    $(this).parent().addClass("complete");
  }
  const todoId = $(this).closest(".todo__list").attr("id");
  const isChecked = $(this).prop("checked");

  let localTodoData = JSON.parse(localStorage.getItem("todos")) || [];
  const todoIndex = localTodoData.findIndex((todo) => todo.id === todoId);
  localTodoData[todoIndex].checked = isChecked;
  localStorage.setItem("todos", JSON.stringify(localTodoData));
});

// editBtn 수정 기능
let editCount = 0;
todoBox.on("click", "#edit__btn", function () {
  // 클릭된 에딧 버튼의 부모 요소인 todo__list의 id 값을 가져옵니다.
  const todoId = $(this).closest(".todo__list").attr("id");

  // 해당 todo의 체크박스가 체크된 상태인지 확인합니다.
  const isChecked = $(this)
    .closest(".todo__list")
    .find("#ckbox")
    .prop("checked");

  // 체크박스가 체크되어 있으면 수정할 수 없도록 합니다.
  if (isChecked) {
    return; // 수정할 수 없는 상태이므로 함수를 종료합니다.
  }

  // 에딧 카운트를 증가시킵니다.
  editCount++;

  // 에딧 카운트가 홀수일 때(수정 모드일 때)
  if (editCount % 2 === 1) {
    // 에딧 버튼 아이콘을 체크 모양으로 변경합니다.
    $(this).removeClass().addClass("fa-solid fa-check");

    // 해당 todo의 텍스트 input을 활성화하고 포커스를 줍니다.
    $(this)
      .closest(".todo__list")
      .find("input[type='text']")
      .removeAttr("disabled")
      .focus();
  } else {
    // 에딧 카운트가 짝수일 때(수정 완료 모드일 때)
    // 에딧 버튼 아이콘을 다시 펜으로 변경합니다.
    $(this).removeClass().addClass("fa-regular fa-pen-to-square");

    // 수정된 텍스트를 가져옵니다.
    const editedValue = $(this)
      .closest(".todo__list")
      .find("input[type='text']")
      .val();

    // 로컬 스토리지에서 해당 할 일 데이터를 가져옵니다.
    let localTodoData = JSON.parse(localStorage.getItem("todos")) || [];

    // 해당 ID와 일치하는 할 일 데이터를 찾습니다.
    const matchedTodo = localTodoData.find((todo) => todo.id === todoId);

    // 수정된 값을 할 일 데이터에 반영합니다.
    matchedTodo.value = editedValue;

    // 수정된 할 일 데이터를 다시 로컬 스토리지에 저장합니다.
    localStorage.setItem("todos", JSON.stringify(localTodoData));

    // 수정이 완료되면 텍스트 input을 비활성화합니다.
    $(this)
      .closest(".todo__list")
      .find("input[type='text']")
      .attr("disabled", true);
  }
});

// reset 버튼 클릭 시 모든 로컬 스토리지 삭제
$("#reset").click(function () {
  localStorage.removeItem("todos");
  location.reload();
});

작성일지

  • 첫 작성 - 2024/05/09 14:48
  • 1차 완성, 배포 - 2024/05/10 15:35

개발 ISSUE!

Javascript ISSUE 2024/05/09 16:35

자바스크립트를 작성 하는 중 Local Storage를 이용 하면서 삽질을 했다...
Local Storage에 랜덤으로 부여한 id 값과, 생성된 HTML을 저장 하는데 까지는 그래도 금방 했으나
remove버튼을 눌렀을때 Local Storage에 저장된 id값, HTML을 삭제하는 기능을 개발하는 중 어찌저찌 코드를 짯으나 console.log에 출력해도 아무런 반응이없고 Local Storage에서도 삭제가 안된다..

// remove 버튼을 누르면 localstoryge에서 데이터 삭제
$("#remove__btn").on("click", function () {
  // 부모 요소 찾기
  const parentTodoList = $(this).closest(".todo__list");
  // 부모 요소에서 data-id 속성 가져오기
  const checkBoxId = parentTodoList.data("id");
  // LocalStorage에서 해당 항목 삭제
  localStorage.removeItem(checkBoxId);
  // DOM에서 해당 요소 삭제
  parentTodoList.remove();
  console.log();
});

이렇게 짯으나 생각처럼 굴러가질 않았다.. 1시간가량 삽질 하다가 Chat GPT에게 물어봤다(너무 빨리 물어본거같긴 하지만 ㅜㅜ 과제라..) 내 코드의 문제점은 각 todo 항목에 대해 동적으로 생성된 remove__btn과 관련된 이벤트 처리 부분이였고
동적으로 생성된 요소에 이벤트를 바인딩할 때는 직접적인 바인딩 대신 이벤트 위임을 사용해야 한다고 하신다!!
삭제 버튼 이벤트 위임을 위해 .on()메소드를 사용해서 이벤트를 위임해야 한다.

todoBox.on("click", "#remove__btn", function () {
  const todoList = $(this).closest(".todo__list");
  const key = todoList.attr("id");
  localStorage.removeItem(key);
  todoList.remove();
});

나는 클릭했을 때 부모 요소를 찾아서 Local Storage의 데이터를 가지고 오는 코드를 작성했는데 전혀 다른길로 가고 있었던 것.. 부모요소를 통해 click이벤트를 위임해서 이벤트가 발생한 요소가 remove__btn일때 처리 해야 정상 작동이 되는것 이였다 다음에 개발할때 이러한 트러블슈팅을 경험 한다면 보다 빠르게 문제를 해결할 수 있기를!

기능 ISSUE 2024/05/09

할 일 목록을 작성하는 input에 아무것도 작성하지 않아도 할 일이 추가가 되는걸 이제야 할았다..
꽤나 중요한? 기능이라 생각하는데.. 이걸..

createBtn.click(function () {
  // 사용자가 입력한 값
  const todoValue = userWriteInput.val(); 
  const todoData = {
    id: randomNum,
    value: todoValue,
  };
  // 로컬 스토리지에서 데이터 가져오기
  const localTodoData = JSON.parse(localStorage.getItem("todos")) || [];
  // 로컬 스토리지에 새로운 데이터 추가
  localTodoData.unshift(todoData);
  // 로컬 스토리지에 저장
  localStorage.setItem("todos", JSON.stringify(localTodoData));
  const todoListAdd = `
            <div class="todo__list" id="${randomNum}">
                <input type="checkbox">
                <input type="text" disabled id="user__todo" value="${todoValue}">
                <button id="edit__btn"><i class="fa-regular fa-pen-to-square"></i></button>
                <button id="remove__btn"><i class="fa-solid fa-eraser"></i></button>
            </div>`;
  todoBox.prepend(todoListAdd);
  userWriteInput.val("");
});

이게 이전 코드인데 그냥 클릭만 하면 생성해주는 너무 착한 기능을 만들어 뒀다ㅋㅋㅋㅋㅋ
if문을 추가해서 input에 아무것도 입력하지 않았을 때 돌려보내 보자^^

// 새로운 todo 생성
createBtn.click(function () {
  // 사용자가 입력한 값
  const todoValue = userWriteInput.val();
  const todoData = {
    id: randomNum,
    value: todoValue,
  };
  // 수정된 부분
  if (todoValue == "") {
    alert("할 일 목록을 작성해 주세요.");
  } else {
    // 로컬 스토리지에서 데이터 가져오기
    const localTodoData = JSON.parse(localStorage.getItem("todos")) || [];
    // 로컬 스토리지에 새로운 데이터 추가
    localTodoData.unshift(todoData);
    // 로컬 스토리지에 저장
    localStorage.setItem("todos", JSON.stringify(localTodoData));
    const todoListAdd = `
            <div class="todo__list" id="${randomNum}">
                <input type="checkbox">
                <input type="text" disabled id="user__todo" value="${todoValue}">
                <button id="edit__btn"><i class="fa-regular fa-pen-to-square"></i></button>
                <button id="remove__btn"><i class="fa-solid fa-eraser"></i></button>
            </div>`;
    todoBox.prepend(todoListAdd);
    userWriteInput.val("");
  }
});

LocalStorage ISSUE 2024/05/010 10:05

어제 공부를 마치면서 한 번 기능실험을 해 봤는데 TodoList를 여려개를 만들고 하나를 삭제 하면 모든 데이터가 삭제 되는걸 발견... 밤새 뭐가 잘못 됐을까 생각하고 고민하며 잠을 설쳤다ㅠㅠ

// 랜덤 키 생성
const randomNum = Math.random().toString();
// 새로운 todo 생성
createBtn.click(function () {
  // 사용자가 입력한 값
  const todoValue = userWriteInput.val();
  const todoData = {
    id: randomNum,
    value: todoValue,
  };
  if (todoValue == "") {
    alert("할 일 목록을 작성해 주세요.");
  } else { ...

우선 이전 코드를 보면 randomNum변수를 createBtn함수 밖에 선언한게 문제였다
콘솔 로그를 찍어보기 전까지 이렇게 단순한 실수인지 몰랐다 콘솔 로그에 removeBtn을 누르면 todoList를 찍어봤는데 정상 출력이였고 뭔가 이상해서 localStorage를 보니 배열의 ID들이 뭔가 이상해서 보니 모든 배열의 ID가 똑같은게 보였고 곧바로 눈치챘다 randomNum변수 호출에서 문제가 생겼다는걸

createBtn.click(function () {
  // 랜덤 키 생성
  const randomNum = Math.random().toString();
  // 사용자가 입력한 값
  const todoValue = userWriteInput.val();
  const todoData = {
    id: randomNum,
    value: todoValue,
  };
  if (todoValue == "") {
    alert("할 일 목록을 작성해 주세요.");
  } else { ...

요롷게 수정해 주니 정상적으로 돌아왔다!!

기능 ISSUE 2024/05/010 11:15

TodoList 체크 기능을 만들던 도중 발생
여러개의 리스트가 있을때 하나의 체크박스를 체크하고 또 다른 체크박스를 체크 했을때 체크가 안됨
문제 = 각각 다른 count를 사용 해야 하는데 하나의 count를 모든 체크박스가 사용해서 문제가 된거 같음.

// 체크박스 체크on off 기능
let count = 2;
todoBox.on("change", "#ckbox", function () {
  const ckCount = count++;
  if (ckCount % 2 === 0) {
    $(this).parent().addClass("complete");
  } else if (ckCount % 1 === 0) {
    $(this).parent().removeClass("complete");
  }
  console.log(ckCount);
}); ...

수정본


// 체크박스 체크on off 기능
todoBox.on("change", "#ckbox", function () {
  // 체크박스에 저장된 count 값을 가져온다. 없으면 초기값 0을 사용
  let ckCount = $(this).data("count") || 0;

  // 카운트를 증가시킨다
  ckCount++;

  // 체크박스에 새로운 count 값을 저장
  $(this).data("count", ckCount);

  // 홀수, 짝수에 따라 클래스 추가 또는 제거
  if (ckCount % 2 === 1) {
    $(this).parent().addClass("complete");
  } else {
    $(this).parent().removeClass("complete");
  }

});

.data()메소드를 이용해 요소에 저장된 데이터를 가져옴 논리연산자를 이용해 만약 데이터가 없다면 값을 0으로 반환
데이터가 이미 설정되어 있으면 데이터 값을 ckCount 변수에 저장, 만약 이전에 클릭된 적이 없어 데이터가 없다면 ckCount는 0으로 초기화

위의 코드는 개발 하던 도중 알았다.. 이렇게 하면 안된다는 것을.. 로컬 스토리지에 저장하고 페이지를 리랜더링 했을때 저장된 checked값을 가져와 사용자에게 보여주어야 한다 hasClass메소드를 이용해 complete가 있으면 없애주고 없다면 만들어주는 코드로 바꿨다

// 체크박스 체크on off 기능
todoBox.on("change", "#ckbox", function () {
  if ($(this).parent().hasClass("complete")) {
    $(this).parent().removeClass("complete");
  } else {
    $(this).parent().addClass("complete");
  }
  const todoId = $(this).closest(".todo__list").attr("id");
  const isChecked = $(this).prop("checked");

  let localTodoData = JSON.parse(localStorage.getItem("todos")) || [];
  const todoIndex = localTodoData.findIndex((todo) => todo.id === todoId);
  localTodoData[todoIndex].checked = isChecked;
  localStorage.setItem("todos", JSON.stringify(localTodoData));
});

완성본 Github, 배포 링크

0개의 댓글