[Javascript] Todo List 만들기 ③ - localStorage에 데이터 저장하고 가져오기

seoyeon·2023년 1월 5일
3

벌써 이만큼이나 만들었네요!

이렇게 만든거.. 새로고침하면 싹 날아가잖아요 🥺
애정을 담아 만든건데 🥺🥺

그래서 오늘은 로컬 스토리지에 데이터를 저장하고 꺼내 써볼 거예요!
그럼 새로고침해도 데이터가 남아있어서 날아가지 않아요!

와..~!

5) localStorage에 데이터 저장하기

❗localStorage란?❗
localStorage를 사용하면, 브라우저에 key-value 값을 Storage에 저장할 수 있습니다.
저장한 데이터는 세션간에 공유됩니다.
즉, 세션이 바뀌어도 저장한 데이터가 유지됩니다.

📍 저장해 줄 데이터는 어떤 게 있을까요?

  • 할 일 목록에 있는 리스트들
  • 완료 표시가 된 리스트

우선 리스트들을 저장해 볼까요!

Script

function createTodo () { 
	const todoList = document.querySelector('#todoList');
  
     console.log(todoList.children[0].querySelector('span').textContent)
}
  • 리스트의 텍스트 데이터를 가져오기 위해 textContent 로 사용합니다.
  • todoList의 하위요소인 li는 인덱스 값으로 접근했습니다!
function saveItemsFn () { // 로컬에 데이터 저장하기
    const saveItems = []; // 빈 배열 할당
  	for (let i = 0; i < todoList.children.length; i++){
		saveItems.push(); // 배열 추가
    }
}
  • saveItemsFn 이라는 이름으로 함수를 생성합니다.
  • 여러개의 데이터를 담을 빈 배열 을 변수에 저장합니다.
  • 인덱스 값으로 가져오고 있는걸 확인 -> 반복문 사용
  • 배열을 추가하는 push() 메소드 사용
function saveItemsFn () { // 로컬에 데이터 저장하기
    const saveItems = []; // 빈 배열 할당
  	for (let i = 0; i < todoList.children.length; i++){
      	const todoObj = {
        	contents: todoList.children[i].querySelector('span').textContent, // 리스트 목록
          	complete: todoList.children[i].classList.contains('complete') // 완료 표시된 리스트
        };
		saveItems.push(todoObj); // 배열 추가
    }
  	console.log(saveItems)
}

아까 저장해 줄 데이터가 2개 있다고 했죠?
객체 형태로 담아서 저장해 볼게요.

  • todoObj 라는 이름을 가진 변수에 객체를 만듭니다.
  • contents key에는 위에서 확인해 본 리스트의 텍스트 데이터를,
  • complete key에는 contains 메소드로 complete 라는 클래스명이 있는지 확인할게요.
  • todoObj를 push() 합니다 -> 배열 안에 객체가 포함되는 구조!

📝 콘솔 확인

배열 안에 객체가 잘 담겼죠?
complete에는 false 가 나오네요! 🧐
왜냐? contains는 boolean 값으로 반환을 해주기 때문입니다!

complete라는 클래스명이 있으면 true를! 없으면 false를 반환해 줘서
지금은 false만 나오고 있네요

newBtn.addEventListener('click', () => {
	newLi.classList.toggle('complete');
  
	saveItemsFn();
});

체크박스 버튼을 누를때 마다 확인을 해줘야 하니까 newBtn 클릭 이벤트에 넣어줍니다.

이제..! 이 배열을 로컬에 저장하겠습니다!
그런데.. 로컬에는 문자열만 저장이 가능한 사실.. 알고 있었나요..? 🫠
산 넘어 산이네요..🏔

❗ 배열을 문자열로 변환하기

배열이나 객체 자체를 문자열로 변환해 줄 수 있는 JSON 이라는 데이터 포맷이 존재!

JSON 은 데이터 통신할 때 많이 사용하고 로컬 스토리지에 배열이나 객체 데이터를 저장할 때도 많이 사용합니다!

function saveItemsFn () { // 로컬에 데이터 저장하기
    const saveItems = [];
    for (let i = 0; i < todoList.children.length; i++){
        const todoObj = {
            contents: todoList.children[i].querySelector('span').textContent, // 리스트 목록
            complete: todoList.children[i].classList.contains('complete') // 완료 표시된 리스트
        };
        saveItems.push(todoObj); // 배열 추가
    }
    console.log(JSON.stringify(saveItems)) // 문자열로 변환
}

JSON 사용방법은 .stringify() 라는 메소드와 같이 쓰입니다.
JSON.stringify() - () 안에 문자열로 변환하고 싶은 데이터를 넣어주면 됩니다!

📝 콘솔 확인

문자열로 잘 나오네요!
이제 진짜 로컬 스토리지에 저장해 봅시다! 🥹

❗setItem() - localStorage에 저장하기❗

  • localStorage.setItem(key, value)

나중에 로컬 스토리지 사용법도 포스팅 할게요!
(점점 쌓여가는 임시글..)

function saveItemsFn () { // 로컬에 데이터 저장하기
    const saveItems = [];
    for (let i = 0; i < todoList.children.length; i++){
        const todoObj = {
            contents: todoList.children[i].querySelector('span').textContent, // 리스트 목록
            complete: todoList.children[i].classList.contains('complete') // 완료 표시된 리스트
        };
        saveItems.push(todoObj); // 배열 추가
    }
	localStorage.setItem('saved-items', JSON.stringify(saveItems)); // localStorage 추가
}

key 값으로는 saved-items 이라는 이름을 지어줬고
value 값에는 문자열로 변환된 JSON.stringify(saveItems) 를 넣어줬습니다.

개발자 도구를 열어서 Application 에 들어가보면 localStorage에 잘 저장된걸 확인할 수 있습니다!
와~! 🥳

6) localStorage에서 데이터 가져오기

이제 새로고침하거나 브라우저를 나갔다가 다시 들어왔을때 localStorage에서 데이터를 가져올 수 있게 작업을 해볼게요!

❗getItem() - localStorage에서 가져오기❗

  • localStorage.getItem(key)
const savedTodoList = localStorage.getItem('saved-items');
console.log(savedTodoList)

가져오고 싶은 데이터의 key 값을 적습니다.

📝 콘솔 확인

오호 이게 뭘까요?
아까 변환시켜준 문자열이네요!

아까 객체 형태로 저장해뒀는데 문자열로 나오네? 안되지 안돼 ❌❌❌
다시 원본 데이터 형태로 변환합시다!
이랬다 저랬다 손이 많이 가네..

❗JSON.parse()❗
JSON 문자열을 객체나 배열로 변환하는 메소드

parse 메소드를 사용해 줄게요 🪄

const savedTodoList = JSON.parse(localStorage.getItem('saved-items'));
console.log(savedTodoList)

📝 콘솔 확인

짜잔 다시 원래되로 돌아왔어요!
이제 savedTodoList 를 활용해 볼게요!

const savedTodoList = JSON.parse(localStorage.getItem('saved-items'));

if (savedTodoList) { // savedTodoList(로컬 데이터)가 존재하면 실행
    for(let i = 0; i < savedTodoList.length; i++){
        createTodo(savedTodoList[i]) // 전달인자로 전달
    }
}

function createTodo (storageData) { // 매개변수
    const todoList = document.querySelector('#todoList');
    const newLi = document.createElement('li');
    const newBtn = document.createElement('button');
    const newSpan = document.createElement('span');
    const todoInput = document.querySelector('#todoInput');
    const deleteAll = document.querySelector('.delete-btn-wrap');

    newLi.appendChild(newBtn);
    newLi.appendChild(newSpan);
    // console.log(newLi)

    newSpan.textContent = todoInput.value;

    todoList.appendChild(newLi);
    // console.log(todoList)

    todoInput.value = '';

    newBtn.addEventListener('click', () => {
        newLi.classList.toggle('complete');

        saveItemsFn();
    });

    newLi.addEventListener('dblclick', () => {
        newLi.remove();
    });

    saveItemsFn();
};

❗전달인자와 매개변수❗
전달인자 : 말 그대로 전달하는 인자!
매개변수 : 함수를 호출할 때 인수로 전달된 값을 함수 내부에서 사용할 수 있게 해주는 변수

  • savedTodoList가 있으면 즉, 로컬에 데이터가 존재하면 그 데이터의 갯수만큼 createTodo 를 실행시켜라
  • 이때 savedTodoList[i] 를 전달인자로 전달해줍니다.
  • 전달해 주면 받을 매개변수도 있어야겠죠? storageData 라는 이름의 매개변수로 받을게요!

갑자기 너무 어려워진 느낌.. 😵‍💫

이제 매개변수인 storageData 를 사용해 볼게요!

const todoInput = document.querySelector('#todoInput');

function createTodo (storageData) {
    let todoContents = todoInput.value;
    if (storageData) {
        todoContents = storageData.contents
    }

    const todoList = document.querySelector('#todoList');
    const newLi = document.createElement('li');
    const newBtn = document.createElement('button');
    const newSpan = document.createElement('span');
    const deleteAll = document.querySelector('.delete-btn-wrap');

    newLi.appendChild(newBtn);
    newLi.appendChild(newSpan);
    // console.log(newLi)

    newSpan.textContent = todoInput.value;

    todoList.appendChild(newLi);
    // console.log(todoList)

    todoInput.value = '';

    newBtn.addEventListener('click', () => {
        newLi.classList.toggle('complete');

        saveItemsFn();
    });

    newLi.addEventListener('dblclick', () => {
        newLi.remove();
    });

    saveItemsFn();
};
  • todoContents 라는 변수를 만들어주고 todoInput.value 를 할당합니다.
  • 만들어뒀던 todoInput 변수는 전역변수로 사용하기 위해 젤 위로 위치를 옮깁니다!

todoInput.value는 저기에 입력된 값이죠?! ⤴

  • 조건문을 만들어 storageData 가 존재한다면(데이터가 존재한다면) todoContents 변수에 storageData.contents 를 재할당 해줍니다.

storageData.contents 는 저기! 객체가 담겨져 있는 contents!
todoInput의 value와 같은 값이죠!

newSpan.textContent = todoInput.value;

그러면 createTodo 함수 안에 있는 이 코드를

newSpan.textContent = todoContents;

이렇게 바꿀 수 있겠죠!

📝 실행 화면

새로고침 해도 데이터가 날라가지 않습니다! 🥳

완료 표시도 데이터가 불러왔을때 있어야하니까

if (storageData && storageData.complete === true) {
	newLi.classList.add('complete')
}

createTodo() 함수 안에 if문 하나 넣어줍니다.

newLi.addEventListener('dblclick', () => {
	newLi.remove();

	saveItemsFn();
});

더블클릭 했을 때도 저장해줍시다!

function deleteAll() { // 전체 삭제 버튼
    const liList = document.querySelectorAll('#todoList li');
    // console.log(liList[0])
    for ( let i = 0; i < liList.length; i++){
        liList[i].remove();
        // console.log(liList[i])
    }
    saveItemsFn();
};

똑같이 전체 삭제 했을때도!

📝 실행 화면

와앙! 🥹🥹

그런데 전체 삭제를 눌렀을 때 Application 을 보면 [] 빈배열이 남아있죠?
이거 필요없으니까 없애주겠습니다!

❗removeItem() - localStorage에 값 삭제하기❗

  • localStorage.removeItem(key)
function saveItemsFn () { 
    const saveItems = [];
    for (let i = 0; i < todoList.children.length; i++){
        const todoObj = {
            contents: todoList.children[i].querySelector('span').textContent,
            complete: todoList.children[i].classList.contains('complete')
        };
        saveItems.push(todoObj);
    }

    if (saveItems.length === 0) { // 데이터가 없다면 값 삭제
        localStorage.removeItem('saved-items')
    }else{
        localStorage.setItem('saved-items', JSON.stringify(saveItems));
    }
}
  • if문을 이용해 saveItems에 데이터가 없으면 로컬 스토리지에 값을 지워줍니다.
  • else문에는 아까 작성한 데이터를 저장하는 코드를 넣어줍니다.

📝 실행 화면

깔끔 ✨

📝 전체 코드

<!DOCTYPE html>
<html lang="en">
<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">
    <title>Document</title>
	<link rel="stylesheet" href="./style.css">
	<script type="text/javascript" src="./todo.js"></script>
</head>
<body>
    <div class="todo-container">
        <h1>Todo</h1>
        <div id="inputField">
            <input type="text" id="todoInput" placeholder="할 일 추가하기" onkeydown="keyCodeCheck()">
            <button type="button" id="addBtn">
                <span></span>
                <span></span>
            </button>
        </div>
        <ul id="todoList">

        </ul>
        <div class="delete-btn-wrap">
            <button onclick="deleteAll();">전체 삭제</button>
        </div>
    </div>
</body>
</html>

CSS

* {
    box-sizing: border-box;
}

body {
    background-color: #fff;
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: 0;
    min-height: 100vh;
}

h1 {
    margin: 0;
    margin-bottom: 12px;
}

.todo-container {
    max-width: 400px;
    width: 100%;
    background-color: #FFE8AD;
    text-align: center;
    padding: 20px;
    margin-top: 40px;
}

#inputField #todoInput {
    width: calc(100% - 45px);
    border: 1px solid #eee;
    border-radius: 4px;
    padding: 10px;
}

#inputField #todoInput:focus {
    outline: none;
}

#inputField #addBtn {
    position: relative;
    width: 35px;
    height: 35px;
    border: none;
    background-color: #242423;
    border-radius: 4px;
    cursor: pointer;
    vertical-align: middle;
}

#inputField #addBtn span {
    display: block;
    width: 2px;
    height: 15px;
    background-color: #FFE8AD;
    position: absolute;
    transform: translate(-50%,-50%);
    top: 50%;
    left: 50%;
}

#inputField #addBtn span:last-child {
    transform: translate(-50%,-50%) rotate(-90deg);
}

#todoList {
    list-style: none;
    margin: 0;
    padding: 10px;
    text-align: left;
}

#todoList li {
    padding: 10px 0;
    user-select: none;
}

#todoList li.complete {
    text-decoration: line-through;
    color: rgb(155, 155, 155);
}

#todoList button {
    width: 15px;
    height: 15px;
    background-color: #fff;
    margin-right: 8px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

#todoList li.complete button::after {
    content: "";
    display: block;
    width: 5px;
    height: 10px;
    transform: translate(-2px, -2px) rotate(45deg);
    border-right: 2px solid #f1b116;
    border-bottom: 2px solid #f1b116;
}

.delete-btn-wrap button {
    cursor: pointer;
    border: none;
    border-radius: 8px;
    background-color: #242423;
    color: #fff;
    padding: 8px;
}

Script

const todoInput = document.querySelector('#todoInput');
const addBtn = document.querySelector('#addBtn');

const savedTodoList = JSON.parse(localStorage.getItem('saved-items'));
console.log(savedTodoList)

if (savedTodoList) { // 로컬에서 데이터 가져오기
    for(let i = 0; i < savedTodoList.length; i++){
        createTodo(savedTodoList[i])
    }
}

function keyCodeCheck () { // 엔터키로 추가
	if(window.event.keyCode === 13 && todoInput.value !== ''){
        createTodo();
    }
}

addBtn.addEventListener('click', () => { // + 버튼으로 추가
    if(todoInput.value !== ''){
        createTodo();
    }
});

function createTodo (storageData) { // 할 일 추가 기능
    let todoContents = todoInput.value;
    if (storageData) {
        todoContents = storageData.contents
    }

    const todoList = document.querySelector('#todoList');
    const newLi = document.createElement('li');
    const newBtn = document.createElement('button');
    const newSpan = document.createElement('span');
    const deleteAll = document.querySelector('.delete-btn-wrap');

    newLi.appendChild(newBtn);
    newLi.appendChild(newSpan);

    newSpan.textContent = todoContents

    todoList.appendChild(newLi);

    todoInput.value = '';

    newBtn.addEventListener('click', () => { // 체크박스 클릭
        newLi.classList.toggle('complete');

        saveItemsFn();
    });

    newLi.addEventListener('dblclick', () => { // 더블 클릭
        newLi.remove();

        saveItemsFn();
    });

    if (storageData && storageData.complete === true) {
        newLi.classList.add('complete')
    }

    saveItemsFn();
};

function deleteAll() { // 전체 삭제 버튼
    const liList = document.querySelectorAll('#todoList li');
    for ( let i = 0; i < liList.length; i++){
        liList[i].remove();
    }
    saveItemsFn();
};

function saveItemsFn () { // 로컬에 데이터 저장하기
    const saveItems = [];
    for (let i = 0; i < todoList.children.length; i++){
        const todoObj = {
            contents: todoList.children[i].querySelector('span').textContent,
            complete: todoList.children[i].classList.contains('complete')
        };
        saveItems.push(todoObj);
    }

    if (saveItems.length === 0) {
        localStorage.removeItem('saved-items')
    }else{
        localStorage.setItem('saved-items', JSON.stringify(saveItems));
    }
}

와아..!

내가 헷갈려서 하는 정리 💦

if (savedTodoList) { // 로컬에서 데이터 가져오기
    for(let i = 0; i < savedTodoList.length; i++){
        createTodo(savedTodoList[i])
    }
}
  • if문을 통해 로컬 스토리지에서 가져 온 데이터가 동작 한다면 반복문 실행!
  • 로컬 스토리지에 저장되어 있는 데이터 하나하나가 createTodo 함수에 담겨짐 -> storageData 라는 매개변수로 받음
function createTodo (storageData) { // 할 일 추가 기능
    let todoContents = todoInput.value;
    if (storageData) {
        todoContents = storageData.contents
    }

    const todoList = document.querySelector('#todoList');
    const newLi = document.createElement('li');
    const newBtn = document.createElement('button');
    const newSpan = document.createElement('span');
    const deleteAll = document.querySelector('.delete-btn-wrap');

    newLi.appendChild(newBtn);
    newLi.appendChild(newSpan);

    newSpan.textContent = todoContents

    todoList.appendChild(newLi);

    todoInput.value = '';

    newBtn.addEventListener('click', () => { // 체크박스 클릭
        newLi.classList.toggle('complete');

        saveItemsFn();
    });

    newLi.addEventListener('dblclick', () => { // 더블 클릭
        newLi.remove();

        saveItemsFn();
    });

    if (storageData && storageData.complete === true) {
        newLi.classList.add('complete')
    }

    saveItemsFn();
};
  • 로컬 스토리지에 저장되어 있던 데이터들이 생성됨!

✨ 결과물

와아 드디어 끝났습니다..! 🥹
로컬에서 가져오는 부분에서 헤맸는데..
좀 더 연습하고 실습을 많이 해봐야 할 거 같아요

멀고도 먼 자바스크립트의 길..
앞으로도 계속 해멜 거 같지만 그래도 열심히 걸어가 보겠습니다..! 👊

0개의 댓글