본문 바로가기
JS

JS 4일차

by teg0 2025. 9. 23.

ToDo List

지금까지 배운 HTML, CSS, JS를 이용하여 todo list를 만들어보는 시간을 가졌다.

 

HMTL

길지만 하나씩 설명을 하자면,

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo List App</title>

    <!-- 외부라이브러리 -->
    <!-- google web font -->
    <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=Noto+Sans+KR:wght@100..900&display=swap" rel="stylesheet">

    <!-- 내부import -->
    <link rel="stylesheet" href="./style.css">
</head>
<body>
    <div class="container">
        <!-- 헤더영역 -->
        <header>   
            <h1>Todo List</h1>
        </header>

        <div class="todo-input">
            <input id="todo-input" type="text" placeholder="새로운 할일을 입력하세요.">
            <button id="todo-add-btn">추가</button>
        </div>

        <div class="filter-buttons">
            <button data-filter="all" class="active">전체</button>
            <button data-filter="active">진행중</button>
            <button data-filter="completed">완료</button>
        </div>

        <ul id="todo-list" class="todo-list"></ul>

        <div class="todo-footer">
            <span id="todo-count">1개 남음</span>
            <button class="delete-btn" id="clear-completed-btn">완료된 항목 삭제</button>
        </div>
    </div>

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

외부 라이브러리 인 구글 폰트와 내부 라이브러리 css를 import.

헤더 영역에는 간단한 h1 제목.

input을 받아 추가 버튼을 누르면, ul안에 js로 만들어 추가하는 방식으로, span(할 일), button(삭제)이 추가된다.

3개 버튼인 전체, 진행 중, 완료를 각각 누르면 전체 리스트 출력, 진행 중인 리스트만 출력, 완료된 리스트만 출력된다.

마지막으로 몇 개 남았는지 확인할 수 있는 span 태그와 완료된 항목 전체 삭제가 있다.

 

CSS

css는 많지만, 그러데이션, 트랜지션을 사용하여 애니메이션을 추가하거나 화려하게 만들었다.

 

JS

js도 길지만, js을 배우기에 모두 올리고 해석했다.

//====== 전역 변수 ========
//할일 목록을 저장하는 배열 - 여러 함수에서 공유해야 하기 떄문에 전역 선언
let todos = JSON.parse(localStorage.getItem('todos')) || [];
let filterState = 'all';

// ====== DOM 요소 ========
const todoList = document.getElementById('todo-list'); //할일목록 ul
const clearCompletedBtn = document.getElementById('clear-completed-btn'); //완료목록삭제버튼
const todoInput = document.getElementById('todo-input'); //todo입력창
const filterBtns = document.querySelectorAll('.filter-buttons button'); //필터 버튼 목록

 

todos 전역 변수를 사용하여 모든 함수에 사용하고 공유할 수 있도록 선언한다.

브라우저에 있는 localStorage에 json을 이용하여, 배열을 문자열로 변환하고 저장한다.

DOM 요소, 함수에서 많이 사용하는 id나 class이기에 전역 변수로 선언한다.

더보기

localStorage

브라우저에 key-value형태로 데이터를 저장할 수 있는 공간.
저장된 데이터는 브라우저를 껐다 켜도 유지가 되며, 도메인별로 저장이 된다.
최대 저장용량은 5MB(브라우저별로 다를 수 있음)

localStorage.setItem(key, value); - 데이터를 저장
localStorage.getItem(key); - 데이터를 불러올 때
localStorage.removeItem(key); - 데이터를 삭제할 때
localStorage.clear(); - 모든 데이터 삭제
문자열만 저장하고 가져올 수 있다.

JSON.stringify(js객체) -> JSON 문자열로 변환
JSON.parse(문자열) -> JS객체로 복원

 

// ===== 초기화 함수 ========
//웹이 시작될 때 실행되는 기본함수
//이벤트 등록과 화면 렌더링을 담당
function init() {
    bindEvents();
    render();
}

초기화 함수 init()은 웹이 시작할 때 실행되며, 렌더링 효과를 주어 삭제, 수정, 추가 등이 완료되고 새롭게 홈페이지를 그려, 오류가 없도록 해주는 기능이다.

 

function bindEvents() {
    const addBtn = document.getElementById('todo-add-btn');
    addBtn.addEventListener('click', addTodo);

    // todoInput 요소에서 키보드 입력 이벤트를 감지
    // e: 발생한 keydown 이벤트 객체
    // e.key가 'Enter'일 경우, 즉 사용자가 엔터 키를 눌렀을 때 addTodo 함수를 호출
    todoInput.addEventListener('keydown', function(e){
        if(e.key === 'Enter'){
            addTodo();
        }
    })

    clearCompletedBtn.addEventListener('click', clearCompletedTodos);

    //필터 버튼들을 가져와서 이벤트를 등록
    // 각 필터 버튼에 클릭 이벤트 리스너를 추가
    // ev: 발생한 click 이벤트 객체
    // ev.target은 클릭된 실제 버튼 요소를 가리킴
    // 클릭된 버튼의 data-filter 속성 값을 setFilter 함수에 전달하여 필터 상태를 변경
    filterBtns.forEach(function(btn){
        btn.addEventListener('click', function(ev){
            setFilter(ev.target.dataset.filter);
        })
    })
}

bindEvents() 이벤트를 부여하여 이벤트 핸들러들을 호출하는 함수이다.

e와 ev는 이벤트명을 가리키는 이벤트 객체이다.

e.keycode도 있지만, 현재 key를 더 많이 사용한다. 

forEach를 사용하여 filterBtns의 개수만큼 반복한다.

반복하면서 click 이벤트 객체의 실제 버튼요소를 가리킨다.

setFilter 함수에게 전달한다.

 

function clearCompletedTodos(){
    let newTodos = [];

    for(let todo of todos){
        if(!todo.completed) {
            newTodos.push(todo); //완료되지 않은 목록만 추가
        }
    }

    todos = newTodos;
    saveTodos();
    render(); //화면 업데이트
}

clearCompletedTodos()는 완료되지 않는 목록만 추가하는 함수이다. 

 

//새로운 할일을 추가하는 함수
function addTodo() {
    const text = todoInput.value.trim();
    if (!text) return; //빈문자열이면 함수 종료

    const todo = {
        id: Date.now(), //현재시간을 ms단위로 변환 -> 고유한 ID로 사용
        content: text,
        completed: false,
        createdAt: new Date().toLocaleString(), //생성시간
    }

    todos.push(todo); //새로운 할일을 목록에 추가
    todoInput.value = "";

    saveTodos();
    render(); //할일목록을 기준으로 UI에 적용
}

bindEvent() 함수에서 보인 addTodo() 함수는 사용자가 입력창에 적은 할 일들을 todo 배열로 변환한다.

전역 변수인 todos에 todo를 저장한다.

 

function deleteTodo(id){
    //해당 ID를 목록에서 제거.
    let newTodo = [];
    for(let todo of todos){
        if(todo.id === id)
            continue;

        newTodo.push(todo);
    }

    todos = newTodo;
    saveTodos();
    render(); //할일목록을 기준으로 UI에 적용
}

function toggleTodo(id){
    //해당 ID를 통해서 할일을 찾아 완료상태 -> 미완료, 미완료 -> 완료 변경.
    for(let todo of todos){
        if(todo.id === id) {
            todo.completed = !todo.completed;
            break;
        }
    }

    saveTodos();
    render();
}

deleteTodo() 함수는 id를 매개변수로 받아, 제거한다.

toggleTodo() 함수는 id를 매개변수로 받아, id의 completed를 반대로 바꾼다.

 

function getFilteredTodos(){
    const filteredTodos = [];
    if(filterState === 'active'){
        //미완료목록만 filteredTodos에 담김
        for(let todo of todos){
            if(!todo.completed){
                filteredTodos.push(todo);
            }
        }
    } else if(filterState === 'completed'){
        //완료목록만 filteredTodos에 담김
        for(let todo of todos){
            if(todo.completed){
                filteredTodos.push(todo);
            }
        }
    } else{
        return todos;
    }

    return filteredTodos;
}

getFilteredTodos() 함수는 새로운 배열을 만들고 전역 변수인 filterState가 active, completed, all을 if-else문으로 구분하여 각각 처리한다.

 

//할일목록을 로컬스토리지영역에 저장하는 함수
function saveTodos(){
    localStorage.setItem('todos', JSON.stringify(todos));
}

saveTodos() 함수는 localStorage 영역에 json으로 저장한다.

 

//메인 렌더링 함수
function render() {
    todoList.innerHTML = ""; //기존 UI 제거

    //현재 필터에 맞는 할일만 목록으로 가져오기
    const filteredTodos = getFilteredTodos();

    if (filteredTodos.length === 0) { //할일목록이 비어있다면
        emptyStateRender();
    } else { //할일 목록이 있는 경우
        filteredTodos.forEach(function (todo) {
            todoItemRender(todo);
        })
    }

    updateCount();
    updateClearButton();
}

render()는 리스트의 데이터의 값이 변하면, 다시 초기화시키고 필요한 목록들을 가져온다.

 

function emptyStateRender(){
    const emptyEl = document.createElement('div');
    emptyEl.className = 'empty-state';
    emptyEl.innerHTML = '할 일이 없습니다.'
    todoList.appendChild(emptyEl);
}

emptyStateRender() 함수는 할 일들이 없을 경우 리스트에 나타나게 한다.

 

function todoItemRender(todo) {
    const todoItem = document.createElement('li');
    todoItem.className = 'todo-item' + (todo.completed ? ' completed' : '');

    todoItem.innerHTML = `<div class="todo-checkbox ${todo.completed ? 'checked' : ''}"></div>
                            <span>${todo.content}</span>
                            <button class="delete-btn">삭제</button>`;

    //새로 생성된 요소들 중에서 이벤트가 필요한 부분만 가져오기.
    const checkBox = todoItem.querySelector('.todo-checkbox'); //todoItem내부에 있는 checkbox요소
    checkBox.addEventListener('click', function(){
        toggleTodo(todo.id);
    })

    const deleteBtn = todoItem.querySelector('.delete-btn'); //todoItem내부에 있는 deleteBtn요소
    deleteBtn.addEventListener('click', function(){
        deleteTodo(todo.id);
    })
    todoList.appendChild(todoItem);
}

todoItemRender() 함수는 todo를 매개변수로 받으며 li를 만들고 class에 참 거짓에 따른 값을 추가한다.

innerHTML으로 사용자가 입력한 텍스트와 삭제 버튼을 생성한다.

checkBox와 deleteBtn을 클래스로 받아 각 함수에게 전달하고 appendChild로 todoList의 자식에게 추가시킨다.

 

//남은 할일의 갯수를 구해서 화면을 업데이트.
function updateCount(){
    const todoCount = document.getElementById('todo-count');
    let count = 0;
    for(let todo of todos){
        if(!todo.completed) count++;
    }

    todoCount.innerHTML = `${count}개 남음`;
}

updateCount() 함수는 남은 개수를 출력하며 반복문으로 completed가 true인 checkBox의 개수를 세며, count를 1씩 증가시킨다.

 

function updateClearButton(){
    let isView = 'none';
    for(let todo of todos){
        if(todo.completed) {
            isView = 'block';
            break;
        }
    }

    //완료된 목록이 있다면 버튼 표시, 없으면 숨김
    clearCompletedBtn.style.display = isView;
}

updateClearButton() 함수는 모두 완료하기 버튼 기능 중, 리스트에 완료된 목록이 없으면 숨기고, 있으면 표시된다.

 

//필터를 설정하고 UI를 업데이트
function setFilter(filter){
    filterState = filter; //전역상태에 필터상태를 변경

    //모든 필터버튼의 active클래스를 조회해서 수정
    filterBtns.forEach(function(btn){
        btn.className = (btn.dataset.filter === filter ? "active" : "");
    });

    render();
}

setFilter() 함수는 전역 변수인 filterState는 HTML에서 data-filter으로 설정한 버튼들의 값을 받고, 만약 참일 경우 active를 거짓일 경우 공백을 data-filter에 추가한다.

 

//DOMContentLoaded -> HTML이 전부 로드되어 DOM트리가 완성되면 실행
document.addEventListener('DOMContentLoaded', init);

// window.onload = function(){
//     init();
// }

마지막으로 DOMContentLoaded 이벤트 명은 HTML이 모두 로드가 되어 DOM 스티가 완성되면 실행하라는 뜻으로, init을 실행시킨다.

window.onload함수는 모두 로드가 되었을 경우이며 둘 다 같다.

'JS' 카테고리의 다른 글

JS 3일차(2)  (0) 2025.09.22
JS 3일차(1)  (1) 2025.09.22
JS 2일차(2)  (2) 2025.09.19
JS 2일차(1)  (0) 2025.09.19
JS 1일차(2)  (0) 2025.09.17