반응형
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 실력 향상의 완벽한 시작점입니다.
'JavaScript > 이벤트 처리' 카테고리의 다른 글
JavaScript 이벤트 객체와 이벤트 위임 - 핵심 가이드 (0) | 2025.06.24 |
---|---|
JavaScript 마우스 이벤트와 키보드 이벤트 - 핵심 가이드 (0) | 2025.06.24 |
JavaScript 이벤트 처리 - onclick vs addEventListener 완전 가이드 (0) | 2025.06.24 |