개발/JavaScript

[JavaScript] JavaScript로 Todo List 만들기

xuwon 2024. 10. 18. 01:32

JavaScript를 복습하는 차원에서, 간단한 Todo List를 만들어 보았습니다.

완성본은 이렇게 생겼습니다!

 

 

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>TODO-LIST</title>
        <link href="styles.css" rel="stylesheet">
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link
            href="https://fonts.googleapis.com/css2?family=Lilita+One&display=swap"
            rel="stylesheet">
        <link
            href="https://fonts.googleapis.com/css2?family=Do+Hyeon&family=Gowun+Dodum&family=Jua&family=Lilita+One&display=swap"
            rel="stylesheet">
        <link
            href="https://fonts.googleapis.com/css2?family=Do+Hyeon&family=Gowun+Dodum&family=Jua&family=Lilita+One&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
            rel="stylesheet">
    </head>
    <body>
        <div class="container">
            <header class="header">
                <div style="display: flex; align-items: center; gap: 20px">
                    <img src="calendar.png" style="width: 32px; height: 32px;">
                    <p class="header-text">TODO LIST</p>
                    <img src="calendar.png" style="width: 32px; height: 32px;">
                </div>
                <div class="header-form">
                    <input id="input-todo" type="text" autocomplete="off">
                    <button id="add-button"
                        style="font-size: large;">추가</button>
                    <button id="delete-button" style="font-size: large;">전체
                        삭제</button>
                </div>
            </header>

            <main class="main">
                <div class="main-list"></div>
            </main>

            <footer class="footer">
                <p
                    style="font-size: 15px; color: rgb(66, 66, 66); font-family: Roboto, sans-serif; font-weight: 500; font-style: normal;">xuwon's
                    TODO-LIST</p>
                <div class="footer-sns" style="display: flex; gap: 5px;">
                    <img src="link.png">
                    <p
                        style="font-size: 13px; color: rgb(166, 166, 166); font-family: Roboto, sans-serif; font-weight: 500; font-style: normal;"">xuwon.tistory.com
                        | github.com/xuuwon</p>
                </div>
            </footer>
        </div>

        <script src="script.js"></script>
    </body>
</html>

먼저, HTML 파일입니다.

container 클래스 안에 header, main, footer로 구성하였습니다.

header에는 TODO LIST라는 제목, 그리고 입력창과 추가, 전체 삭제 버튼이 포함되어 있습니다.
main에는 추가하려는 리스트들이 들어가게 됩니다.
footer에는 그냥 제 정보를 조금 끄적여 봤습니다.

 

바로 JavaScript 코드로 넘어가 보겠습니다.

const headerText = document.querySelector(".header-text");
const mainList = document.querySelector(".main-list");
const inputTodo = document.querySelector("#input-todo");
const addBtn = document.querySelector("#add-button");
const deleteBtn = document.querySelector("#delete-button");

먼저 받아와야 하는 요소들을 document.querySelector() 를 통해 받아옵니다.

 

Todo List 생성하기

 

우선 Todo List를 생성하는 함수를 작성했습니다.

function makeTodoList (inputText, isDone) {
    const list = document.createElement('div');
    const listButtons = document.createElement('div');
    list.className = "list";
    listButtons.className = "list-buttons";

    inputText = inputText || inputTodo.value; // 로컬 스토리지에서 불러올 때 사용

    if (isDone) { // 로컬 스토리지에서 불러올 때
        list.classList.add('done-list');
    }

    const todoText = document.createElement('p');
    todoText.textContent = inputText;
    todoText.className = "todo-text";
    console.log(todoText.textContent)

    const checkBtn = document.createElement('button');
    checkBtn.textContent = "Done!";
    checkBtn.className = "done-button";

    const deleteListBtn = document.createElement('button');
    deleteListBtn.textContent = "Delete";
    deleteListBtn.className = "delete-button";

    list.appendChild(todoText);
    listButtons.appendChild(checkBtn);
    listButtons.appendChild(deleteListBtn);

    list.appendChild(listButtons);

    mainList.appendChild(list);

    // 버튼들 이벤트리스너

    // 하나의 리스트 삭제하기
    deleteListBtn.addEventListener('click', function() {
        mainList.removeChild(list);

        saveList();
    }) 
    
    // 완료 처리하기
    checkBtn.addEventListener('click', function() {
        let firstChild = mainList.firstChild;

        mainList.removeChild(list);
        list.classList.contains('done-list') ? mainList.insertBefore(list, firstChild) 
        : mainList.appendChild(list);
        list.classList.toggle('done-list');

        saveList();
    }) 
}

우선... 함수가 너무 길죠;;
차차 수정하도록 하겠습니다 흑흑..

 

저는 우선 위처럼 한 리스트 안에 내용과, Done 버튼, Delete 버튼을 추가하기 위해
document.createElement() div 태그를 생성하고, 그 안에 appendChild() 를 활용하여 자식으로 넣어주는 방식으로 진행하였습니다.

const list = document.createElement('div');
const listButtons = document.createElement('div');
list.className = "list";
listButtons.className = "list-buttons";

className은 추후 CSS 적용을 위하여 작성해 주었습니다.

list에는 할 일의 내용이 들어갈 것이고,
listButtons에는 Done! 버튼과 Delete 버튼이 들어가게 됩니다.

const todoText = document.createElement('p');
todoText.textContent = inputText;
todoText.className = "todo-text";

const checkBtn = document.createElement('button');
checkBtn.textContent = "Done!";
checkBtn.className = "done-button";

const deleteListBtn = document.createElement('button');
deleteListBtn.textContent = "Delete";
deleteListBtn.className = "delete-button";

list.appendChild(todoText);
listButtons.appendChild(checkBtn);
listButtons.appendChild(deleteListBtn);

list.appendChild(listButtons);

mainList.appendChild(list);


이제 텍스트와 버튼들도 하나씩 생성해 줍니다.

listButtons에 버튼들을 넣어준 뒤, list에 자식으로 추가해 주었습니다.
그리고 최종적으로 mainList에 추가해 주면 됩니다.


// 하나의 리스트 삭제하기
deleteListBtn.addEventListener('click', function() {
    mainList.removeChild(list);

    saveList();
})

하나의 리스트를 삭제하는 버튼입니다.

어렵지 않게 그냥 Delete 버튼에 click 이벤트가 발생했을 때,
removeChild() 로 해당 listmainList에서 삭제해 주는 이벤트 리스너 함수를 작성해 주면 됩니다.

saveList() 는 로컬 스토리지에 업데이트 하는 내용인데, 추후에 설명하겠습니다.

 

// 완료 처리하기
checkBtn.addEventListener('click', function() {
    let firstChild = mainList.firstChild;

    mainList.removeChild(list);
    list.classList.contains('done-list') ? mainList.insertBefore(list, firstChild) 
    : mainList.appendChild(list);
    list.classList.toggle('done-list');

    saveList();
})

완료 처리를 하는 이벤트 리스너 함수입니다.

우선 저는, 완료되지 않은 항목에 Done! 버튼이 눌렸을 때
removeChild() mainList에서 먼저 삭제하고
appendChild() mainList에 다시 추가하였습니다.

appendChild() 는 맨 뒤에 요소가 추가되기 때문에 완료된 리스트들을 밑으로 보낼 수 있습니다.
그리고 취소선 처리를 위해 'done-list' 클래스를 토글 처리 하였습니다.




두번째는 완료된 항목에서 Done! 버튼을 눌러 완료를 취소할 때입니다.
완료된 항목만 맨 아래에 두기 위해, 완료 취소가 된 항목을 맨 위로 올려주는 방식으로 작성하였습니다.

  let firstChild = mainList.firstChild;  
우선 firstChild를 선언해서 mainList의 맨 첫번째 자식 요소를 할당합니다.

그리고 완료 처리와 같은 방식으로 Done! 버튼이 클릭된 항목을 mainList에서 제거합니다.


이후 'done-list'classList에 포함된 경우 (완료되었던 항목),

  mainList.insertBefore(listfirstChild)  

insertBefore() listfirstChild 앞에 다시 넣어줍니다.

그렇다면 'done-list'classList에 포함되지 않은 경우(완료되기 전 항목)는
앞에서 언급한대로 appendChild() 로 뒤에 추가해주면 됩니다.


이렇게 하면 Todo list를 생성하는 함수는 마무리 됩니다.

해당 함수는 header에 위치한 추가 버튼에 이벤트 리스너 함수로 등록해주면 됩니다.

 

addBtn.addEventListener('click', function() {
    if(inputTodo.value.trim()) {
        makeTodoList();
        saveList(); // 추가 후 로컬 스토리지 업데이트
        inputTodo.value = '';
    }
})

inputTodo.value가 비어있는 경우엔 추가되지 않도록 조건문으로 구분해 주었습니다.
(공백 또한 제한하기 위해 trim() 메소드를 사용하였습니다.)

그리고 입력창에 내용을 작성하고 추가 버튼을 누른 후에는 입력창이 비도록   inputTodo.value = ''  를 작성해 줍니다.



추가 버튼으로 항목을 추가하는 것도 좋지만, 저는 enter키를 눌렀을 때도 추가가 되도록
inputTodokeydown 이벤트가 발생했을 때에도 항목이 생성되도록 작성했습니다.

inputTodo.addEventListener('keydown', function(event) {
    if (event.key === 'Enter') {
        if(inputTodo.value) {
            makeTodoList();
            saveList(); // 추가 후 로컬 스토리지 업데이트
            inputTodo.value = '';
        }
    }
})

event 객체의 key 속성에는 사용자가 누른 키가 담겨있습니다.
따라서 enter 키를 누른 경우, 똑같이 항목이 추가됩니다.


전체 삭제
// 전체 삭제
deleteBtn.addEventListener('click', function() {
    mainList.innerHTML = '';
    localStorage.removeItem('todos'); // 로컬 스토리지에서 삭제
})

전체 삭제 버튼입니다.

전체 삭제 버튼에 click 이벤트가 발생했을 때,
mainListinnerHTML을 비워줍니다.


그리고   localStorage.removeItem('todos'); 
로컬 스토리지에서 삭제해 줍니다.


이렇게 하면 모든 기능은 구현이 완료되었습니다.

저는 여기서 추가로, 새로고침이 되었을 때도 내용이 유지되기 위해
로컬 스토리지에 저장하고, 불러오는 내용까지 작성하였습니다.



로컬 스토리지에 리스트 저장
function saveList() {
    const todos = [];
    const lists = document.querySelectorAll('.list'); // list 내용을 노드 리스트 형태로 넣음
    lists.forEach(list => {
        todos.push({
            text: list.children[0].textContent,
            done: list.classList.contains('done-list')
        });
    });
    localStorage.setItem('todos', JSON.stringify(todos));
}

우선 lists는 현재까지 작성된 리스트들이 담긴 노드 리스트입니다.
todos는 빈 배열로, 각 리스트들의 textdone 여부를 저장할 것입니다.

lists.forEach(list => {
    todos.push({
        text: list.children[0].textContent,
        done: list.classList.contains('done-list')
    });
});

forEach() 고차함수를 이용해서 각 리스트를 순회하여
각 항목의 textdonetodos 배열에 추가합니다.

list에는 자식들이 많은데, 앞에서 text를 맨 처음에 appendChild 처리했기 때문에
list.children[0].textcontent text가 됩니다!

이렇게 두 개 항목이 담겨있는 경우

로컬 스토리지에 정상적으로 textdone이 들어간 것을 볼 수 있습니다!



saveList() 함수는 항목이 추가되거나 삭제되거나 완료 처리가 되었을 때마다 실행되어야 로컬 스토리지에 업데이트 할 수 있습니다.
따라서 각 함수들에 넣어주었습니다.

 

로컬 스토리지에서 리스트 불러오기

 

// 로컬 스토리지에서 리스트 불러오기
function loadList() {
    const savedTodos = JSON.parse(localStorage.getItem('todos')) || [];
    savedTodos.forEach(todo => {
        makeTodoList(todo.text, todo.done); // 완료 여부를 전달하여 취소선 적용
    });
}

// 페이지 로드 시 저장된 리스트 불러오기
window.addEventListener('load', loadList);

저장을 했다면 페이지 로드 시 저장된 내용들을 불러와야 합니다.

  localStorage.getItem('todos')  
localStorage.getItem() 를 통해 todos를 받아와 savedTodos 변수에 할당합니다.
todos가 없는 경우 오류가 날 수 있기 때문에, 단축 평가를 사용하여 todos가 없는 경우 빈 배열이 할당되도록 합니다.

||를 사용한 단축 평가는
앞이 falsy한 값이면 뒤의 값을, 앞이 truthy한 값이면 앞의 값이 됩니다.


localStorage.getItem() 으로 받아온 todos는 문자열 형태이므로, JSON.parse() 를 통해 JSON 객체 형태로 변환해줍니다.

해당 객체를 forEach() 를 통해 순회하며,
todo.text todo.done을 매개변수로 넘겨주며 makeTodoList() 를 호출하면 됩니다.

마지막으로 windowload 될 때 리스트를 불러올 수 있도록 이벤트 리스너 함수를 등록해 주면 됩니다.


그렇게 되면 text 내용과 done 여부를 받아 페이지가 로드되었을 때
원래 있던 리스트들을 정상적으로 불러올 수 있게 됩니다.

 


 

아까 Todo List 생성하는 함수에서 설명하지 않은 부분이 있습니다.

inputText = inputText || inputTodo.value; // 로컬 스토리지에서 불러올 때 사용

if (isDone) { // 로컬 스토리지에서 불러올 때
    list.classList.add('done-list');
}

바로 이 부분인데요.

로컬 스토리지에서 불러올 내용이 있을 때만 inputTextisDone을 매개변수로 전달하며 함수를 호출합니다.
매개변수가 없을 때는 새로 추가하는 경우이니, text 내용은 inputTodo.value가 되어야 합니다. (입력창에 입력한 값)

따라서 단축 평가를 통해 inputTextfalsy한 값일 때, (매개변수를 전달하지 않을 경우 undefined가 됩니다.)
inputTodo.value로 평가되게 하고

inputText에 내용이 들어있다면 그대로 inputText로 평가되도록 작성합니다.

 

그리고 isDonetrue일 때만 취소선이 적용되고 Done! 버튼이 색칠됩니다.
따라서 isDonetrue일 때 조건문을 사용하여 classList'done-list'를 추가해주면 됩니다.

 


이렇게 간단한 Todo List 만들기가 끝이 났습니다!

생각보다 쉽게 만들어서 기분이 좋습니다 ㅎㅎ. 성장하고 있는 거 같아요.
현재는 React를 배우는 중인데, React로도 얼른 Todo List를 만들어보고 싶습니다.

 

https://github.com/xuuwon/TodoList-JS

 

GitHub - xuuwon/TodoList-JS

Contribute to xuuwon/TodoList-JS development by creating an account on GitHub.

github.com