지금까지 배운 자바스크립트 기초를 총동원해 투두리스트 앱을 처음부터 완성한다. DOM 조작·이벤트·배열·모듈·로컬스토리지를 통합하는 실전 경험을 쌓는다.
종합 실습 — 투두리스트 완성하기
이번 강의는 1~19강에서 배운 모든 개념을 통합하는 미니 프로젝트입니다. 기능 추가, 삭제, 완료 체크, 필터, 로컬스토리지 저장까지 실무에서 쓰는 패턴을 직접 구현합니다.
완성 목표 기능
구현할 기능:
✅ 투두 항목 추가 (Enter 키 또는 버튼)
✅ 항목 삭제
✅ 완료/미완료 토글
✅ 필터 (전체/미완료/완료)
✅ 완료된 항목 일괄 삭제
✅ 로컬스토리지 저장 (새로고침 유지)
✅ 항목 개수 표시
HTML 구조
<div id="app">
<h1>Todo List</h1>
<div class="input-area">
<input id="todo-input" type="text" placeholder="할 일을 입력하세요" />
<button id="add-btn">추가</button>
</div>
<div class="filters">
<button class="filter-btn active" data-filter="all">전체</button>
<button class="filter-btn" data-filter="active">미완료</button>
<button class="filter-btn" data-filter="completed">완료</button>
</div>
<ul id="todo-list"></ul>
<div class="footer">
<span id="count"></span>
<button id="clear-btn">완료 항목 삭제</button>
</div>
</div>
<script type="module" src="./app.js"></script>
상태(State) 설계 — store.js
const STORAGE_KEY = "todos";
function loadFromStorage() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
}
function saveToStorage(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}
export let todos = loadFromStorage();
export function addTodo(text) {
const newTodo = {
id: Date.now(),
text: text.trim(),
completed: false,
createdAt: new Date().toISOString()
};
todos = [...todos, newTodo];
saveToStorage(todos);
return newTodo;
}
export function toggleTodo(id) {
todos = todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
saveToStorage(todos);
}
export function deleteTodo(id) {
todos = todos.filter(todo => todo.id !== id);
saveToStorage(todos);
}
export function clearCompleted() {
todos = todos.filter(todo => !todo.completed);
saveToStorage(todos);
}
export function filterTodos(filter) {
if (filter === "active") return todos.filter(t => !t.completed);
if (filter === "completed") return todos.filter(t => t.completed);
return todos;
}
렌더링 함수 — render.js
import { todos, filterTodos } from "./store.js";
let currentFilter = "all";
export function setFilter(filter) { currentFilter = filter; }
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
export function render() {
const list = document.querySelector("#todo-list");
const countEl = document.querySelector("#count");
const filtered = filterTodos(currentFilter);
const activeCount = todos.filter(t => !t.completed).length;
countEl.textContent = `${activeCount}개 남음`;
if (filtered.length === 0) {
list.innerHTML = "<li class=\"empty\">항목이 없습니다</li>";
return;
}
list.innerHTML = filtered.map(todo => `
<li class="todo-item ${todo.completed ? "completed" : ""}" data-id="${todo.id}">
<input type="checkbox" class="toggle" ${todo.completed ? "checked" : ""} />
<span class="todo-text">${escapeHtml(todo.text)}</span>
<button class="delete-btn" data-id="${todo.id}">삭제</button>
</li>
`).join("");
}
이벤트 처리 — app.js
import { addTodo, toggleTodo, deleteTodo, clearCompleted } from "./store.js";
import { render, setFilter } from "./render.js";
render(); // 초기 렌더링
const input = document.querySelector("#todo-input");
function handleAdd() {
const text = input.value.trim();
if (!text) return;
addTodo(text);
input.value = "";
render();
input.focus();
}
document.querySelector("#add-btn").addEventListener("click", handleAdd);
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") handleAdd();
});
// 이벤트 위임 — 토글 및 삭제
document.querySelector("#todo-list").addEventListener("click", (e) => {
const el = e.target.closest("[data-id]");
if (!el) return;
const id = Number(el.dataset.id);
if (e.target.classList.contains("toggle")) {
toggleTodo(id); render();
}
if (e.target.classList.contains("delete-btn")) {
deleteTodo(id); render();
}
});
// 필터 버튼
document.querySelector(".filters").addEventListener("click", (e) => {
const btn = e.target.closest(".filter-btn");
if (!btn) return;
document.querySelectorAll(".filter-btn").forEach(b =>
b.classList.toggle("active", b === btn)
);
setFilter(btn.dataset.filter);
render();
});
document.querySelector("#clear-btn").addEventListener("click", () => {
clearCompleted(); render();
});
이 코드에서 사용한 개념들
| 강의 | 적용된 개념 |
|---|---|
| 2강 | let/const, 불변 상태 패턴 |
| 4강 | 옵셔널 체이닝, 논리 연산자 |
| 7·8강 | 함수 분리, 클로저(currentFilter 유지) |
| 9·10강 | filter, map, 스프레드로 불변 배열 업데이트 |
| 11·12강 | 객체 생성, 스프레드로 불변 업데이트 |
| 13강 | trim(), 템플릿 리터럴, HTML 이스케이프 |
| 14강 | querySelector, innerHTML, dataset |
| 15강 | 이벤트 위임, closest, keydown 처리 |
| 19강 | import/export 모듈 분리 |
더 발전시켜보기
추가 도전 과제:
1. 항목 수정 기능 (더블클릭 시 인라인 편집)
2. 드래그 앤 드롭 순서 변경
3. 마감일 설정 및 D-Day 표시
4. 서버 API 연동 (fetch + async/await)
5. TypeScript 마이그레이션
이 수준이 되면 준비된 것들:
- React/Vue: 컴포넌트 = 함수 + 상태 + 렌더링 (이미 알고 있음)
- Node.js 서버: async/await + 모듈 이미 앎
- TypeScript: 기존 JS에 타입만 추가
자바스크립트 기초 20강을 완주하셨습니다. 이 강좌에서 배운 것들은 어떤 프레임워크를 써도 변하지 않는 핵심 지식입니다. React든 Vue든 Node.js든, 내부는 결국 이 강좌에서 배운 자바스크립트가 동작합니다.
관련 검색어 · 자바스크립트 투두리스트, 바닐라 JS 프로젝트, 로컬스토리지 사용법, 이벤트 위임 실습, DOM 조작 실전, 자바스크립트 포트폴리오