Learning
레슨 8 / 9·25분

실전: Todo 앱 만들기

프로젝트 개요

지금까지 배운 JavaScript 핵심 개념을 활용해 Todo 앱을 만듭니다. DOM 조작, 이벤트 처리, 배열 메서드, localStorage를 모두 사용합니다.

  • 할 일 추가, 완료 토글, 삭제 기능
  • 필터링 (전체 / 진행중 / 완료)
  • localStorage로 데이터 영속화
  • 이벤트 위임으로 효율적인 이벤트 처리

HTML 구조

html
<div id="app">
  <h1>Todo App</h1>
  <form id="todo-form">
    <input id="todo-input" type="text"
           placeholder="할 일을 입력하세요" required />
    <button type="submit">추가</button>
  </form>
  <div id="filters">
    <button class="filter active" data-filter="all">전체</button>
    <button class="filter" data-filter="active">진행중</button>
    <button class="filter" data-filter="completed">완료</button>
  </div>
  <ul id="todo-list"></ul>
  <p id="count"></p>
</div>

데이터 관리

javascript
// 상태 관리
let todos = JSON.parse(localStorage.getItem("todos")) || [];
let currentFilter = "all";

// 저장
function save() {
  localStorage.setItem("todos", JSON.stringify(todos));
}

// 할 일 추가
function addTodo(text) {
  todos.push({
    id: Date.now(),
    text,
    completed: false,
  });
  save();
  render();
}

// 토글
function toggleTodo(id) {
  const todo = todos.find(t => t.id === id);
  if (todo) todo.completed = !todo.completed;
  save();
  render();
}

// 삭제
function deleteTodo(id) {
  todos = todos.filter(t => t.id !== id);
  save();
  render();
}

렌더링과 이벤트

javascript
function getFiltered() {
  if (currentFilter === "active") {
    return todos.filter(t => !t.completed);
  }
  if (currentFilter === "completed") {
    return todos.filter(t => t.completed);
  }
  return todos;
}

function render() {
  const list = document.querySelector("#todo-list");
  const filtered = getFiltered();

  list.innerHTML = filtered.map(todo =>
    '<li data-id="' + todo.id + '"' +
    ' class="' + (todo.completed ? "completed" : "") + '">' +
    '<input type="checkbox"' +
    (todo.completed ? " checked" : "") + ' />' +
    '<span>' + todo.text + '</span>' +
    '<button class="delete-btn">삭제</button>' +
    '</li>'
  ).join("");

  const remaining = todos.filter(t => !t.completed).length;
  document.querySelector("#count").textContent =
    "남은 할 일: " + remaining + "개";
}

// 폼 제출
document.querySelector("#todo-form")
  .addEventListener("submit", (e) => {
    e.preventDefault();
    const input = document.querySelector("#todo-input");
    if (input.value.trim()) {
      addTodo(input.value.trim());
      input.value = "";
    }
  });

// 이벤트 위임 — 리스트
document.querySelector("#todo-list")
  .addEventListener("click", (e) => {
    const li = e.target.closest("li");
    if (!li) return;
    const id = Number(li.dataset.id);

    if (e.target.matches(".delete-btn")) {
      deleteTodo(id);
    } else if (e.target.matches("input[type=checkbox]")) {
      toggleTodo(id);
    }
  });

// 필터 버튼
document.querySelector("#filters")
  .addEventListener("click", (e) => {
    if (!e.target.matches(".filter")) return;
    currentFilter = e.target.dataset.filter;
    document.querySelectorAll(".filter")
      .forEach(b => b.classList.remove("active"));
    e.target.classList.add("active");
    render();
  });

// 초기 렌더링
render();
💡

실제 프로젝트에서는 innerHTML을 사용자 입력과 함께 사용하면 XSS 취약점이 생깁니다. 이 예제는 학습용이며, 실무에서는 createElement + textContent 조합이나 프레임워크(React, Vue)를 사용하세요.