To-do list 바닐라JS로 만들기

쇼핑몰이요? 그거 로그인은 둘째치고 SSL 설정 개꼬여서 걍 인스턴스 엎고 만들라고… 아니 nginx 깔았는데 uWSGI에서 막혀서 하루종일 그거 찾았다니까. 아무도 안 알려줘 그걸…

깝깝해서 오라클 클라우드도 알아봐야 하나 생각중임 지금..


Reference

https://woojong92.tistory.com/entry/JS-%EB%B0%94%EB%8B%90%EB%9D%BC-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-ToDo-List-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%EA%B8%B0%EB%8A%A5%EC%A0%95%EC%9D%98-%EB%B0%8F-HTMLCSS

단계별로 시리즈가 나뉘어져 있는데, 잘 따라하면 기본 기능정도는 구현할 수 있다.

To-do list의 CRUD

전에 쇼핑몰 얘기 하면서 설명했던 CRUD에 대해 잠깐 짚고 넘어가보자. CRUD는 생성/열람/수정/삭제 네 가지라고 했는데… 아니 거기도 크루드가? ㅇㅇ 있음. 어지간한 프로그램에는 다 있다. 여기서 구현할 최소한의 요소이기도 하고… 전에 유기체의 4대 요소(탄수화물/지질/단백질/핵산) 얘기하면서 비슷한거라고 했는데 ㄹㅇ 비슷하다.

그럼 여기서 CRUD는 뭘까? 

  1. Create: 할 일을 추가한다 
  2. Read: 추가한 할 일을 읽는다 
  3. Update: 할 일의 내용이나 상태를 수정한다
  4. Delete: 할 일을 삭제한다 

미리 스포하자면 3번이 제일 빡셌음. 

Create

할 일을 목록에 추가하는 기능이다. 일단 거두절미하고 짤로 보자.

아니 뭐야 이걸 어케해요!!! 아이 나도 했음. 

const todoText = document.querySelector('.todo-text')
const todoAdd = document.querySelector('.todo-add')
const todoDelete = document.querySelector('.delete')
const todoEdit = document.querySelector('.modify')
const todoCheck = document.querySelector('.checkbox')
const todoList = document.querySelector('.todo-list')

let todo = []
let id = 0;
//변수!! 

function setTodo(newTodo) {
    todo = newTodo
}

function getTodo() {
    return todo
}

function addTodo(text) {
    const newId = id++
    const newTodo = getTodo().concat({id: newId, isCompleted: false, content: text })
    setTodo(newTodo)
    displayTodo()
}

function displayTodo() {
    todoList.innerHTML = null;
        const allTodo = getTodo()

    allTodo.forEach(function(todo){
        const todoItem = document.createElement('li')
        const todoCheck = document.createElement('div')
        const todoCont = document.createElement('div')
        const todoBtn = document.createElement('div')
        const todoEdit = document.createElement('button')
        const todoDel = document.createElement('button')

        todoItem.classList.add('todo-item')
        todoCheck.classList.add('checkbox')
        todoCont.classList.add('todo')
        todoBtn.classList.add('todo-button-group')
        todoEdit.classList.add('modify')
        todoDel.classList.add('delete')
        console.log(todoDel)
        todoCont.innerText = todo.content
        todoEdit.innerHTML = '<i class="fa-solid fa-pen"></i>'
        todoDel.innerHTML = '<i class="fa-solid fa-trash-can"></i>'

        if(todo.isCompleted) {
            todoItem.classList.add('checked');
            todoCheck.innerHTML = '<i class="fa-solid fa-check"></i>'
        }

        todoList.appendChild(todoItem)
        todoItem.appendChild(todoCheck)
        todoItem.appendChild(todoCont)
        todoItem.appendChild(todoBtn)
        todoBtn.appendChild(todoEdit)
        todoBtn.appendChild(todoDel)
    })
    
}

function init() {
    todoText.addEventListener('keypress',function(e){
        if (e.key === 'Enter') {
            addTodo(e.target.value);
            todoText.value ='';
        }
    })
}

init()

이게 문제의 코드. 근데 이렇게 했더니 추가가 이상하게 되는겨. 그래서 봤지.

<li class="todo-item">
<div class="checkbox"></div>
<div class="todo">삼시세끼 버터먹는 세토 혼내주기</div>
<div class="todo-button-group">
	<button class="modify"><i class="fa-solid fa-pen"></i></button>
	<button class="delete"><i class="fa-solid fa-trash-can"></i></button>
</div>
</li>

HTML 구조가 이렇게 되어 있는데, appendChild를 죄다 리스트에 줘버렸기 때문… 그러니 CSS도 적용이 안되고 개판 5분전인 리스트가 탄생한 것이다. (li가 아이템이고 ul이 리스트)

todoList.appendChild(todoItem)
todoItem.appendChild(todoCheck)
todoItem.appendChild(todoCont)
todoItem.appendChild(todoBtn)
todoBtn.appendChild(todoEdit)
todoBtn.appendChild(todoDel)

그래서 각각 구조에 맞게 appendChild를 다시 설정하니까 됐다. (박수)

Update/Delete

업데이트는 두개다. 하나는 상태를 수정하는것(다 했는지 아닌지)이고 하나는 내용을 수정하는 것. 근데 업데이트 어렵다면서요? 네, 내용 수정이요. 그럼 이번에도 짤로 한번 보자. 참고로 이번에 삭제와 함께 추가하는 업데이트는 상태 업데이트다. 

function deleteTodo(todoId) {
    const newTodo = getTodo().filter(todo => todo.id !== todoId )
    setTodo(newTodo)
    displayTodo()
}

function completeTodo(todoId) {
    const newTodo = getTodo().map(todo => todo.id === todoId ? {...todo,  isCompleted: !todo.isCompleted} : todo )
    setTodo(newTodo)
    displayTodo()
}

각 버튼에 이벤트 리스너를 추가할건데, 그 전에 이렇게 함수를 세팅하면 된다. 위에서 []로 선언했는데, 이게 뭔 소리냐면 할 일이 ‘배열’이라는 얘기다. 그래서 삭제는 삭제할 id를 제외한 다른 요소들을 보여주는 것. …근데 completed는 모르것음.

todoCheck.addEventListener('click',() => completeTodo(todo.id))
todoDel.addEventListener('click', () =>  deleteTodo(todo.id))

아무튼 각 버튼에 이벤트 리스너를 추가했다. click은 이 이벤트가 ‘클릭하면’ 실행되게 하는것이라고 보면 된다. Keypress는 키 입력. (추가가 Keypress이다)

function init() {
    todoText.addEventListener('keypress',function(e){
        if (e.key === 'Enter') {
            addTodo(e.target.value);
            todoText.value ='';
        }
    })
}

이게 추가 코드. 잘 보면 keypress가 있고 밑에 e.key === ‘Enter’가 있는데, 키(엔터키)를 누르면 이 이벤트를 실행해줘라 이런 얘기.

Update(내용)

일단 이것도 짤로 보자.

일단 내용 수정을 위해서는 두 가지가 있어야 하는데 

  1. 버튼을 누르면 수정할 내용 입력을 위한 input창을 호출하고(다른 부분을 누르면 닫고)
  2. 수정한 내용을 반영한다

이렇게 두 개이다. 

function modifyTodo(text, todoId) {
    const currentTodo = getTodo()
    const newTodo = currentTodo.map(todo => todo.id === todoId ? ({...todo, content: text}) : todo)
    setTodo(newTodo)
    displayTodo()
}

그럼 함수 소환해주시고…

function summonTodo(e, todoId) {
    const todoElem = e.target
    const inputText = e.target.innerText
    const todoItemElem = document.querySelector('.modify')
    const todoEdit = document.createElement('input')

    todoEdit.value = inputText
    todoEdit.classList.add('todo-text-edit')
    todoEdit.addEventListener('keypress',function(e){
        if (e.key === 'Enter') {
            modifyTodo(e.target.value, todoId)
            document.body.removeEventListener('click', onClickBody)
        }
    })

    const onClickBody = (e) => {
        if (e.target !== todoEdit) {
            todoItemElem.removeChild(todoEdit)
            document.body.removeEventListener('click', onClickBody)
        }
    }

    todoItemElem.appendChild(todoEdit)
    document.body.removeEventListener('click', onClickBody)
}

위쪽 if문은 엔터키가 눌렸을 때 수정한 내용을 반영하라는 얘기이고, 아래쪽에 const는 입력창이 아닌 다른 곳을 클릭하면 닫으라는 얘기다. todoEdit에 createElement를 주고 appendChild를 쓰면 원하는 곳에 생성할 수도 있다. 

todoEdit.addEventListener('dblclick',(event) => summonTodo(event,todo.id))

클릭하면 자꾸 1+1로 젠돼서 더블클릭 해놨음… 


전체 코드-HTML

<html>

<head>
    <title>To-Do List</title>
    <link href="style.css" rel="stylesheet">
    <script src="https://kit.fontawesome.com/dc58858c96.js" crossorigin="anonymous"></script>
</head>

<body>
    <div class="todo-wrapper">
        <div class="todo-title">
            To-Do List
        </div>
        <div class="todo-body">
            <div class="todo-input">
                <input type="text" class="todo-text" placeholder="할 일을 입력하고 엔터키를 빡!!!(너무 세게 치면 컴퓨터 고장나요)">
                <button class="todo-add"><i class="fa-solid fa-plus"></i></button>
            </div>
            <div class="todo-post">
                <ul class="todo-list">
                    <!--<li class="todo-item checked">
                        <div class="checkbox"><i class="fa-solid fa-check"></i></div>
                        <div class="todo">니나브랑 발탄 찜갈비 먹기</div>
                        <div class="todo-button-group">
                            <button class="modify"><i class="fa-solid fa-pen"></i></button>
                            <button class="delete"><i class="fa-solid fa-trash-can"></i></button>
                        </div>
                    </li>
                    <li class="todo-item">
                        <div class="checkbox"></div>
                        <div class="todo">삼시세끼 버터먹는 세토 혼내주기</div>
                        <div class="todo-button-group">
                            <button class="modify"><i class="fa-solid fa-pen"></i></button>
                            <button class="delete"><i class="fa-solid fa-trash-can"></i></button>
                        </div>
                    </li>
                    <li class="todo-item">
                        <div class="checkbox"></div>
                        <div class="todo">알비온 밥주기</div>
                        <div class="todo-button-group">
                            <button class="modify"><i class="fa-solid fa-pen"></i></button>
                            <button class="delete"><i class="fa-solid fa-trash-can"></i></button>
                        </div>
                    </li>-->
                </ul>
            </div>
        </div>
        <p class="info">아, 근데 저장은 안됩니다. </p>
    </div>
    <script src="script.js"></script>
</body>

</html>

전체 코드-CSS

@font-face {
    font-family: 'DalseoHealingBold';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2207-01@1.0/DalseoHealingBold.woff2') format('woff2');
    font-weight: 700;
    font-style: normal;
}

* {
    margin: 0;
    padding: 0;
    font-family: 'DalseoHealingBold';
    font-size: 15pt;
    color: #2b2d42;
}

html {
    height: 100%;
}

body {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    background-color: #fcfcfc;
    min-height: 100%;
}

li {
    list-style-type: none;
}

button {
    background-color:transparent;
    border: 0;
}

.todo-wrapper {
    justify-content: center;
    margin-top: 3rem;
    min-width: 600px;
}

.todo-title {
    font-size:3em;
    text-align:center;
    padding:1em;
}

.todo-body {
    background-color: #fefefe;
}

.todo-input {
    display: flex;
    flex-wrap: nowrap;
    flex-direction: row;
    height: 3em;
    border: 1px solid #2b2d42;
    justify-content: center;
    align-items: center;
}

.todo-text {
    width: 80%;
    text-align: center;
    border: 0;
    outline: none;
    font-size: 1.3em;
    border-bottom:2px solid #E2DCC8;
    margin-right:20px;
}

.todo-text-edit {
    text-align: center;
    border: 0;
    outline: none;
    font-size: 1.1em;
    border-bottom:2px solid #E2DCC8;
    max-width:280px;
}

.todo-add {
    width: 1.3em;
    height: 1.3em;
    border-radius: 50px;
    cursor: pointer;
    font-size: 1.3em;
    border:2px solid #E2DCC8;
}

.todo-list {
    display:grid;
    grid-template-columns: repeat(3, 1fr);
    margin-top:1em;
    width:960px;
    gap: 10px;
}

.todo-item {
    background-color:#a9def9;
    padding:5px;
    box-shadow: 3px 3px 5px gray;
    border-radius:5px;
}

.todo-item:nth-child(2n) {
    background-color:#fcf6bd;
}

.checkbox {
    width: 1.5rem;
    height: 1.5rem;
    margin: 0.5rem 0.5rem;
    border-radius: 50px;
    border: 1px solid #2b2d42;
    cursor: pointer;
    text-align: center;
}

.checkbox > i {
    margin-top: 0.2em;
}

.todo-item .todo {
    padding:10px;
    font-size:1.1em;
}

.todo-item.checked .todo {
    font-style: italic;
    text-decoration: line-through;
    color:#354f52;
}

.todo-button-group {
    display:flex;
    flex-direction: row-reverse;
    flex-wrap: nowrap;
}

.modify, .delete {
    width: 1.3em;
    height: 1.3em;
    font-size:1.1em;
}

.info {
    text-align:center;
    margin-top:1.5em;
}

전체 코드-JS

const todoText = document.querySelector('.todo-text')
const todoAdd = document.querySelector('.todo-add')
const todoDelete = document.querySelector('.delete')
const todoEdit = document.querySelector('.modify')
const todoCheck = document.querySelector('.checkbox')
const todoList = document.querySelector('.todo-list')

let todo = []
let id = 0;
//변수!! 

function setTodo(newTodo) {
    todo = newTodo
}

function getTodo() {
    return todo
}

function addTodo(text) {
    const newId = id++
    const newTodo = getTodo().concat({id: newId, isCompleted: false, content: text })
    setTodo(newTodo)
    displayTodo()
}

function deleteTodo(todoId) {
    const newTodo = getTodo().filter(todo => todo.id !== todoId )
    setTodo(newTodo)
    displayTodo()
}

function completeTodo(todoId) {
    const newTodo = getTodo().map(todo => todo.id === todoId ? {...todo,  isCompleted: !todo.isCompleted} : todo )
    setTodo(newTodo)
    displayTodo()
}

function modifyTodo(text, todoId) {
    const currentTodo = getTodo()
    const newTodo = currentTodo.map(todo => todo.id === todoId ? ({...todo, content: text}) : todo)
    setTodo(newTodo)
    displayTodo()
}

function summonTodo(e, todoId) {
    const todoElem = e.target
    const inputText = e.target.innerText
    const todoItemElem = document.querySelectorAll('.todo')
    const todoEdit = document.createElement('input')

    todoEdit.value = inputText
    todoEdit.classList.add('todo-text-edit')
    todoEdit.addEventListener('keypress',function(e){
        if (e.key === 'Enter') {
            modifyTodo(e.target.value, todoId)
            document.body.removeEventListener('click', onClickBody)
        }
    })

    const onClickBody = (e) => {
        if (e.target !== todoEdit) {
            todoItemElem.removeChild(todoEdit)
            document.body.removeEventListener('click', onClickBody)
        }
    }

    todoItemElem[todoId].appendChild(todoEdit)
    document.body.removeEventListener('dblclick', onClickBody)
}

function displayTodo() {
    todoList.innerHTML = null;
        const allTodo = getTodo()

    allTodo.forEach(function(todo){
        const todoItem = document.createElement('li')
        const todoCheck = document.createElement('div')
        const todoCont = document.createElement('div')
        const todoBtn = document.createElement('div')
        const todoEdit = document.createElement('button')
        const todoDel = document.createElement('button')

        todoItem.setAttribute("data-id","todo.id")
        todoCheck.addEventListener('click',() => completeTodo(todo.id))
        todoDel.addEventListener('click', () => deleteTodo(todo.id))
        todoEdit.addEventListener('dblclick',(event) => summonTodo(event,todo.id))

        todoItem.classList.add('todo-item')
        todoCheck.classList.add('checkbox')
        todoCont.classList.add('todo')
        todoBtn.classList.add('todo-button-group')
        todoEdit.classList.add('modify')
        todoDel.classList.add('delete')
        todoCont.innerText = todo.content
        todoEdit.innerHTML = '<i class="fa-solid fa-pen"></i>'
        todoDel.innerHTML = '<i class="fa-solid fa-trash-can"></i>'

        if(todo.isCompleted) {
            todoItem.classList.add('checked');
            todoCheck.innerHTML = '<i class="fa-solid fa-check"></i>'
        }

        todoList.appendChild(todoItem)
        todoItem.appendChild(todoCheck)
        todoItem.appendChild(todoCont)
        todoItem.appendChild(todoBtn)
        todoBtn.appendChild(todoEdit)
        todoBtn.appendChild(todoDel)
    })
    
}

function init() {
    todoText.addEventListener('keypress',function(e){
        if (e.key === 'Enter') {
            addTodo(e.target.value);
            todoText.value ='';
        }
    })
}

init()