JavaScript/이벤트 처리

JavaScript 실습: Todo List 만들기 - 초보자도 30분이면 완성!

코딩하는 패션이과생 2025. 6. 24. 22:57
반응형

Todo List 프로젝트 소개

Todo List(할 일 목록)는 JavaScript 학습에 완벽한 첫 번째 프로젝트입니다. DOM 조작, 이벤트 처리, 배열 다루기, 로컬 저장소 활용 등 실무에서 자주 사용하는 핵심 기능들을 모두 경험할 수 있습니다.

🎯 학습 목표

  • DOM 조작: 요소 생성, 수정, 삭제
  • 이벤트 처리: 클릭, 키보드 입력
  • 배열 메서드: push, filter, map 활용
  • localStorage: 데이터 영구 저장
  • 실무 패턴: 코드 구조화와 함수 분리

필요한 기능 정리

우리가 만들 Todo List의 핵심 기능들:

✅ 기본 기능

  • 할 일 추가하기
  • 할 일 완료 표시하기
  • 할 일 삭제하기
  • 할 일 목록 표시하기

🚀 고급 기능

  • 브라우저 새로고침 후에도 데이터 유지
  • 전체 삭제 기능
  • 완료된 항목 숨기기/보기
  • 남은 할 일 개수 표시

HTML 구조 만들기

먼저 기본 HTML 구조를 만들어봅시다:

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>나만의 Todo List</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>📝 나의 할 일 목록</h1>

        <!-- 입력 폼 -->
        <div class="input-section">
            <input type="text" id="todoInput" placeholder="할 일을 입력하세요...">
            <button id="addBtn">추가</button>
        </div>

        <!-- 필터 버튼들 -->
        <div class="filter-section">
            <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="todoList"></ul>

        <!-- 상태 정보 -->
        <div class="status">
            <span id="todoCount">0개의 할 일</span>
            <button id="clearCompleted">완료된 항목 삭제</button>
        </div>
    </div>

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

CSS 스타일링

Todo List를 예쁘게 꾸며봅시다:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
}

.container {
    max-width: 500px;
    margin: 0 auto;
    background: white;
    border-radius: 15px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
    overflow: hidden;
}

h1 {
    text-align: center;
    color: #333;
    padding: 30px 20px;
    background: #f8f9fa;
    margin: 0;
}

.input-section {
    display: flex;
    padding: 20px;
    gap: 10px;
}

#todoInput {
    flex: 1;
    padding: 12px 15px;
    border: 2px solid #e9ecef;
    border-radius: 8px;
    font-size: 16px;
    outline: none;
    transition: border-color 0.3s;
}

#todoInput:focus {
    border-color: #667eea;
}

#addBtn {
    padding: 12px 20px;
    background: #667eea;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-weight: bold;
    transition: background 0.3s;
}

#addBtn:hover {
    background: #5a6fd8;
}

.filter-section {
    display: flex;
    padding: 0 20px;
    gap: 5px;
}

.filter-btn {
    flex: 1;
    padding: 10px;
    border: none;
    background: #f8f9fa;
    cursor: pointer;
    transition: background 0.3s;
}

.filter-btn.active {
    background: #667eea;
    color: white;
}

#todoList {
    list-style: none;
    padding: 20px;
    min-height: 200px;
}

.todo-item {
    display: flex;
    align-items: center;
    padding: 15px;
    border: 1px solid #e9ecef;
    border-radius: 8px;
    margin-bottom: 10px;
    background: #f8f9fa;
    transition: all 0.3s;
}

.todo-item:hover {
    background: #e9ecef;
}

.todo-item.completed {
    opacity: 0.6;
    background: #d1ecf1;
}

.todo-item.completed .todo-text {
    text-decoration: line-through;
    color: #6c757d;
}

.todo-checkbox {
    margin-right: 10px;
    width: 18px;
    height: 18px;
}

.todo-text {
    flex: 1;
    font-size: 16px;
}

.delete-btn {
    background: #dc3545;
    color: white;
    border: none;
    padding: 5px 10px;
    border-radius: 5px;
    cursor: pointer;
    font-size: 12px;
}

.delete-btn:hover {
    background: #c82333;
}

.status {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px;
    background: #f8f9fa;
    border-top: 1px solid #e9ecef;
}

#clearCompleted {
    background: #6c757d;
    color: white;
    border: none;
    padding: 8px 15px;
    border-radius: 5px;
    cursor: pointer;
    font-size: 14px;
}

#clearCompleted:hover {
    background: #545b62;
}

.hidden {
    display: none;
}

JavaScript 기능 구현

이제 Todo List의 핵심 기능을 JavaScript로 구현해봅시다:

1단계: 기본 변수와 함수 설정

// DOM 요소들
const todoInput = document.getElementById('todoInput');
const addBtn = document.getElementById('addBtn');
const todoList = document.getElementById('todoList');
const todoCount = document.getElementById('todoCount');
const filterBtns = document.querySelectorAll('.filter-btn');
const clearCompleted = document.getElementById('clearCompleted');

// 데이터 저장 배열
let todos = [];
let currentFilter = 'all';

// 초기화 함수
function init() {
    loadFromStorage();
    render();
    bindEvents();
}

// 이벤트 리스너 등록
function bindEvents() {
    addBtn.addEventListener('click', addTodo);
    todoInput.addEventListener('keypress', handleKeyPress);
    clearCompleted.addEventListener('click', clearCompletedTodos);

    filterBtns.forEach(btn => {
        btn.addEventListener('click', handleFilter);
    });
}

2단계: 할 일 추가 기능

// 할 일 추가
function addTodo() {
    const text = todoInput.value.trim();

    if (text === '') {
        alert('할 일을 입력해주세요!');
        return;
    }

    const newTodo = {
        id: Date.now(),
        text: text,
        completed: false,
        createdAt: new Date().toISOString()
    };

    todos.push(newTodo);
    todoInput.value = '';

    saveToStorage();
    render();
}

// Enter 키 처리
function handleKeyPress(e) {
    if (e.key === 'Enter') {
        addTodo();
    }
}

3단계: 할 일 표시 및 조작

// 화면에 표시
function render() {
    const filteredTodos = getFilteredTodos();

    todoList.innerHTML = '';

    filteredTodos.forEach(todo => {
        const li = createTodoElement(todo);
        todoList.appendChild(li);
    });

    updateCount();
}

// Todo 요소 생성
function createTodoElement(todo) {
    const li = document.createElement('li');
    li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
    li.dataset.id = todo.id;

    li.innerHTML = `
        <input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}>
        <span class="todo-text">${todo.text}</span>
        <button class="delete-btn">삭제</button>
    `;

    // 체크박스 이벤트
    const checkbox = li.querySelector('.todo-checkbox');
    checkbox.addEventListener('change', () => toggleTodo(todo.id));

    // 삭제 버튼 이벤트
    const deleteBtn = li.querySelector('.delete-btn');
    deleteBtn.addEventListener('click', () => deleteTodo(todo.id));

    return li;
}

// 완료 상태 토글
function toggleTodo(id) {
    todos = todos.map(todo => 
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );

    saveToStorage();
    render();
}

// 할 일 삭제
function deleteTodo(id) {
    if (confirm('정말 삭제하시겠습니까?')) {
        todos = todos.filter(todo => todo.id !== id);
        saveToStorage();
        render();
    }
}

4단계: 필터링 기능

// 필터링
function getFilteredTodos() {
    switch (currentFilter) {
        case 'active':
            return todos.filter(todo => !todo.completed);
        case 'completed':
            return todos.filter(todo => todo.completed);
        default:
            return todos;
    }
}

// 필터 버튼 처리
function handleFilter(e) {
    currentFilter = e.target.dataset.filter;

    filterBtns.forEach(btn => btn.classList.remove('active'));
    e.target.classList.add('active');

    render();
}

// 개수 업데이트
function updateCount() {
    const activeCount = todos.filter(todo => !todo.completed).length;
    todoCount.textContent = `${activeCount}개의 할 일`;
}

// 완료된 항목 모두 삭제
function clearCompletedTodos() {
    const completedCount = todos.filter(todo => todo.completed).length;

    if (completedCount === 0) {
        alert('완료된 항목이 없습니다.');
        return;
    }

    if (confirm(`완료된 ${completedCount}개 항목을 삭제하시겠습니까?`)) {
        todos = todos.filter(todo => !todo.completed);
        saveToStorage();
        render();
    }
}

localStorage로 데이터 저장

브라우저를 새로고침해도 데이터가 유지되도록 localStorage를 활용합니다:

// 로컬 저장소에 저장
function saveToStorage() {
    localStorage.setItem('todos', JSON.stringify(todos));
}

// 로컬 저장소에서 불러오기
function loadFromStorage() {
    const saved = localStorage.getItem('todos');
    if (saved) {
        todos = JSON.parse(saved);
    }
}

// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', init);

고급 기능 추가

💡 할 일 편집 기능

// 더블클릭으로 편집 모드
function createTodoElement(todo) {
    // ... 기존 코드 ...

    const todoText = li.querySelector('.todo-text');
    todoText.addEventListener('dblclick', () => editTodo(todo.id, todoText));

    return li;
}

function editTodo(id, element) {
    const currentText = element.textContent;
    const input = document.createElement('input');
    input.type = 'text';
    input.value = currentText;
    input.className = 'edit-input';

    element.replaceWith(input);
    input.focus();
    input.select();

    function saveEdit() {
        const newText = input.value.trim();
        if (newText && newText !== currentText) {
            todos = todos.map(todo => 
                todo.id === id ? { ...todo, text: newText } : todo
            );
            saveToStorage();
        }
        render();
    }

    input.addEventListener('blur', saveEdit);
    input.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') saveEdit();
    });
}

📊 통계 기능

function updateStats() {
    const total = todos.length;
    const completed = todos.filter(todo => todo.completed).length;
    const active = total - completed;

    document.getElementById('statsTotal').textContent = total;
    document.getElementById('statsCompleted').textContent = completed;
    document.getElementById('statsActive').textContent = active;
}

완성된 코드

HTML (index.html)

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>나만의 Todo List - JavaScript 실습</title>
    <meta name="description" content="JavaScript로 만든 완전한 Todo List 앱. 로컬 저장소, 필터링, 편집 기능 포함">
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>📝 나의 할 일 목록</h1>

        <div class="input-section">
            <input type="text" id="todoInput" placeholder="할 일을 입력하세요... (Enter로 추가)">
            <button id="addBtn">추가</button>
        </div>

        <div class="filter-section">
            <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="todoList"></ul>

        <div class="status">
            <span id="todoCount">0개의 할 일</span>
            <button id="clearCompleted">완료된 항목 삭제</button>
        </div>

        <div class="tips">
            <small>💡 팁: 할 일을 더블클릭하면 편집할 수 있습니다!</small>
        </div>
    </div>

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

JavaScript (script.js)

// DOM 요소들
const todoInput = document.getElementById('todoInput');
const addBtn = document.getElementById('addBtn');
const todoList = document.getElementById('todoList');
const todoCount = document.getElementById('todoCount');
const filterBtns = document.querySelectorAll('.filter-btn');
const clearCompleted = document.getElementById('clearCompleted');

// 전역 변수
let todos = [];
let currentFilter = 'all';

// 초기화
function init() {
    loadFromStorage();
    render();
    bindEvents();
}

// 이벤트 바인딩
function bindEvents() {
    addBtn.addEventListener('click', addTodo);
    todoInput.addEventListener('keypress', handleKeyPress);
    clearCompleted.addEventListener('click', clearCompletedTodos);

    filterBtns.forEach(btn => {
        btn.addEventListener('click', handleFilter);
    });
}

// 핵심 기능들
function addTodo() {
    const text = todoInput.value.trim();

    if (text === '') {
        todoInput.focus();
        return;
    }

    const newTodo = {
        id: Date.now(),
        text: text,
        completed: false,
        createdAt: new Date().toISOString()
    };

    todos.unshift(newTodo); // 최신 항목을 위로
    todoInput.value = '';

    saveToStorage();
    render();
    todoInput.focus();
}

function handleKeyPress(e) {
    if (e.key === 'Enter') {
        addTodo();
    }
}

function render() {
    const filteredTodos = getFilteredTodos();

    if (filteredTodos.length === 0) {
        todoList.innerHTML = '<li class="empty-state">할 일이 없습니다 🎉</li>';
    } else {
        todoList.innerHTML = '';
        filteredTodos.forEach(todo => {
            const li = createTodoElement(todo);
            todoList.appendChild(li);
        });
    }

    updateCount();
}

function createTodoElement(todo) {
    const li = document.createElement('li');
    li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
    li.dataset.id = todo.id;

    li.innerHTML = `
        <input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}>
        <span class="todo-text">${escapeHtml(todo.text)}</span>
        <button class="delete-btn" title="삭제">🗑️</button>
    `;

    // 이벤트 등록
    const checkbox = li.querySelector('.todo-checkbox');
    checkbox.addEventListener('change', () => toggleTodo(todo.id));

    const deleteBtn = li.querySelector('.delete-btn');
    deleteBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        deleteTodo(todo.id);
    });

    const todoText = li.querySelector('.todo-text');
    todoText.addEventListener('dblclick', () => editTodo(todo.id, todoText));

    return li;
}

function toggleTodo(id) {
    todos = todos.map(todo => 
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );

    saveToStorage();
    render();
}

function deleteTodo(id) {
    todos = todos.filter(todo => todo.id !== id);
    saveToStorage();
    render();
}

function editTodo(id, element) {
    const currentText = element.textContent;
    const input = document.createElement('input');
    input.type = 'text';
    input.value = currentText;
    input.className = 'edit-input';

    element.replaceWith(input);
    input.focus();
    input.select();

    function saveEdit() {
        const newText = input.value.trim();
        if (newText && newText !== currentText) {
            todos = todos.map(todo => 
                todo.id === id ? { ...todo, text: newText } : todo
            );
            saveToStorage();
        }
        render();
    }

    input.addEventListener('blur', saveEdit);
    input.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') saveEdit();
        if (e.key === 'Escape') render();
    });
}

// 필터링 및 유틸리티
function getFilteredTodos() {
    switch (currentFilter) {
        case 'active':
            return todos.filter(todo => !todo.completed);
        case 'completed':
            return todos.filter(todo => todo.completed);
        default:
            return todos;
    }
}

function handleFilter(e) {
    currentFilter = e.target.dataset.filter;

    filterBtns.forEach(btn => btn.classList.remove('active'));
    e.target.classList.add('active');

    render();
}

function updateCount() {
    const activeCount = todos.filter(todo => !todo.completed).length;
    todoCount.textContent = `${activeCount}개의 할 일`;
}

function clearCompletedTodos() {
    const completedTodos = todos.filter(todo => todo.completed);

    if (completedTodos.length === 0) {
        return;
    }

    todos = todos.filter(todo => !todo.completed);
    saveToStorage();
    render();
}

// 저장소 관리
function saveToStorage() {
    try {
        localStorage.setItem('todos', JSON.stringify(todos));
    } catch (error) {
        console.error('저장 실패:', error);
    }
}

function loadFromStorage() {
    try {
        const saved = localStorage.getItem('todos');
        if (saved) {
            todos = JSON.parse(saved);
        }
    } catch (error) {
        console.error('불러오기 실패:', error);
        todos = [];
    }
}

// 보안 함수
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// 앱 시작
document.addEventListener('DOMContentLoaded', init);

마무리

축하합니다! 🎉 완전한 기능을 가진 Todo List를 성공적으로 만들었습니다!
이제 여러분만의 창의적인 기능을 추가해보세요! Todo List는 JavaScript 실력 향상의 완벽한 시작점입니다.