레슨 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)를 사용하세요.