localStorage
서버와의 통신 없이 ToDoList 구현하기
이번에는 서버와의 통신 없이 할 일 목록을 관리하기 위한 기능을 구현해 보도록 하겠습니다
기능에 대해서 간단히 말하자면 주어진 HTML문서에 있는 요소들을 선택하고, 사용자 입력에 따라 서버와의 통신 없이 할 일을 추가 및 삭제하는 것을 구현해보고자 합니다.
ToDoList 구현
html Code
...
<form id="todo-form">
<input type="text" placeholder="Write a To Do and Press Enter" />
</form>
<ul id="todo-list"></ul>
...
JavaScript Code
const toDoForm = document.getElementById("todo-form");
const toDoInput = toDoForm.querySelector("input");
const toDoList = document.getElementById("todo-list");
function paintToDo(newTodo) {
const li = document.createElement("li");
const span = document.createElement("span");
li.appendChild(span);
span.innerText = newTodo;
toDoList.appendChild(li);
}
function handleToDoSubmit(event) {
event.preventDefault();
const newTodo = toDoInput.value;
toDoInput.value = "";
paintToDo(newTodo);
}
toDoForm.addEventListener("submit", handleToDoSubmit);
실행화면
이 코드는 할 일 목록을 관리하는 간단한 기능을 구현한 예시입니다. 사용자가 할 일을 입력하고 제출하면 할 일이 목록에 추가되는 동작을 수행합니다.
하지만 아직 기능이 부족해 보입니다. 새로고침을 하면 할 일 목록들이 어딘가에 저장이 되지 않아 초기화되고, 할 일 목록을 추가할 수는 있지만 삭제 기능이 없습니다.
이제부터 하나하나 기능을 추가해 보도록 하겠습니다.
삭제기능
먼저 삭제 기능을 추가하기 위해서 기존코드에 있던 function paintToDo(newTodo) { ... }
에 대해서 설명드리겠습니다.
paintToDo
함수는 새로운 할 일을 화면에 그리는 역할을 합니다.li
변수는 새로운<li>
요소를 생성합니다.span
변수는 새로운<span>
요소를 생성합니다.li.appendChild(span);
은li
요소의 자식 요소로span
요소를 추가합니다.span.innerText = newTodo;
는span
요소의 텍스트 내용으로 새로운 할 일(newTodo
)을 설정합니다.toDoList.appendChild(li);
는li
요소를toDoList
에 추가합니다.
// index.html
...
<ul id="todo-list">
<li>
<span></span>
</li>
</ul>
...
paintToDo 함수가 실행이 된다면 이런 형식으로 부모 요소 ul
에 li
요소와 li
요소의 자식 요소로 span
이 추가될것입니다.
일단 삭제 버튼부터 추가해보겠습니다.
// todo.js
function deleteToDo() {
console.log("I will delete this");
}
function paintToDo(newTodo) {
// const li = document.createElement("li");
// const span = document.createElement("span");
// span.innerText = newTodo;
const button = document.createElement("button");
button.innerText = "❌";
button.addEventListener("click", deleteToDo);
// li.appendChild(span);
li.appendChild(button);
// toDoList.appendChild(li);
}
// 주석 처리 부분은 기존 코드입니다.
li
요소의 자식 요소인 span
요소에 형제 요소인 button
을 추가해 줍니다. 참고로 li.appendChlid(button)
이 li.appendChlid(span)
보다 앞으로 와서 삭제 버튼이 할 일 목록의 앞으로 오지 않도록 주의합니다.
button
이 Click 되면 deleteToDo()
가 실행됩니다. 이벤트 리스너를 등록하고 콘솔로 테스트를 진행해 보겠습니다.
<아직 삭제 기능은 구현되지 않은 상태입니다>
Click된 li
요소를 선택해 삭제해야 하는데 어떤 li
요소가 선택됐는지 구분할 수 없는 문제점이 발생하였습니다.
function deleteToDo()
에서 click 이벤트에 대해서 살펴보겠습니다.
// todo.js
...
function deleteToDo(event) {
console.log(event);
}
...
JavaScript의 이벤트(Event)객체는 일부 속성(properties)을 가지는데, 그 중 이벤트가 발생한 HTML 요소를 나타내는 target
속성이 있습니다.
target
속성은 일반적으로 이벤트가 발생한 요소를 가리키지만, 경우에 따라서는 실제 이벤트가 발생한 요소의 부모 요소를 참조하는 경우도 있습니다. 이때, target
속성을 통해 부모 요소에 접근하려면 target
속성의 parentElement
속성을 사용합니다. 이 점을 활용하여 다시 콘솔에 테스트 해보겠습니다.
// todo.js
...
function deleteToDo(event) {
console.log(event.target.parentElement);
}
...
이제 제가 Click한 button
의 부모 요소를 구분할 수 있습니다. 이는 곧 어떤 li
요소가 선택됐는지 구분할 수 있다는 것입니다. 이제 저희는 어떤 li
를 삭제해야할지 알게되었습니다.
자 이제 위의 결과를 활용하여 function deleteToDo()
를 수정해보겠습니다.
// todo.js
...
function deleteToDo(event) {
const li = event.target.parentElement;
li.remove();
}
...
잘 동작하는것을 보실 수 있습니다. 이렇게 삭제 기능을 추가해 보았습니다.
localStorage 사용하여 데이터 저장
이제 실제로 할 일 목록을 브라우저에 저장해보도록 하겠습니다.
브라우저에 저장을 하려면 필요한게 있습니다. 바로 localStorage
입니다.
localStorage
는 웹 브라우저에서 제공하는 웹 스토리지 기술 중 하나입니다. 이 기술을 사용하면 웹 페이지에서 데이터를 클라이언트 측, 즉 사용자의 로컬 컴퓨터에 저장할 수 있습니다. localStorage는 키 - 값 쌍으로 데이터를 저장하며, 저장된 데이터는 웹 페이지가 닫혔다가 다시 열려도 유지됩니다.
먼저 할 일 목록들을 저장하기 위해서 toDos
배열을 만들어 보겠습니다.
// todo.js
...
const toDos = [];
...
제가 원하는건 function paintToDo(newTodo) { … } 에서 newTodo
가 그려질 때마다 그 텍스트를 toDos
배열에 push 하고 싶습니다.
// todo.js
function handleToDoSubmit(event) {
// event.preventDefault();
// const newTodo = toDoInput.value;
// toDoInput.value = "";
toDos.push(newTodo);
// paintToDo(newTodo);
// 주석처리는 기존 Code 입니다.
}
보다시피 toDos
배열에 잘 push
되는걸 확인할 수 있습니다.
이제 localStorage에 한 번 넣어보겠습니다.
toDos
를 localStorage에 넣는 함수를 작성하겠습니다.
그리고 나서 function handleToDoSubmit(){...}
에 추가합니다.
// todo.js...
function saveToDos() {
localStorage.setItem("todos", toDos)
}
...
function handleToDoSubmit(event) {
// event.preventDefault();
// const newTodo = toDoInput.value;
// toDoInput.value = "";
// toDos.push(newTodo);
// paintToDo(newTodo);
// 주석 처리는 기존 Code 입니다.
saveToDos();
}
실행하면 localStorage
에 toDos
저장이 잘 된걸 보실 수 있습니다.
문제점
하지만 위 영상을 보시면 아마 문제점을 찾으셨을겁니다. 첫 번째 문제는 새로고침을 하면 localStorage
에는 그대로 Value 값이 남아있지만 브라우저에서는 나타나지 않는 문제가 생깁니다. 그리고 두 번째 문제는 새로고침 상태로 값을 다시 입력하면 todos의 Value 값이 덮어쓰기 되는걸 보실 수 있을겁니다. 이건 제가 원하는게 아닙니다.
1. 첫 번째 문제 해결과정
먼저 localStorage
를 잘 살펴봅시다.
localStorage
에 저장할 수 있는 데이터는 문자열 형태로 저장이됩니다. 그렇게 때문에 객체나 배열과 같은 복잡한 데이터를 저장하기 위해서는 JSON.stringify()
와 JSON.parse()
를 사용하여 데이터를 직렬화하고 역직렬화해야 합니다.
JSON.stringify() 란 ?
JSON.stringify()
함수는 JavaScript 객체나 배열을 JSON 문자열로 변환합니다.
직렬화된 JSON 문자열은 네트워크를 통해 전송하거나 localStorage
에 저장하는 등의 용도로 사용됩니다.
JSON.stringify()
를 활용하여 Code를 수정하겠습니다.
// todo.js
...
function saveToDos() {
// 기존 Code
// localStorage.setItem("todos", toDos);
// JSON.stringify() 활용
localStorage.setItem("todos", JSON.stringify(toDos));
}
배열 형태의 문자열로 바뀌었습니다.
이제 다시 데이터를 역직렬화 해야합니다. 그때 필요한게 JSON.parse()
함수입니다.
JSON.parse() 란?
JSON.parse() 함수는 JSON 문자열을 JavaScript 객체로 변환합니다.
역직렬화된 객체는 프로그램에서 직접 사용할 수 있습니다.
자 다시 Code를 수정해보겠습니다.
// todo.js
...
const TODOS_KEY = "todos";
const toDos = [];
function saveToDos() {
localStorage.setItem(TODOS_KEY, JSON.stringify(toDos));
}
...
const savedToDos = localStorage.getItem(TODOS_KEY);
if(savedToDos !== null){
const parsedToDos = JSON.parse(savedToDos); // 역직렬화
parsedToDos.forEach(paintToDo);
}
이를 통해서 첫 번째 문제인 새로고침을 하면 브라우저에 할 일 목록이 나타나지 않는 문제를 해결 하였습니다.
2. 두 번째 문제 해결과정
덮어쓰는 문제는 생각보다 간단합니다. 코드를 살펴보면 application 이 시작될 때 toDos
배열은 항상 비어있습니다.
그 상태로 handleToDoSubmit(){...}
이 실행될 떄마다 newTodo
를 toDos
빈배열에 그냥 push 하게 됩니다.
저는 계속 기존의 toDo 들은 갖지 않은 상태로 새로운 toDo 들만 포함하고 있는 배열을 저장하는 꼴입니다.
기존의 toDo들은 localStorage
에 있습니다. 이 문제를 해결하기 위해서는 기존의 할 일 목록과 새로 추가된 할 일 목록을 합쳐서 저장해야 합니다.
우선 toDos
배열을 const
에서 let
으로 바꿔 업데이트가 가능하도록 수정합니다. 그리고 기존 조건문에 toDos = parsedToDos
를 넣어 기존의 toDos
를 복원할겁니다.
// todo.js
...
let toDos = [];
...
if (savedToDos !== null) {
const parsedToDos = JSON.parse(savedToDos);
// 기존의 toDos 복원
toDos = parsedToDos;
parsedToDos.forEach(paintToDo);
}
이제 할 일 목록이 덮어쓰기 되는 두 번째 문제도 해결이 되었습니다.
그런데 사실 문제가 하나 더 남았습니다. 삭제 된 할 일은 toDos
배열에서 제거되지 않으므로, 새로고침 후에는 삭제된 할 일이 다시 나타납니다. 이 문제를 해결하기 위해서 저는 삭제된 할 일에 대한 처리를 추가해야 합니다.
콘솔을 통해서 toDos
를 살펴보겠습니다.
삭제버튼을 누르고 새로고침 하면 여전히 localStorage
에는 값이 남아있습니다. 저는 제가 원하는 item을 지우고 싶습니다. 하지만 어떤 item인지 알 수 없습니다.
제가 위에서 구현했던 deleteToDo
는 JavaScript, HTML 입장으로 화면에서 어떤 HTML의 Element를 지워야 하는지 알고 있습니다. 그렇지만 어떤 todo를 데이터베이스에서 지워야 하는지는 모릅니다. 예를들어 보겠습니다 아래 사진을 살펴봅시다.
만약 같은 A가 여러개 존재한다고 가정합시다. 제가 첫 번째 A를 지우면 어떤걸 지웠는 지 알 수 있을까요? 첫 번째인지 네 번째인지 마지막 번째인지 알 수 없습니다.
저는 값이 각각의 할 일 항목을 고유하게 식별하기 위한 식별자identifier이 필요합니다. 이를 통해서 어떤 항목을 삭제해야 하는지 구분할 수 있도록 해야합니다.
현재 시간을 밀리초 단위로 반환하는 JavaSciprt 내장 함수인 Date.now()
를 사용하여 현재 시간을 기반으로 한 고유한 숫자 값을 할당하겠습니다.
// todo.js
...
function handleToDoSubmit(event) {
event.preventDefault();
const newTodo = toDoInput.value;
toDoInput.value = "";
const newTodoObj = {
text: newTodo,
id: Date.now(),
};
toDos.push(newTodoObj);
paintToDo(newTodo);
saveToDos();
}
아직 크게 바뀐건 없습니다. 그저 toDos
에 text를 저장하지 않고 object를 저장한다는점과 고유 id가 생겼을뿐입니다.
이제 id를 사용해 볼겁니다. 일단 id를 HTML에 두고싶은데 handleToDoSubmit(){...}
함수에서 paintToDo
에 string으로 newTodo
를 주는 것 대신에 newTodoObj
를 주겠습니다. 그리고 paintToDo
함수를 수정하겠습니다.
// todo.js
...
function paintToDo(newTodo) {
...
li.id = newTodo.id;
...
span.innerText = newTodo.text;
...
}
function handleToDoSubmit(event) {
...
const newTodo = toDoInput.value;
...
paintToDo(newTodoObj);
saveToDos();
}
저희는 데이터베이스에게 id를 저장하는 옵션을 줬습니다. 드디어 삭제할 수 있게 된겁니다.
filter()
먼저 filter()
에 대해서 알아보겠습니다.
filter()
는 배열의 각 요소에 대해 주어진 조건을 평가하여 조건을 만족하는 요소들로 이루어진 새로운 배열을 생성합니다. filter()
는 원본 배열을 변경하지 않고 새로운 배열을 반환합니다.
filter()
메서드의 구문은 다음과 같습니다.
const newArray = array.filter(callback(element, index, array));
-
array
: 필수 매개변수로,filter()
를 호출한 배열입니다. -
callback
: 필수 매개변수로, 각 요소를 평가하는 함수입니다.callback
함수는 세 가지 매개변수를 받습니다: element
: 현재 처리 중인 요소입니다.index
(선택적): 현재 요소의 인덱스입니다.-
array
(선택적):filter()
를 호출한 배열입니다. newArray
: 새로운 배열로,callback
함수에서true
를 반환하는 요소들로 이루어집니다.
callback
함수에서는 조건을 평가하여 true
또는 false
를 반환해야 합니다. true
를 반환하는 요소는 결과 배열에 포함되고, false
를 반환하는 요소는 결과 배열에서 제외됩니다.
예를 들어, 숫자 배열에서 짝수만 추출하여 새로운 배열을 생성하는 예제를 살펴보겠습니다:
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(evenNumbers); // [2, 4, 6]
위의 예제에서 filter()
메서드는 numbers
배열의 각 요소를 number
매개변수로 받아와서 해당 요소가 짝수인지 확인합니다. number % 2 === 0
조건을 만족하는 요소들로 이루어진 새로운 배열인 evenNumbers
가 생성되고 출력됩니다.
filter()
를 사용하면 배열에서 원하는 조건에 맞는 요소들만 추출할 수 있어 매우 유용한 메서드입니다.
이제 filter()
를 통해서 toDos
배열에서 해당 항목을 제거하여 화면에도 반영하고 데이터를 동기화 할 수 있게 해봅시다.
클릭했던 li의 id를 갖고 있는 toDo
를 지우고 싶습니다. 다시말해 toDo
의 id가 li의 id와 다른 걸 남기고 싶습니다.
...
function deleteToDo(event) {
const li = event.target.parentElement;
li.remove();
toDos = toDos.filter((toDo) => toDo.id !== parseInt(li.id));
saveToDos();
}
...
이렇게 서버와의 통신 없이 LocalStorage를 통해 데이터 추가, 삭제를 구현해보았습니다.
최종코드
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Momentum App</title>
</head>
<body>
<form id="todo-form">
<input type="text" placeholder="Write a To Do and Press Enter" required />
</form>
<ul id="todo-list"></ul>
<script src="js/todo.js"></script>
</body>
</html>
todo.js
const toDoForm = document.getElementById("todo-form");
const toDoInput = toDoForm.querySelector("input");
const toDoList = document.getElementById("todo-list");
const TODOS_KEY = "todos";
let toDos = [];
function saveToDos() {
localStorage.setItem(TODOS_KEY, JSON.stringify(toDos));
}
function deleteToDo(event) {
const li = event.target.parentElement;
li.remove();
toDos = toDos.filter((toDo) => toDo.id !== parseInt(li.id));
saveToDos();
}
function paintToDo(newTodo) {
const li = document.createElement("li");
li.id = newTodo.id;
const span = document.createElement("span");
span.innerText = newTodo.text;
const button = document.createElement("button");
button.innerText = "❌";
button.addEventListener("click", deleteToDo);
li.appendChild(span);
li.appendChild(button);
toDoList.appendChild(li);
}
function handleToDoSubmit(event) {
event.preventDefault();
const newTodo = toDoInput.value;
toDoInput.value = "";
const newTodoObj = {
text: newTodo,
id: Date.now(),
};
toDos.push(newTodoObj);
paintToDo(newTodoObj);
saveToDos();
}
toDoForm.addEventListener("submit", handleToDoSubmit);
const savedTodos = localStorage.getItem(TODOS_KEY);
if (savedTodos !== null) {
const parsedToDos = JSON.parse(savedTodos);
toDos = parsedToDos;
parsedToDos.forEach(paintToDo);
}
댓글남기기