멘토링 과제 : TODO list 제작기

SeanKim·2023년 12월 8일
post-thumbnail

코드잇 스프린트 : 프론트엔드 2기를 수강하며 작성하였습니다.

프로젝트의 시작 배경

팀 프로젝트가 시작 하기 전 멘토님이 새로운 구현 과제를 내주셨다.
바로 TODO list

꽤 재밌는 프로젝트가 될 것 같다.

프로젝트 시작!

요구사항

데모사이트 확인해보기

할일을 하나 적어보니, li 태그에 input, label, button이 생성되었다.

코드 읽어보기


현재 있는 것은 index.htmlcss, 그리고 조금의 assets 이미지들이다.
요구사항과 비교해보며, 어떤 태그에, 어떻게 줘야될 지 한번 생각해보자.

inputul

 <main>
        <input class="toggle-all" type="checkbox" />
        <ul id="todo-list" class="todo-list"></ul>

main 태그의 아래에는 input태그와, 리스트를 저장할 ul 태그가 있다.

todo list에 todoItem을 키보드로 입력하여 추가하기

키보드로 Input에 입력한 할일을 ul 태그에 li로 넣어주면 될 것 같다.

클래스는 데모사이트에서 확인한 대로 지정해주면 될 것 같다.
일단 이대로 하드코딩 해보자.

<ul id="todo-list" class="todo-list">
          <li class="todo-item">
            <input type="checkbox" class="toggle" false />
            <label class="label">할일1</label>
            <button class="destroy"></button>
          </li>
        </ul>

예쁘게 html 문서에 하드코딩 해봤다.

할일을 했는지 체크하는 체크박스와, 할일 목록, 그리고 삭제 버튼이 생겼다.

<li class="todo-item completed">
            <input type="checkbox" class="toggle" checked />
            <label class="label">할일1</label>
            <button class="destroy"></button>
          </li>

요구사항 대로 li에 completed 클래스를 추가하고, input에 checked 속성을 추가해봤다.

체크 박스가 표시되고, 할일에 밑줄이 쳐진다.

이대로 한번 만들어보자

src 디렉토리를 만들고, index.js 파일을 생성했다.

querySelector 함수로 만들기

우선 DOM 요소를 많이 다룰 것 같으므로, 선택자 $ 함수를 만들어야 할 것 같다.

const $ = (element) => document.querySelector(element);

input 제출 이벤트

const dummyCountArray = []; // 더미 배열

const handleSubmit = (e) => {
  e.preventDefault();

  // 새 li 생성
  const newItem = createNewItem($('#new-todo-title').value, dummyCountArray.length);
  dummyCountArray.push(newItem);

  $('#new-todo-title').value = '';

  checkListCount(); // 개수 체크
};

일단 새로고침을 막고,
새로운 list 아이템을 생성한다. 인자로는 input의 값과 배열의 길이를 줬다.
배열의 길이를 주는 이유는 후술.

그리고 input의 값을 초기화 해주고, 추가된 할일 목록을 계산한다.

createNewItem


const createNewItem = (value, index) => {
  const li = document.createElement('li');
  li.setAttribute('data-index', String(index));

  const div = document.createElement('div');
  div.classList.add('view');

  const input = document.createElement('input');
  input.classList.add('toggle');
  input.setAttribute('type', 'checkbox');

  const label = document.createElement('label');
  label.classList.add('label');
  label.textContent = value;

  const button = document.createElement('button');
  button.classList.add('destroy');

  div.append(input, label, button);
  li.append(div);
  $('.todo-list').append(li);

  return li;
};

템플릿 대로 요소를 생성하고, 필요한 클래스명과 값을 적용해준다. 그리고 리스트에 추가해준다.
배열의 길이를 받아온 이유는 li 태그에 data-index 속성을 주어 구별하기 위함이다.

checkListCount

const checkListCount = () => {
  $('.todo-count > strong').textContent = $('.todo-list').childElementCount;
};


총 할일 개수를 확인해서 반영해주는 함수다.
ul 태그의 자식 요소 개수를 textContent로 넣어줬다.

중간 점검


여기까지는 잘 만들어졌다.

할일 목록 클릭 이벤트


const handleCheckBoxClick = (e) => {
  // 부모 li에 담긴 data-index 값 가져오기
  const indexNum = e.target.parentElement.parentElement.dataset['index'];
  if (!indexNum) return;

  const clickedTarget = e.target;

  switch (clickedTarget.className) {
    case 'toggle':
      // 해당 li 완료하기
      $(`[data-index="${indexNum}"]`).classList.toggle('completed');
      $(`[data-index="${indexNum}"] > div > input`).toggleAttribute('checked');
      break;

    case 'destroy':
      // 해당 li 삭제하기
      $(`[data-index="${indexNum}"]`).remove();
      checkListCount();
      break;

    default:
      break;
  }
};

우선 클릭한 대상의 부모의 부모요소, 즉 li 태그의 data-index 값을 가져온다.
얼리 리턴으로 불필요한 동작을 막았다.

클릭한 요소를 클래스명으로 구분해 동작 하도록 했다.

toggle : 할일을 했는지 확인 하는 체크박스

체크박스를 클릭하면, 부모 li 태그에 'completed' 클래스를 추가한다.
inputchecked 속성을 추가해준다.

destroy : 삭제 버튼

해당 li 태그를 할일 목록에서 지운다.
그리고 checkListCount로 개수를 갱신해준다.

필터 버튼 클릭 이벤트

const handleFilterClick = (e) => {
  const clickedTarget = e.target;
  if (!clickedTarget.tagName === 'LI') return;

  const lists = $('.todo-list').children;

  // 버튼에 따른 리스트 보이고 가리기
  if (clickedTarget.classList.contains('all')) {
    for (let i = 0; i < lists.length; i++) {
      lists[i].style.display = 'block';
    }
  }
  if (clickedTarget.classList.contains('active')) {
    for (let i = 0; i < lists.length; i++) {
      lists[i].classList.contains('completed')
        ? (lists[i].style.display = 'none')
        : (lists[i].style.display = 'block');
    }
  }
  if (clickedTarget.classList.contains('completed')) {
    for (let i = 0; i < lists.length; i++) {
      lists[i].classList.contains('completed')
        ? (lists[i].style.display = 'block')
        : (lists[i].style.display = 'none');
    }
  }
};

마찬가지로, 얼리 리턴을 활용해 다른 버튼을 누르면 동작을 끝낸다.
그리고 필터 리스트의 클래스 명에 따라 동작을 분기한다.

lists 에는 할일 목록 li 태그를 담았다. 참고로 HTML Collection이라 유사배열이다.

전체를 눌렀을 때

lists를 순회하며 전부 display: block을 적용해 보이도록 했다.

active : 아직 안한 목록 보여주기

lists 를 순회하며 '완료했다'는 의미의 completed 클래스가 있는지 확인하고, 있으면 display:none으로 가려준다. 완료하지 않은 목록들은 보여주도록 했다.

completed : 완료한 목록 보여주기

lists를 순회하며 '완료했다'는 의미의 completed 클래스가 있는지 확인하고, 있으면 display:block으로 표시해준다. 없으면 안보이게 가려주도록 했다.

더블클릭 이벤트

const handleDoubleClick = (e) => {
  const clickedTarget = e.target;
  if (!clickedTarget.tagName === 'LABEL') return;

  const indexNum = clickedTarget.parentElement.parentElement.dataset['index'];
  $(`[data-index="${indexNum}"]`).classList.toggle('editing');

  createInput(indexNum);

  $(`[data-index="${indexNum}"] .edit`).addEventListener('keydown', handleKeyDown);
};

마찬가지로 얼리 리턴을 활용해 불필요한 동작을 막았다.
더블클릭한 위치의 부모 li 태그의 data-index 값을 가져왔고, 이를 활용해 input요소를 생성해줬다.

생성된 input 요소에는 키보드 이벤트리스너를 달아줬다.

createInput

const createInput = (indexNum) => {
  const input = document.createElement('input');
  input.classList.add('edit');

  input.value = $(`[data-index="${indexNum}"] > div > label`).textContent;
  $(`[data-index="${indexNum}"]`).append(input);
};

인풋 태그를 생성하도록 했다.
지정된 클래스명을 적용해줬고, 기존의 값을 기억하도록 inputvalue에 현재 아이템 label의 값을 넣어줬다.

키보드 이벤트

const handleKeyDown = (e) => {
  const indexNum = e.target.parentElement.dataset['index'];
  if (e.key === 'Enter') {
    $(`[data-index="${indexNum}"] > div > label`).textContent = e.target.value;
    $(`[data-index="${indexNum}"]`).classList.remove('editing');
    $(`[data-index="${indexNum}"] .edit`).remove();
  }
  if (e.key === 'Escape') {
    $(`[data-index="${indexNum}"]`).classList.remove('editing');
    $(`[data-index="${indexNum}"] .edit`).remove();
  }
};

인풋 창에서 엔터 클릭시 현재 값을 기존 li 태그에 적용시켜주고, 방금 작성한 input을 삭제시켜줬다.

인풋 창에서 esc 클릭시 그냥 input을 삭제시키고, 기존의 li 태그가 보이도록 해줬다.

결과




후기

한줄로 요약하자면...

도대체 리액트 없을 때 개발 어떻게 한거임?

생각보다 어려웠다.
리액트를 배워서 그런지 완료한 상태나, 리스트들의 반복되는 것을 보면서
'useState나 useEffect가 있다면 더 편할텐데...',
'jsx로 li를 반복 시키면 좋을 텐데...',
'props를 활용하면 쉬울 것 같은데...'
와 같은 생각이 아른아른 거렸다.

뭔가 함수로 빼서 재사용을 하고 싶어도, 이것저것 다른 부분이 있어 만들기 애매했고,
요소의 데이터 값을 가져오려고 해도 부모요소에 왔다갔다 해서 가져와야 하는 등, 불편한 점도 많았다.

또 어떻게 어찌저찌 만들었지만, 저번 숫자야구 때 보다는 코드도 만족스럽게 짰다는 느낌은 들지 않았던 것 같다.

이번에도 스스로 정말 자바스크립트를 잘 하고 있는게 맞는지, 되돌아 볼 수 있는 시간이였다...

profile
프론트엔드 공부 중

0개의 댓글