[Vanilla JS] Momentum 크롬 앱 기능 만들기 - 2. ToDo App
이전 포스팅
[Vanilla JS] Momentum 크롬 앱 기능 만들기 - 1. Greeting (tistory.com)
우리는 첫번째 포스팅에서 아이디를 입력받고, 메인 화면으로 이동하는 방법을 구현했다.
이제 이 앱의 메인 기능인 ToDo 기능을 구현할 것이다.
기능 예시
1. todo item을 입력받아서 enter를 누르면
2. todo list에 이를 추가하고, 이걸 화면에 출력할 것이다.
3. list의 아이템을 클릭하면 취소선을 그으며 회색이 되게할 것이며
4. 오른쪽의 'x'버튼을 누르면 리스트의 아이템을 사라지게 만들 것이다.
기능 구현
todoList 구현하기
먼저 Input을 통해 입력받은 데이터를 저장하고, 이를 리스트로 출력하는 과정을 만들어 보자.
위 구조에서 input을 통해 입력받은 데이터는 id와 text 정보를 담은 객체로 저장되어 ul 밑에 li태그로 출력될 것이다.
Date()객체를 통해 현재 시간을 그대로 id로 사용하면 고유한 id로 활용할 수 있다.
입력받은 객체를 토대로 바로 li태그로 변환하여 ul 태그에 appendChild() 해주면 리스트가 출력된다.
localStorage 활용하기
이제 localStorage를 활용하여 새로고침을 해도 그대로 화면이 유지되도록 해보자.
localStorage는 기본적으로 네트워크 통신을 이용하므로 데이터의 크기를 압축시킬 필요가 있다.
따라서 모든 데이터는 string 형태로 저장된다.
그러므로 객체나 배열같은 데이터들 모두 문자열로 저장되는데, 이를 다시 객체로 만들기 위해서는 어려운 문자열 파싱 작업들이 필요하다.
그런것들을 알아서 해주는 객체가 있는데 JSON 객체이다.
JSON - JavaScript | MDN (mozilla.org)
JSON - JavaScript | MDN
JSON 객체는 JavaScript Object Notation(JSON)을 분석하거나 값을 JSON으로 변환하는 메서드를 가지고 있습니다. JSON을 직접 호출하거나 인스턴스를 생성할 수 없으며, 두 개의 메서드를 제외하면 자신만의
developer.mozilla.org
쉽게말해 JSON.stringify()는 객체를 문자열로 치환해주고, JSON.parse()는 다시 그 문자열을 객체로 바꿔준다.
이를 활용하면 다음과 같다.
새로고침을 하게 되면 todos.forEach()에 의해 기존에 존재하던 배열들로 초기 리스트를 구현하고
이후 submit 이벤트가 발생하게 되면, todos가 업데이트 되어 localStorage에 저장되며 리스트에 추가되어 구현된다.
만약 todos가 localStorage에 존재하지 않는다면 getItem()은 null을 반환하므로, 이에 대한 예외처리도 반드시 필요하다.
delete 액션 구현하기.
delete 액션을 구현하기 위해선 list 오른쪽에 button을 추가해야 한다.
이러면 다음과 같이 버튼이 추가된다.
해당 버튼들에 클릭 이벤트리스너를 달아 이벤트객체를 출력해보면 다음과 같이 이벤트 발생 지점을 알아낼 수 있다.
따라서 해당 이벤트의 id를 알아내어 제거하는 것이 가능하다.
이벤트 객체의 target 속성에는 parentElement 또는 parentNode 속성이 있는데 이게 바로 상위 노드를 가리킨다.
따라서 다음과 같은 코드를 통해 DOM 상에서 해당 list요소를 제거할 수 있다.
그러나 이렇게만 하면 다시 새로고침을 했을 때 사라졌던 요소들이 다시 생기는 문제점이 발생한다.
localStorage 자체에서 해당 요소를 삭제하는 작업이 추가로 필요하다.
그런데 여기서 주의할 점은 todo.id와 parentEl.id의 형식이 다르다는 점이다.
parentEl에는 id 정보가 string이지만, todo.id에서는 숫자형데이터로 존재하기 때문이다.
따라서 한가지 데이터형으로 일치시켜준 다음 filter작업을 거치면 된다.
잘 작동한다.
취소선 작업하기
이제 리스트 요소를 누르면 해당 요소에 취소선을 긋고, 색깔을 조금 연하게 만들어줄 것이다.
classList를 이용하면 쉽게 구현할 수 있다.
결과
결과물
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./css//main.css"/>
<script defer src="./javascript/greeting.js"></script>
<script defer src="./javascript/todo.js"></script>
<title>Momentum practice</title>
</head>
<body>
<div id="greeting">
<form id="login">
<div>Hello, what's your name?</div>
<input type="text" placeholder="이름" />
</form>
<div id="todo" class="hidden">
<div id="hello"></div>
<form>
<input type="text" placeholder="할 일" />
</form>
<ul>
</ul>
</div>
</div>
</body>
</html>
const todoInputForm = document.querySelector("#todo form");
const todoList = document.querySelector("#todo ul");
let todos = JSON.parse(localStorage.getItem("todos"));
if (todos === null) {
todos = [];
} else {
todos.forEach((todo) => {
drawTodos(todo);
});
}
function drawTodos(todo) {
const li = document.createElement("li");
const btn = document.createElement("button");
li.classList.add("cursor");
btn.classList.add("cursor");
li.id = todo.id;
li.innerText = todo.text;
li.addEventListener("click", (e) => {
e.target.classList.toggle("checked");
});
btn.innerText = "X";
btn.addEventListener("click", (e) => {
const parentEl = e.target.parentElement;
const id = parseInt(parentEl.id);
todoList.removeChild(parentEl);
todos = todos.filter((todo) => {
console.log(todo.id, id);
return todo.id !== id;
});
console.log(todos);
localStorage.setItem("todos", JSON.stringify(todos));
});
li.appendChild(btn);
todoList.appendChild(li);
}
todoInputForm.addEventListener("submit", (e) => {
e.preventDefault();
date = new Date();
nextTodo = {
id: date.getTime(), // 데이터의 삭제를 위한 id
text: e.target[0].value,
};
todos.push(nextTodo);
localStorage.setItem("todos", JSON.stringify(todos));
e.target[0].value = "";
drawTodos(nextTodo);
});
body {
background-color: rgb(196, 196, 196);
}
.hidden {
display: none;
}
.cursor {
cursor: pointer;
}
.checked {
color: rgb(43, 43, 43);
text-decoration: line-through;
}