ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JavaScript] 투두리스트 만들기 - 생성한 아이템 업데이트하기 (2): 완료 상태 업데이트 기능
    만학도 프로젝트/JavaScript TodoList 2026. 3. 21. 22:17

     

    지난 글에서는 실제로 업데이트하기보다는 업데이트 준비를 했었는데요.

    이번엔 정말로 데이터 업데이트 로직을 작성해 봅시다.

     

    지난 글에서 로컬스토리지에 저장할 때 수정했던 데이터 구조 기억하시나요?

    타이틀만 저장하던 객체에서 id와 isCompleted를 추가하는 작업까지 마쳤었는데요.

    이 두 데이터 속성을 활용해서 체크박스 완료 상태 업데이트하는 기능을 만들어봅시다.

     

    체크박스(input type="checkbox")의 특징 알고 가기

    본격적으로 기능을 만들기 전에 사실 지금까지 작성한 투두 리스트 화면을 보면

    (이미 해보셨을 수도 있겠지만,) 새로 추가한 할 일의 체크박스를 클릭했을 때 딱히 이벤트를 등록하지 않았는데도 UI에서 체크 표시가 생겼다 사라졌다 하는 걸 확인할 수 있습니다.

    이건 체크박스 타입을 가진 인풋 태그의 고유한 특징인데요.

    별다른 자바스크립트 코드 없이도 스스로 클릭을 감지하고 상태를 변경하는 능력을 가지고 있는 거죠.

    감사하게도 이 덕분에 우리가 자바스크립트로 해야 할 일은 더 간단한데요.

    자바스크립트로 클릭했을 때 체크박스 상태를 변하도록 하는 코드 보다,

    체크박스가 스스로 상태를 바꿀 때 그 상태를 감지해서 우리의 로컬스토리지 데이터에 저장하는 코드를 만들어야 해요.

     

    이벤트 위임(Event Delegation)으로 변화 감지하기

    그럼 이제 체크박스의 상태가 변하는 걸 자바스크립트가 감지하도록 해야 하는데요.

    여기서 한 가지 고민이 생기게 됩니다.

    감지하는 그 이벤트를 어디에 붙여줄 것인가.

    가장 직관적으로는 각 아이템에 이벤트를 붙여주는 아이디어가 있습니다.

    다만, 그렇게 하면 아이템을 생성할 때마다 이벤트를 등록해 주기 때문에 아이템을 생성할 때 필요한 동작이 하나 더 늘어나게 됩니다.

    아이템이 100개라면 이벤트 리스너가 100개가 생기게 되겠죠?

    이 방법이 절대 나쁘다거나 요즘은 성능상에 크게 문제가 있는 방법도 아니긴 하지만, 그래도 이것 보다 조금 더 괜찮은 방식으로 코드를 작성해보려고 하는데요.

     

    감지 이벤트를 각각의 아이템에 아니라 리스트에 붙이는 방법이에요.

    아이템들의 부모인 ul 태그에 todo-list라는 id를 달고 todoList라는 변수에 담아뒀었죠?

    todoList에 이벤트를 달아두면 자식 요소들에게서 일어나는 이벤트를 확인할 수가 있는데 이 방식을 활용할 거예요.

    이 방식의 장점은 아이템이 100개 1,000개가 되더라도 리스트에 붙은 이벤트 리스너 1개로 모든 처리가 가능하다는 거예요.

    혹시 좀 더 궁금하시다면 "이벤트 위임"이라는 키워드로 한번 검색해 보시는 걸 추천드려요! 정말 모르겠다 하시면 '이벤트 버블링'이라는 개념도 같이 공부하시길 권장드립니다.

    (언제가 될진 모르지만 나중에 저도 자바스크립트 이벤트와 관련해서 개념 정리글도 작성해 볼게요)

     

    자 그럼 코드를 조금 작성해 봅시다!

    // 리스트 내부에서 일어나는 상태 변화 감지 (이벤트 위임)
    todoList.addEventListener('change', (event) => {
      // 이벤트가 발생한 대상이 체크박스인지 확인
      if (event.target.classList.contains('item-checkbox')) {
        const checkbox = event.target;
        
        // 클릭된 체크박스가 속한 최상위 todo-item(li 태그) 찾기
        const todoItem = checkbox.closest('.todo-item'); 
        
        // 아이템의 id 가져오기 
        const itemId = todoItem.getAttribute('id'); 
        
        // 업데이트 된 현재 체크박스의 상태 (true/false)
        const isChecked = checkbox.checked; 
    
        // ... 이제 이 데이터들을 활용해서 로컬스토리지를 업데이트해봅시다.
      }
    });

     

    위에서부터 주석을 기준으로 코드를 하나씩 보면

    • // 리스트 내부에서 일어나는 상태 변화 감지 (이벤트 위임):
      체크박스에서 상태가 변하면 change이벤트가 발생하기 때문에 todoList에 change이벤트리스너를 만들어준 거예요.
    • // 이벤트가 발생한 대상이 체크박스인지 확인:
      이벤트가 일어난 대상이 체크박스가 맞는지를 확인하고 체크박스 일 때만 로직을 실행하도록 조건문을 만든 거예요.
    • // 클릭된 체크박스가 속한 최상위 todo-item(li 태그) 찾기:
      closest라는 메서드를 활용해서 체크박스를 감싸고 있는 가장 가까운 item을 찾는 거예요.
    • // 아이템의 id 가져오기
      로컬스토리지의 데이터를 업데이트하려면 id를 가지고 와야겠죠? 업데이트를 위한 id를 찾는 과정입니다.
    • // 업데이트된 현재 체크박스의 상태 (true/false):
      체크박스가 체크가 된 상태라면 true, 빈칸이라면 false값을 가지게 됩니다. 이 데이터를 로컬스토리지에 업데이트하는 거죠.

    로컬스토리지 데이터 업데이트하기

    이제 어떤 아이템의 체크박스가 눌렸는지도 알아냈고 체크박스의 상태도 알아낸 겁니다.

    이걸 로컬 스토리지에 있는 데이터에 업데이트만 하면 되겠죠?

    todoList.addEventListener('change', (event) => {
      if (event.target.classList.contains('item-checkbox')) {
        const checkbox = event.target;
        const todoItem = checkbox.closest('.todo-item'); 
        
        const itemId = todoItem.getAttribute('id'); 
        const isChecked = checkbox.checked; 
    
        // 기존 데이터 불러오기
        const todos = getTodos(); 
        
        // 내가 클릭한 아이템이 배열에서 몇 번째(index) 방에 있는지 찾기
        const targetIndex = todos.findIndex(todo => todo.id === itemId);
        // 만약 제대로 찾았다면 (-1이 아니라면) 
        if (targetIndex !== -1) {
          // 그 방에 있는 객체의 isCompleted 속성만 콕 집어서 isChecked 상태로 덮어쓰기!
          todos[targetIndex].isCompleted = isChecked;
          
          // 상태가 수정된 기존 배열을 그대로 다시 로컬스토리지에 밀어넣기
          localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
        }
      }
    });

    이번에도 주석을 기준으로 간단하게 설명해 보자면

    • // 기존 데이터 불러오기:
      로컬스토리지에 있는 원본 배열을 가져옵니다.
    • // 내가 클릭한 아이템이 배열에서 몇 번째(index) 방에 있는지 찾기:
      업데이트하고자 하는 아이템이 배열의 어디에 있는지를 확인하는 과정입니다.
    • // 만약 제대로 찾았다면 (-1이 아니라면)
      findIndex메서드는 index를 찾았다면 index를, 못 찾았다면 -1을 리턴하는데요. 이 특성을 활용했어요.
    • // 그 방에 있는 객체의 isCompleted 속성만 콕 집어서 isChecked 상태로 덮어쓰기!:
      로컬스토리지에 있는 원본 배열에서 업데이트할 아이템의 상태를 업데이트해 주는 코드예요
    • // 상태가 수정된 기존 배열을 그대로 다시 로컬스토리지에 밀어 넣기:
      todos배열이 업데이트되긴 했지만 로컬스토리지에도 업데이트를 해줘야 업데이트가 완전히 끝이 나요!

     

    마무리

    여기까지 js 코드를 한 번에 정리하면 다음과 같습니다.

    const todoForm = document.getElementById('todo-form');
    const todoInput = document.getElementById('todo-input');
    const todoList = document.getElementById('todo-list');
    const todoTemplate = document.getElementById('todo-template');
    
    const STORAGE_KEY = 'todos';
    
    const getTodos = () => {
      return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
    };
    
    const saveTodoItem = (todoItem) => {
      const todoData = {
        id: todoItem.getAttribute('id'),
        title: todoItem.querySelector('.item-title').textContent,
        isCompleted: todoItem.querySelector('.item-checkbox').checked
      };
      const todos = getTodos().concat(todoData);
    
      localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
    };
    
    const createTodoItem = (todo) => {
      const todoItem = todoTemplate.content
        .cloneNode(true)
        .querySelector('.todo-item');
    
      todoItem.querySelector('.item-title').textContent = todo.title;
      todoItem.querySelector('.item-checkbox').checked = todo.isCompleted;
      todoItem.setAttribute('id', todo.id);
    
      return todoItem;
    };
    
    const addTodoItem = (todo) => {
      const todoItem = createTodoItem(todo);
      todoList.appendChild(todoItem);
      return todoItem;
    };
    
    const loadTodoItem = () => {
      getTodos().forEach((todo) => { addTodoItem(todo) });
    };
    
    const clearTodoInput = () => {
      todoInput.value = '';
    };
    
    const handleFormSubmit = (event) => {
      event.preventDefault();
    
      const inputValue = todoInput.value.trim();
    
      if (inputValue === '') return;
    
      const newTodo = {
        id: Date.now(),
        title: inputValue,
        isCompleted: false,
      };
    
      saveTodoItem(addTodoItem(newTodo));
      clearTodoInput();
    };
    
    todoList.addEventListener('change', (event) => {
      if (event.target.classList.contains('item-checkbox')) {
        const checkbox = event.target;
        const todoItem = checkbox.closest('.todo-item');
    
        const itemId = todoItem.getAttribute('id');
        const isChecked = checkbox.checked;
    
        const todos = getTodos();
    
        const targetIndex = todos.findIndex(todo => todo.id === itemId);
    
        if (targetIndex !== -1) {
          todos[targetIndex].isCompleted = isChecked;
          localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
        }
      }
    });
    
    const initTodoApp = () => {
      todoForm.addEventListener('submit', handleFormSubmit);
      loadTodoItem();
      clearTodoInput();
    };
    
    document.addEventListener('DOMContentLoaded', initTodoApp);

     

    이벤트를 등록하는 부분만 생긴거라 사실상 큰 코드변화는 없죠?

    이제 코드를 실행하고 브라우저로 들어가서 체크박스를 눌러볼까요? 그리고 새로고침도 한번 해보세요!

    투두 아이템의 상태가 로컬스토리지와 잘 연동되어 있고, 새 로고침해도 상태가 그대로 유지되어 있는 모습을 확인하실 수 있을 거예요!

     

    이번에 제 블로그에 정리되지 않은 이벤트 위임이나 findIndex 메서드 같은 개념이 등장하는 바람에 조금 내용이 어색하게 느껴질 수도 있을 것 같은데요.

    혹시 조금 개념이 너무 생소하셨다면 해당 키워드로 꼭 개념을 익혀보시는 걸 권장드려요.

     

    아무튼, 여기까지 할 일 완료 상태 업데이트 기능을 구현해 봤는데요.

    다음 글에서는 아이템 오른쪽에 붙은 Edit 버튼! 저 edit 버튼을 통해서 타이틀을 수정하는 기능을 완성해 봅시다.


    질문이나, 의문점이 드는 부분이라거나 제가 실수를 했다거나, 혹은 좀 더 좋은 방향이 있다면 댓글 부탁드립니다.

    더 좋은 글을 쓰고 제가 성장하는 데에도 큰 도움이 될 거예요! :)

    감사합니다.

    댓글

Designed by BigTop.