JavaScript

[vanilla practice] Todo List 실습

오류확인자 2024. 11. 6. 17:35

이번 리액트 수업하기 전, 간단하게 자바스크립트로 Todo List를 실습해보는 것이다.

나는 사실 자바스크립트로는 로그인, 회원가입을 제외한 기능을 구현해본 적이 없다. 즉 게시물이라던지 화면에서 삭제하고, 그런걸 제대로 해본 적이 없다. 그래서 이번 실습을 할 때, 코드는 이해가 되었으나 뭔가 바로 구현을 하기에는 좀 어려움이 있었다. 

일단 순서대로 진행을 해보겠다. 아래 기본적인 화면이다. 

할일을 적는 input과, 추가 버튼, 그리고 리스트와 삭제 버튼이 있다. 차근차근 진행해보겠다.

일단 html 코드이다.

<body>
  <div id="todo">
    <h1>Todo List - 목록 조회 :)</h1>
    <p>파일 경로: <span id="filepath"></span></p>
    </header>
    <div id="main">
      <div id="container">
        <ul>
          <li>
            <h2>쇼핑 목록</h2>
            <div class="todoinput">
              <input type="text" autofocus">
              <button type="button">추가</button>
            </div>
            <ul class="todolist">
              <li>
                <span>1</span>
                <span><s>샘플1</s></span>
                <button type="button">삭제</button>
              </li>
              <li>
                <span>2</span>
                <span>샘플2</span>
                <button type="button">삭제</button>
              </li>
              <li>
                <span>3</span>
                <span>샘플3</span>
                <button type="button">삭제</button>
              </li>
            </ul>
          </li>
        </ul>
      </div>
    </div>

 

  <script type="text/javascript">
    document.querySelector('#filepath').textContent = `ch${document.URL.split('/ch')[1]}index.html`;

    // 샘플 목록
    let itemList = [
      { no: 1, title: '두부', done: true },
      { no: 2, title: '계란', done: false },
      { no: 3, title: '라면', done: true },
    ];

 

위는 기본적인 리스트이다.

 

1. 목록 출력

여기에 이제 ul 요소를 선택해서 변수에 저장하여 ul노트를 통해 조작을 해보자.

const todoListElem = document.querySelector('.todolist');

 

일단 화면에 나와있듯이 기존에 있던 리스트를 화면에서 삭제해보자

   // 기존 목록 삭제
    while (todoListElem.firstChild) {
      todoListElem.firstChild.remove();
    }

 

while 반복문을 통해 첫 번째 요소가 모두 삭제 될 때까지, 반복한다. ul 안에 li 몇개가 있던 삭제를 한다.

 

삭제를 했으니, 리스트를 추가해야겠지. 근데 여기서 추가하는 방법이 여러가지가 있지만,

대표적으로 2가지를 뽑아서 진행하겠다.

첫 번째는 innerHTML을 이용하여 동적으로 생성하는 방법과  두 번째 방법은 요소를 DOM으로 선택하여 진행하는 방법이 있다.

첫 번째 방법은 간단하다.

 

아래와 같이 동적으로 생성을 한 후, 만약 추가가 된다면

 function getTodoItemElem(item) {
      // return (`
      // <li>
      //   <span>${item.no}</span>
      //   <span>${item.done ? `<s>${item.title}</s>` : item.title}</span>
      //   <button type="button" onclick="deleteItem()">삭제</button>
      // </li>

 

아래와 배열을 돌면서 생성하는 것이다.

    itemList.forEach((item, index) => {
      const liElem = getTodoItemElem(item);

      todoListElem.innerHTML += liElem;
    })

 

근데 첫 번째 방법이 아닌 두 번재 방법으로 구현해보자, 첫 번째 방법은 많이 사용했으며, 이는 이벤트 처리하기가 좀 어려워진다.

 itemList.forEach((item, index) => {
      const liElem = getTodoItemElem(item);
   
      todoListElem.appendChild(liElem);

    })

 

노가다 이지만 각 요소를 이런 식으로 할당을 해주었다.

// element Node
      // <li>
      const liElem = document.createElement('li');

      // <span>
      const noElem = document.createElement('span');

      // <span>
      const titleElem = document.createElement('span');

      // <button>
      const deleteElem = document.createElement('button');

      // 1
      const noTxt = document.createTextNode(item.no);

      // 샘플1
      const titleTxt = document.createTextNode(item.title);

      // 삭제
      const deleteTxt = document.createTextNode('삭제');

      deleteElem.appendChild(deleteTxt);

 

각 li, span, button 그리고 숫자에 들어갈 데아터, 타이틀, 삭제를 변수에 할당해준다.

그리고 아래와 같이 각각의 줄에 노드를 추가하여 생성하는 것이다.

 // <span>1</span>
      noElem.appendChild(noTxt);
      // <span><s>샘플1</s></span>
      if (item.done) { // 완료
        // <s>
        const sElem = document.createElement('s');
        sElem.appendChild(titleTxt);
        
        // <span><s>샘플1</s></span>
        titleElem.appendChild(sElem);

      } else { // 미완료
        // <span>샘플1</span>
        titleElem.appendChild(titleTxt);
      }
      // <button type="button">
      deleteElem.setAttribute('type', 'button');

      // <button type="button">삭제</button>
      deleteElem.appendChild(deleteTxt);

      // <li><span>1</span></li>
      liElem.appendChild(noElem);

      // <li>
      //   <span>1</span>
      //   <span><s>샘플1</s></span>
      // </li>
      liElem.appendChild(titleElem);

      // <li>
      //   <span>1</span>
      //   <span><s>샘플1</s></span>
      //   <button type="button">삭제</button>
      // </li>
      liElem.appendChild(deleteElem);

      // <li data-no="1">
      //   <span>1</span>
      //   <span>샘플1</span>
      //   <button type="button">삭제</button>
      // </li>
      liElem.setAttribute('data-no', item.no);

      // <li data-no="1">
      //   <span>1</span>
      //   <span onclick="toggleDone(1)">샘플1</span>
      //   <button type="button">삭제</button>
      // </li>
      titleElem.setAttribute('onclick', `toggleDone(${item.no})`);

      // <li data-no="1">
      //   <span>1</span>
      //   <span onclick="toggleDone(1)">샘플1</span>
      //   <button type="button" onclick="deleteItem(1)">삭제</button>
      // </li>
      deleteElem.setAttribute('onclick', `deleteItem(${item.no})`);

      return liElem;

 

2. 등록

추가 버튼에 onclick으로 이벤트 함수를 추가해준다.

아래 코드와 같이 이벤트 함수에 todoinput안에 있는 input 요소를 선택하여, 이 값이 빈값이 아니면 아이템을 추가해주고, 그리고 input은 빈값으로 출력한다. 

그리고 다시 포커스 할 수 있도록 한다. 여기에 엔터 이벤트를 추가해서 엔터를 입력했을 때, 이벤트 함수가 작동 될 수 있도록 만든 것이다.

// '추가' 클릭 이벤트 핸들러
    const handleAdd = () => {
      const inputElem = document.querySelector('.todoinput > input');
      if (inputElem.value.trim() !== '') {
        addItem(inputElem.value);
        inputElem.value = '';
        inputElem.focus();
      }
    };

    // 엔터 이벤트
    const handleAddKeyup = (event) => {
      if (event.key === 'Enter') handleAdd();
    };

 

아래 코드는 이제 투두를 추가시켜주는 로직이다.

중간에 번호 부분에서

 no: itemList[itemList.length - 1].no + 1 이 부분은 itemList[itemList.length - 1]는 배열의 마지막을 뜻하며, 이를 통해 마지막 요소를 가져올 수 있다. 그리고 .no + 1 이 부분은 마지막 값에 1을 더하면서 이전 번호보다 1을 더하여 배열을 추가시키는 것이다.

그래서 push 메서드는 통해 데이터 갱신을 한다.

그리고 맨 아래 코드이 경우 이제 화면에 출력하는 것이다. 

// 할일 추가
    function addItem(title) {
      const item = {
        no: itemList[itemList.length - 1].no + 1,
        title,
        done: false,
      };

      // TODO: 데이터 갱신
      // 맨 끝 아이템 추가
      itemList.push(item);

      // TODO: 화면 갱신
      const liElem = getTodoItemElem(item);
      const todoListElem = document.querySelector('.todolist');
      todoListElem.appendChild(liElem);

 

 

3. 완료 / 미완료 처리

위와 같이 완료가 되서 클릭하면 밑줄이 되고, 그게 아니면 저런식으로 되는 건데

이는 토글 형식으로 하면 된다.

 

	function toggleDone(no) {

      // TODO: 데이터 갱신
      let selectedItem = itemList.find(item => item.no === no);
      selectedItem.done = !selectedItem.done;

 

위 코드와 같이 find 메서드를 통해, item의 숫자가 no 숫자랑 같다면 그것만 뽑아주는 것이다. 이 항목만 검사를 반환하고 다른 항목은 검사하지 않는다.

결국 번호를 찾아서 클릭 시 !selectedItem.done으로 false로 바꿔주고, 또 클릭 하면 true로 바꿔주는 것이다.

그리고 이것은 이제 화면을 갱신 하는건데, 아래 와 같이 요소를 노드 생성해주고, selectedItem.done이 true인지, false로 밑줄 생성과 아닌 것을 나눈 것이다.

// TODO: 화면 갱신
      const selectedLiElem = document.querySelector(`.todolist > li[data-no="${no}"]`);
      // <li>
      //   <span>2</span>
      //   <span>샘플2</span>
      //   <button type="button">삭제</button>
      // </li>
      const titleSpanElem = selectedLiElem.children[1];
      
      if (selectedItem.done) {
        // <span>샘플2</span> -> <span><s><샘플2</s></span>
        const sElem = document.createElement('s');
        
        //<s>샘플2</s>
        sElem.appendChild(titleSpanElem.firstChild);
        titleSpanElem.appendChild(sElem);
        
      } else {
        // <span><s><샘플2</s></span> -> <span>샘플2</span>
        titleSpanElem.appendChild(titleSpanElem.firstChild.firstChild);
        titleSpanElem.firstChild.remove();
      }
    }

 

4. 할 일 삭제

이것은 이제 삭제 버튼을 누르면 삭제를 시키는 것인데, 저 filter를 통해서 item.no 와 no의 숫자가 같다면 제외하고, 그 나머지를 새로운 배열이 생성이 된다. 즉 내가 클릭한 항목만 비교를 하여 삭제를 시키는 것이다.

 function deleteItem(no) {
      // TODO: 데이터 갱신
      itemList = itemList.filter(item => item.no !== no);

      // TODO: 화면 갱신
      const selectedLiElem = document.querySelector(`.todolist > li[data-no="${no}"]`);
      selectedLiElem.remove();
    }

 

그리고 화면 갱신은 이제 번호를 실질적으로 삭제를 시키는 것이다. 화면에서 삭제되는 것을 출력 시키는 코드이다.

 

위에 할 일 삭제의 경우는 다른 방법이 있다. 그 방법에 대해서는 추후 다시 업로드를 하겠다.