실시간 동기화 가능한 Todo List 앱 만들기
안녕하세요! 오늘은 여러 기기에서 실시간으로 동기화되는 Todo List 앱을 만드는 방법에 대해 알아보겠습니다. 이 앱은 GitHub Pages를 이용해 배포하므로 별도의 서버 설정 없이도 간편하게 사용할 수 있습니다.
주요 기능
- 실시간 동기화
- 여러 기기에서 접근 가능
- 간편한 URL 공유
- 모바일 최적화 UI
- localStorage를 통한 데이터 저장
코드 구현
먼저, HTML 구조와 CSS 스타일을 살펴보겠습니다.
`xml<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Todo List App</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <style> /* 스타일 코드 */ body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .container { background-color: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); padding: 20px; width: 100%; max-width: 500px; } h1 { text-align: center; color: #333; } #todo-form { display: flex; margin-bottom: 20px; } #todo-input { flex: 1; padding: 10px; font-size: 16px; border: 1px solid #ddd; border-radius: 4px 0 0 4px; } #todo-submit { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 0 4px 4px 0; cursor: pointer; } .todo-item { display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #eee; } .todo-checkbox { margin-right: 10px; } .todo-text { flex: 1; padding: 5px; border: 1px solid transparent; border-radius: 3px; min-height: 44px; display: flex; align-items: center; } .todo-text:focus { border-color: #2196F3; outline: none; } .todo-item.completed .todo-text { text-decoration: line-through; color: #888; } .todo-actions { display: flex; gap: 5px; } .edit-btn, .delete-btn { background: none; border: none; cursor: pointer; font-size: 18px; color: #888; } .todo-filters { display: flex; justify-content: center; margin-top: 20px; } .filter-btn { margin: 0 5px; padding: 5px 10px; background-color: #ddd; border: none; border-radius: 3px; cursor: pointer; } .filter-btn.active { background-color: #4CAF50; color: white; } footer { display: flex; justify-content: space-between; align-items: center; margin-top: 20px; color: #888; } #share-url { position: fixed; top: 10px; right: 10px; padding: 10px; background: #2196F3; color: white; border: none; border-radius: 5px; cursor: pointer; } @media (max-width: 768px) { .container { margin: 0; max-width: 100%; min-height: 100vh; border-radius: 0; } } </style> </head> <body> <button id="share-url">URL 공유하기</button> <div class="container"> <h1>Todo List</h1> <form id="todo-form"> <input type="text" id="todo-input" placeholder="할 일을 입력하세요" required> <button type="submit" id="todo-submit">추가</button> </form> <div class="todo-list"></div> <div class="todo-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> <footer> <span>총 <span id="total-count">0</span>개 항목</span> <span>완료: <span id="completed-count">0</span>개</span> <button id="clear-completed">완료 항목 삭제</button> </footer> </div> <script> // JavaScript 코드 // 공유 URL 복사 기능 const shareButton = document.getElementById('share-url'); shareButton.addEventListener('click', async () => { try { await navigator.clipboard.writeText(window.location.href); alert('URL이 복사되었습니다. 다른 기기에서 이 URL을 열어보세요!'); } catch (err) { alert('URL: ' + window.location.href); } });
// 고유한 룸 ID 생성 또는 가져오기
const roomId = window.location.hash.slice(1) || Date.now().toString();
window.location.hash = roomId;
// 할일 목록을 저장할 배열
let todos = JSON.parse(localStorage.getItem(`todos_${roomId}`)) || [];
// DOM 요소들
const todoForm = document.getElementById('todo-form');
const todoInput = document.getElementById('todo-input');
const todoList = document.querySelector('.todo-list');
const filterButtons = document.querySelectorAll('.filter-btn');
const totalCount = document.getElementById('total-count');
const completedCount = document.getElementById('completed-count');
const clearCompletedBtn = document.getElementById('clear-completed');
// localStorage에 할일 목록 저장
function saveTodos() {
localStorage.setItem(`todos_${roomId}`, JSON.stringify(todos));
}
// 할일 추가 함수
function addTodo(text) {
todos.push({
id: Date.now(),
text,
completed: false
});
saveTodos();
}
// UI 업데이트 함수
function updateUI(filter = 'all') {
const filteredTodos = filterTodos(filter);
todoList.innerHTML = filteredTodos.map(todo => `
<div class="todo-item ${todo.completed ? 'completed' : ''}" data-id="${todo.id}">
<input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}>
<div class="todo-text" contenteditable="true">${todo.text}</div>
<div class="todo-actions">
<button class="delete-btn">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('');
totalCount.textContent = todos.length;
completedCount.textContent = todos.filter(todo => todo.completed).length;
}
// 할일 필터링 함수
function filterTodos(filter) {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}
// 이벤트 리스너 등록
todoForm.addEventListener('submit', (e) => {
e.preventDefault();
const text = todoInput.value.trim();
if (text) {
addTodo(text);
todoInput.value = '';
updateUI();
}
});
todoList.addEventListener('click', (e) => {
const todoItem = e.target.closest('.todo-item');
if (!todoItem) return;
const id = Number(todoItem.dataset.id);
const todo = todos.find(t => t.id === id);
if (e.target.classList.contains('todo-checkbox')) {
todo.completed = e.target.checked;
saveTodos();
updateUI();
} else if (e.target.closest('.delete-btn')) {
if (confirm('이 항목을 삭제하시겠습니까?')) {
todos = todos.filter(t => t.id !== id);
saveTodos();
updateUI();
}
}
});
todoList.addEventListener('focusout', (e) => {
if (e.target.classList.contains('todo-text')) {
const todoItem = e.target.closest('.todo-item');
const id = Number(todoItem.dataset.id);
const todo = todos.find(t => t.id === id);
const newText = e.target.innerText.trim();
if (newText && newText !== todo.text) {
todo.text = newText;
saveTodos();
} else {
e.target.innerText = todo.text;
}
}
});
filterButtons.forEach(btn => {
btn.addEventListener('click', () => {
filterButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
updateUI(btn.dataset.filter);
});
});
clearCompletedBtn.addEventListener('click', () => {
if (confirm('완료된 항목을 모두 삭제하시겠습니까?')) {
todos = todos.filter(todo => !todo.completed);
saveTodos();
updateUI();
}
});
// 주기적으로 데이터 동기화 (5초마다)
setInterval(() => {
const savedTodos = JSON.parse(localStorage.getItem(`todos_${roomId}`)) || [];
if (JSON.stringify(savedTodos) !== JSON.stringify(todos)) {
todos = savedTodos;
updateUI();
}
}, 5000);
// 초기 UI 업데이트
updateUI();
</script>
</body> </html>`
이 코드는 HTML, CSS, JavaScript를 모두 포함하고 있습니다. 주요 기능들을 살펴보겠습니다.
주요 기능 설명
- 실시간 동기화: setInterval 함수를 사용하여 5초마다 localStorage의 데이터와 현재 앱의 데이터를 비교하고 동기화합니다.
- URL 공유: "URL 공유하기" 버튼을 클릭하면 현재 페이지의 URL을 클립보드에 복사합니다. 이 URL을 다른 사람과 공유하면 같은 Todo 리스트에 접근할 수 있습니다.
- 고유한 룸 ID: URL의 해시값을 이용해 고유한 룸 ID를 생성합니다. 이를 통해 여러 개의 독립적인 Todo 리스트를 만들 수 있습니다.
- 로컬 스토리지 사용: 브라우저의 localStorage를 이용해 데이터를 저장합니다. 이를 통해 페이지를 새로고침해도 데이터가 유지됩니다.
- 반응형 디자인: 모바일 기기에서도 사용하기 편하도록 반응형 디자인을 적용했습니다.
사용 방법 (30단계)
- GitHub 계정에 로그인합니다.
- 새로운 저장소(repository)를 생성합니다.
- 저장소 이름을 입력합니다 (예: todo-list-app).
- "Add a README file" 옵션을 체크합니다.
- "Create repository" 버튼을 클릭합니다.
- 생성된 저장소 페이지에서 "Add file" 버튼을 클릭합니다.
- "Create new file"을 선택합니다.
- 파일 이름을 "index.html"로 입력합니다.
- 위에서 제공한 HTML 코드를 복사하여 붙여넣습니다.
- 페이지 하단의 "Commit new file" 버튼을 클릭합니다.
- 저장소 설정(Settings) 페이지로 이동합니다.
- 왼쪽 메뉴에서 "Pages"를 클릭합니다.
- "Source" 섹션에서 "main" 브랜치를 선택합니다.
- "Save" 버튼을 클릭합니다.
- GitHub Pages URL이 생성될 때까지 기다립니다 (몇 분 소요될 수 있습니다).
- 생성된 URL을 클릭하여 Todo List 앱에 접속합니다.
- 입력 필드에 할 일을 입력합니다.
- "추가" 버튼을 클릭하여 새로운 할 일을 추가합니다.
- 체크박스를 클릭하여 완료된 항목을 표시합니다.
- 할 일 텍스트를 클릭하여 직접 수정합니다.
- 삭제 버튼(휴지통 아이콘)을 클릭하여 항목을 삭제합니다.
- 필터 버튼을 사용하여 전체, 진행 중, 완료된 항목을 분류합니다.
- "완료 항목 삭제" 버튼을 클릭하여 완료된 항목을 일괄 삭제합니다.
- "URL 공유하기" 버튼을 클릭하여 현재 페이지의 URL을 복사합니다.
- 복사된 URL을 다른 기기나 사람과 공유합니다.
- 공유받은 URL로 접속하여 동일한 Todo List에 접근합니다.
- 여러 기기에서 동시에 접속하여 실시간으로 변경사항을 확인합니다.
- 브라우저를 닫았다가 다시 열어도 데이터가 유지되는지 확인합니다.
- 모바일 기기에서 접속하여 반응형 디자인을 확인합니다.
- 필요에 따라 URL의 해시값을 변경하여 새로운 Todo List를 생성합니다.
이 Todo List 앱은 GitHub Pages를 통해 배포되어 별도의 서버 설정 없이도 여러 기기에서 동일한 목록을 공유할 수 있습니다. localStorage를 사용하여 데이터를 저장하고, URL 해시를 통해 고유한 목록을 관리합니다. 주기적인 데이터 동기화를 통해 실시간에 가까운 업데이트를 제공하며, 반응형 디자인으로 모바일 환경에서도 편리하게 사용할 수 있습니다[1].
'IT' 카테고리의 다른 글
디지털 광고 비즈니스에서의 데이터 파이프라인, 데이터 생산자, 데이터 소비자 (0) | 2025.01.18 |
---|---|
데이터 파이프라인, 데이터 생산자, 데이터 소비자에 대한 종합적인 이해 (0) | 2025.01.18 |
Google Sheets의 Gemini 기반 향상된 스마트 채우기 기능 사용하기 (0) | 2025.01.18 |
Gemini를 활용한 Google Workspace 생산성 향상 가이드 (0) | 2025.01.18 |
Google Vids에서 AI를 활용한 비디오 제작 가이드_Help me create (0) | 2025.01.18 |