第20講

종합 실습: 미니 프로젝트

4 閲覧 1

지금까지 배운 자바스크립트 기초를 총동원해 투두리스트 앱을 처음부터 완성한다. 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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

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 조작 실전, 자바스크립트 포트폴리오

채용정보
공지사항
인사이트
MY