IT

실시간 동기화 가능한 Todo List 앱 만들기

esmile1 2025. 1. 18. 07:05

실시간 동기화 가능한 Todo List 앱 만들기

안녕하세요! 오늘은 여러 기기에서 실시간으로 동기화되는 Todo List 앱을 만드는 방법에 대해 알아보겠습니다. 이 앱은 GitHub Pages를 이용해 배포하므로 별도의 서버 설정 없이도 간편하게 사용할 수 있습니다.

주요 기능

  1. 실시간 동기화
  2. 여러 기기에서 접근 가능
  3. 간편한 URL 공유
  4. 모바일 최적화 UI
  5. 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를 모두 포함하고 있습니다. 주요 기능들을 살펴보겠습니다.

주요 기능 설명

  1. 실시간 동기화setInterval 함수를 사용하여 5초마다 localStorage의 데이터와 현재 앱의 데이터를 비교하고 동기화합니다.
  2. URL 공유: "URL 공유하기" 버튼을 클릭하면 현재 페이지의 URL을 클립보드에 복사합니다. 이 URL을 다른 사람과 공유하면 같은 Todo 리스트에 접근할 수 있습니다.
  3. 고유한 룸 ID: URL의 해시값을 이용해 고유한 룸 ID를 생성합니다. 이를 통해 여러 개의 독립적인 Todo 리스트를 만들 수 있습니다.
  4. 로컬 스토리지 사용: 브라우저의 localStorage를 이용해 데이터를 저장합니다. 이를 통해 페이지를 새로고침해도 데이터가 유지됩니다.
  5. 반응형 디자인: 모바일 기기에서도 사용하기 편하도록 반응형 디자인을 적용했습니다.

사용 방법 (30단계)

  1. GitHub 계정에 로그인합니다.
  2. 새로운 저장소(repository)를 생성합니다.
  3. 저장소 이름을 입력합니다 (예: todo-list-app).
  4. "Add a README file" 옵션을 체크합니다.
  5. "Create repository" 버튼을 클릭합니다.
  6. 생성된 저장소 페이지에서 "Add file" 버튼을 클릭합니다.
  7. "Create new file"을 선택합니다.
  8. 파일 이름을 "index.html"로 입력합니다.
  9. 위에서 제공한 HTML 코드를 복사하여 붙여넣습니다.
  10. 페이지 하단의 "Commit new file" 버튼을 클릭합니다.
  11. 저장소 설정(Settings) 페이지로 이동합니다.
  12. 왼쪽 메뉴에서 "Pages"를 클릭합니다.
  13. "Source" 섹션에서 "main" 브랜치를 선택합니다.
  14. "Save" 버튼을 클릭합니다.
  15. GitHub Pages URL이 생성될 때까지 기다립니다 (몇 분 소요될 수 있습니다).
  16. 생성된 URL을 클릭하여 Todo List 앱에 접속합니다.
  17. 입력 필드에 할 일을 입력합니다.
  18. "추가" 버튼을 클릭하여 새로운 할 일을 추가합니다.
  19. 체크박스를 클릭하여 완료된 항목을 표시합니다.
  20. 할 일 텍스트를 클릭하여 직접 수정합니다.
  21. 삭제 버튼(휴지통 아이콘)을 클릭하여 항목을 삭제합니다.
  22. 필터 버튼을 사용하여 전체, 진행 중, 완료된 항목을 분류합니다.
  23. "완료 항목 삭제" 버튼을 클릭하여 완료된 항목을 일괄 삭제합니다.
  24. "URL 공유하기" 버튼을 클릭하여 현재 페이지의 URL을 복사합니다.
  25. 복사된 URL을 다른 기기나 사람과 공유합니다.
  26. 공유받은 URL로 접속하여 동일한 Todo List에 접근합니다.
  27. 여러 기기에서 동시에 접속하여 실시간으로 변경사항을 확인합니다.
  28. 브라우저를 닫았다가 다시 열어도 데이터가 유지되는지 확인합니다.
  29. 모바일 기기에서 접속하여 반응형 디자인을 확인합니다.
  30. 필요에 따라 URL의 해시값을 변경하여 새로운 Todo List를 생성합니다.

이 Todo List 앱은 GitHub Pages를 통해 배포되어 별도의 서버 설정 없이도 여러 기기에서 동일한 목록을 공유할 수 있습니다. localStorage를 사용하여 데이터를 저장하고, URL 해시를 통해 고유한 목록을 관리합니다. 주기적인 데이터 동기화를 통해 실시간에 가까운 업데이트를 제공하며, 반응형 디자인으로 모바일 환경에서도 편리하게 사용할 수 있습니다[1].